从0开始mobile安全!
学了一下渗透,发现我真的不适合,不喜欢那种不确定性,打了两个HTB,WP写好了我又给删了,我认为我以后不会从事这个工作。我觉得还是能看见代码的东西更适合我
现在从0刷一刷Mobile题,感觉还挺有趣的。边刷边学知识点
IDA有些快捷操作,之后再整理
XCTF mobile part1 有一说一,现在有了AI,代码审计的工作比渗透之类的好做太多了
基础Android Jadx反编译apk,打开项目后看到MainActivity(这种找main函数的思路在哪都适用)
看到MainActivity,由于是第一次学,解释详细一点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class MainActivity extends AppCompatActivity { private Button login; private EditText passWord; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.acticity_main_1); this .passWord = (EditText) findViewById(R.id.passWord); this .login = (Button) findViewById(R.id.button); this .login.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { String str = MainActivity.this .passWord.getText().toString(); Check check = new Check (); if (check.checkPassword(str)) { Toast.makeText(MainActivity.this , "Good,Please go on!" , 0 ).show(); Intent intent = new Intent (MainActivity.this , (Class<?>) MainActivity2.class); MainActivity.this .startActivity(intent); MainActivity.this .finish(); return ; } Toast.makeText(MainActivity.this , "Failed" , 0 ).show(); } }); } }
login
是登录按钮,passWord
是用户输入密码的输入框
onCreate方法先加载了 acticity_main_1.xml
这个布局文件,然后用findViewById获取界面上的password输入框和登录按钮,最后是最核心的,设置了一个点击事件监听器绑定在登录按钮上
这个监听器通过MainActivity.this.passWord.getText().toString();
读取密码输入框内容,然后调用check.checkPassword去对输入进行验证,如果校验成功:提示 "Good,Please go on!"
,跳转到 MainActivity2
,Intent是创建一个内部窗口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 this .login.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { String str = MainActivity.this .passWord.getText().toString(); Check check = new Check (); if (check.checkPassword(str)) { Toast.makeText(MainActivity.this , "Good,Please go on!" , 0 ).show(); Intent intent = new Intent (MainActivity.this , (Class<?>) MainActivity2.class); MainActivity.this .startActivity(intent); MainActivity.this .finish(); return ; } Toast.makeText(MainActivity.this , "Failed" , 0 ).show(); } });
OK继续看到checkPassword方法,pass的长度为12,重点是要满足下面这个循环
也就是每一位的pass需要满足(char) (((255 - len) - 100) - pass[len])
==0
我直接写个脚本爆破字符就完了呗
1 2 3 4 5 6 for i in range (12 ): for j in range (32 , 127 ): c = chr ((255 - i) - 100 - j) if c == '0' : print (f"{chr (j)} " ,end='' )
得到kjihgfedcba`
看到MainActivity2,调用了sendBroadcast
sendBroadcast可不是一个自定义函数,而是发送广播,广播内容为一个editText控件的字符串
广播的知识点:
https://www.runoob.com/android/android-broadcast-receivers.html
广播接收器需要实现为BroadcastReceiver类的子类,并重写onReceive()方法来接收以Intent对象为参数的消息。
满足这个要求的是GetAndChange方法,它的onReceive方法只有很短的两句,就是生成NextContent类作为主窗口
这个对象创建时触发onCreate,调用了Init和Change方法,其中Change方法将 APK 中 assets 目录下的 timg_2.zip
文件复制到 app 的数据库目录中,命名为 img.jpg
,然后用它更新界面上的一张图片。
值得注意的是,getApplicationContext().getResources().getAssets()
对应了assets/
目录
那么我们直接在jdx中导出assets/time_2.zip
文件,并修改后缀为jpg即可
图片得到flag
好像我们之前写的脚本没用呢?
我们用mumu打开apk,输入kjihgfedcba`,得到图片如下:
注意到AndroidManifest.xml中配置了一个广播接收器
允许外部应用 向这个接收器发送广播(⚠️ 这是个安全关键点)
该接收器监听的广播名称(此处是自定义的 "android.is.very.fun"
)
1 Intent intent = new Intent ("android.is.very.fun" );
只要你发出的广播 action
是 "android.is.very.fun"
,这个接收器 GetAndChange
就会被触发。
所以我们输入android.is.very.fun后就能显示flag
上面导出图片是个非预期,还是按照代码走打好基础
Android 2.0 经典的算法逆向题,学习IDA的使用!
程序逻辑 jadx,开!
看到MainActivity,发现调用了JNI.getResult,如果返回值为1则输出Great
跟进到JNI,发现getResult是个native函数
注意一下,这种加载动态链接库去调用native方法的,在资源文件一般都会有动态链接库文件,这里是libNative.so
导出出来,放到IDA分析
在export找到Java_com_example_test_ctf03_JNI_getResult方法
跳转后F5看到Java_com_example_test_ctf03_JNI_getResult反编译的c代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 bool __fastcall Java_com_example_test_ctf03_JNI_getResult (int a1, int a2, int a3) { int v3; const char *v4; char *v5; char *v6; char *v7; int i; int j; v3 = 0 ; v4 = (const char *)(*(int (__fastcall **)(int , int , _DWORD))(*(_DWORD *)a1 + 0x2A4 ))(a1, a3, 0 ); if ( strlen (v4) == 0xF ) { v5 = (char *)malloc (1u ); v6 = (char *)malloc (1u ); v7 = (char *)malloc (1u ); Init(v5, v6, v7, v4, 0xF ); if ( !First(v5) ) return 0 ; for ( i = 0 ; i != 4 ; ++i ) v6[i] ^= v5[i]; if ( !strcmp (v6, a5) ) { for ( j = 0 ; j != 4 ; ++j ) v7[j] ^= v6[j]; return strcmp (v7, "AFBo}" ) == 0 ; } else { return 0 ; } } return v3; }
简单解释一下这个代码
下面的代码可以简单的表示为v4 = env->GetStringUTFChars(...)
,也就是说v4是Java层传入的字符串参数
1 v4 = (*(int (__fastcall **)(int , int , _DWORD))(*(_DWORD *)a1 + 0x2A4 ))(a1, a3, 0 );
然后if (strlen(v4) == 0xF)
检查v4是否为15个字符
然后分配三个缓冲区,调用 Init(v5, v6, v7, v4, 0xF);
,Init函数肯定给v5,v6,v7分配值了,等下我们再看Init函数的逻辑
接着往后看,对v5进行一个Fisrt判断,如果Fisrt返回false则直接退出程序。
然后是这个循环,将v6[0..3]
与 v5[0..3]
逐位异或
1 2 3 for ( i = 0 ; i != 4 ; ++i ) v6[i] ^= v5[i]; if ( !strcmp (v6, a5) )
比对v6和a5,不过函数前面没看到a5呢?
第二轮异或,将v6[0..3]
与 v7[0..3]
异或,并比较v7与AFBo}
1 2 3 for ( j = 0 ; j != 4 ; ++j ) v7[j] ^= v6[j]; return strcmp (v7, "AFBo}" ) == 0 ;
程序分析 看下Init函数在干什么?这种直接甩给gpt看就完了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 int __fastcall Init (int result, char *a2, char *a3, const char *a4, int a5) { int v5; int v6; int v7; if ( a5 < 1 ) { v6 = 0 ; } else { v5 = 0 ; v6 = 0 ; do { v7 = v5 % 3 ; if ( v5 % 3 == 2 ) { a3[v5 / 3u ] = a4[v5]; } else if ( v7 == 1 ) { a2[v5 / 3u ] = a4[v5]; } else if ( !v7 ) { ++v6; *(_BYTE *)(result + v5 / 3u ) = a4[v5]; } ++v5; } while ( a5 != v5 ); } *(_BYTE *)(result + v6) = 0 ; a2[v6] = 0 ; a3[v6] = 0 ; return result; }
根据参数
1 Init(v5, v6, v7, v4, 0xF );
将 15 字符输入 a4
拆分成 3 组,每 3 个字符为一个 cycle,第一个放入v5,第二个放入v6,第三个放到v7:
继续看下First方法,我们知道First传入的参数是v5,经过``的处理后结果等于LN^Dl
1 2 3 4 5 6 7 8 bool __fastcall First (char *a1) { int i; for ( i = 0 ; i != 4 ; ++i ) a1[i] = (2 * a1[i]) ^ 0x80 ; return strcmp (a1, "LN^dl" ) == 0 ; }
1 2 if ( !First(v5) ) return 0 ;
写一个Python还原v5,还原的公式为(2 * a1[i]) ^ 0x80
的逆向,((b ^ 0x80) // 2)
1 2 3 4 5 6 7 8 9 10 def inverse_v5 (): target = b"LN^dl" res = [] for b in target: orig = ((b ^ 0x80 ) // 2 ) res.append(orig) return bytes (res) v5_orig = inverse_v5() print (v5_orig)
得到原来的v5为fgorv,注意for循环只修改了四位字符,所以原来的v5应该是fgorl
不过很显然,First是会改变v5的值的,所以后续应该用LN^dl运算
接着,根据下面的代码
1 2 3 for ( i = 0 ; i != 4 ; ++i ) v6[i] ^= v5[i]; if ( !strcmp (v6, a5) )
我们在ida中找一下a5在哪定义的
.rodata
区段(只读数据段 )定义了C 风格的字符串数组(const char a5[]
),起始地址是 0x00002888
,DCB分配了常量值,
我们关注到字符串的结尾DCB 0x00 ; null terminator(字符串结尾)
所以a5的值应该是 空格5-\x16a
对应的python脚本解出v6:
1 2 3 4 5 6 7 8 9 10 11 def second (): a5 = [ord (' ' ), ord ('5' ), ord ('-' ), 0x16 , ord ('a' )] v5 = [ord ('L' ), ord ('N' ), ord ('^' ), ord ('d' ), ord ('l' )] v6 = "" for i in range (0 , 4 ): t = v5[i]^a5[i] v6 += (chr (t)) v6 += 'a' print (v6) second()
得到原来的v6为l{sra
继续得到v7为asoy}
1 2 3 4 5 6 7 8 9 10 def third (): a6 = [ord ('A' ), ord ('F' ), ord ('B' ), ord ('o' ), ord ('}' )] v6 = [ord (' ' ), ord ('5' ), ord ('-' ), 0x16 , ord ('a' )] v7 = '' for i in range (0 , 4 ): t = v6[i]^a6[i] v7 += (chr (t)) v7 += '}' print (v7) third()
再写一个Init的逆向还原算法,叫gpt写就完了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def reverse_init (v5: bytes , v6: bytes , v7: bytes ) -> bytes : """ 给定 Init 拆分后的三个等长部分,按 Init 的逻辑还原原始 a4 字符串。 """ assert len (v5) == len (v6) == len (v7) a4 = bytearray () for i in range (len (v5)): a4.append(v5[i]) a4.append(v6[i]) a4.append(v7[i]) return bytes (a4) v5 = b'fgorl' v6 = b'l{sra' v7 = b'asoy}' a4 = reverse_init(v5, v6, v7) print (a4.decode())
得到flag{sosorryla}
APK逆向 jadx打开后,核心逻辑就是调用checkSN查看输入的用户名和SN是否合法
跟进checkSN,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public boolean checkSN (String userName, String sn) { if (userName == null ) { return false ; } try { if (userName.length() == 0 || sn == null || sn.length() != 22 ) { return false ; } MessageDigest digest = MessageDigest.getInstance("MD5" ); digest.reset(); digest.update(userName.getBytes()); byte [] bytes = digest.digest(); String hexstr = toHexString(bytes, "" ); StringBuilder sb = new StringBuilder (); for (int i = 0 ; i < hexstr.length(); i += 2 ) { sb.append(hexstr.charAt(i)); } String userSN = sb.toString(); return new StringBuilder ().append("flag{" ).append(userSN).append("}" ).toString().equalsIgnoreCase(sn); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return false ; } }
对用户名( "Tenshine"
)计算 MD5 哈希,然后将将哈希转为十六进制字符串 hexstr
(32 个十六进制字符)
然后for循环从hexstr中取每隔两个字符的字符,也就是偶数位的字符,最后包装成包装成 flag{...}
跟用户输入的sn做比对
那就很简单了,直接写个脚本得到sn码:
1 2 3 4 5 6 7 8 9 10 import hashlibdef generate_sn (user_name: str ) -> str : md5_bytes = hashlib.md5(user_name.encode()).hexdigest() short_md5 = '' .join(md5_bytes[i] for i in range (0 , len (md5_bytes), 2 )) return f"flag{{{short_md5} }}" print (generate_sn("Tenshine" ))
flag{bc72f242a6af3857}
人民的名义-抓捕赵德汉1-200 jadx打开,在MANIFEST.MF写了入口为CheckPassword
看到CheckPassword,里面有个main方法,调用loadCheckerObject(),然后调用checkerObject.checkPassword去验证输入
跟进到loadCheckObject
从资源路径 /ClassEnc
加载加密的 .class
文件,AES解密密钥为hexKey,然后用defineClass加载自定义字节码
我们在代码里可以看到hexKey
1 static String hexKey = "bb27630cf264f8567d185008c10c3f96" ;
资源文件也有ClassEnc
我们直接写个脚本AES解密ClassEnc,然后输出文件,再反编译就能看到ClassEnc
jadx直接导出ClassEnc可能会报错,不过Jar可以直接解压出来的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from Crypto.Cipher import AEShex_key = "bb27630cf264f8567d185008c10c3f96" with open ("ClassEnc" , "rb" ) as f: encrypted_data = f.read() key_bytes = bytes .fromhex(hex_key) cipher = AES.new(key_bytes, AES.MODE_ECB) decrypted_data = cipher.decrypt(encrypted_data) with open ("DecryptedClass.class" , "wb" ) as f: f.write(decrypted_data) print ("[+] 解密完成,输出文件为:DecryptedClass.class" )
解密出来ClassEnc如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package defpackage;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public class CheckPass implements CheckInterface { public boolean checkPassword (String input) { MessageDigest md5Obj = null ; try { md5Obj = MessageDigest.getInstance("MD5" ); } catch (NoSuchAlgorithmException e) { System.out.println("Hash Algorithm not supported" ); System.exit(-1 ); } byte [] bArr = new byte [40 ]; md5Obj.update(input.getBytes(), 0 , input.length()); byte [] hashBytes = md5Obj.digest(); return byteArrayToHexString(hashBytes).equals("fa3733c647dca53a66cf8df953c2d539" ); } private static String byteArrayToHexString (byte [] data) { int i; StringBuffer buf = new StringBuffer (); for (int i2 = 0 ; i2 < data.length; i2++) { int halfbyte = (data[i2] >>> 4 ) & 15 ; int two_halfs = 0 ; do { if (halfbyte >= 0 && halfbyte <= 9 ) { buf.append((char ) (48 + halfbyte)); } else { buf.append((char ) (97 + (halfbyte - 10 ))); } halfbyte = data[i2] & 15 ; i = two_halfs; two_halfs++; } while (i < 1 ); } return buf.toString(); } }
checkPassWord要求对输入进行MD5后等于”fa3733c647dca53a66cf8df953c2d539”,直接上cmd5解密得到monkey99
flag{monkey99}
ill-intentions 进来看到MainActivity,创建一个 IntentFilter
,监听广播事件 com.ctf.INCOMING_INTENT
注册Send_to_Activity
的自定义广播接收器。并指定 **接收广播需要有权限 Manifest.permission._MSG
**。
跟进到Send_toActivity,根据不同的msg字符串,分别调用ThisIsTheRealOne、IsThisTheRealOne、DefinitelyNotThisOne
简单的看一下ThisIsTheRealOne类
构建了一个abc组合的msg intent进行广播,这里好几个native方法
类中声明了几个 native 方法(JNI),并在 static
块加载了 hello-jni
库
创建一个 Intent
,设置 Action 为自定义广播 "com.ctf.OUTGOING_INTENT"
1 2 Intent intent = new Intent ();intent.setAction("com.ctf.OUTGOING_INTENT" );
这几行是构造 JNI 函数的三个参数:
a
是字符串资源 str2
加 "YSmks"
。
b
和 c
是通过 Utilities.doBoth(...)
处理两个字符串:
一个是 R.string.dev_name
另一个是当前类名
1 2 3 String a = getResources().getString(R.string.str2) + "YSmks" ;String b = Utilities.doBoth(getResources().getString(R.string.dev_name));String c = Utilities.doBoth(getClass().getName());
然后调用native方法 orThat(String, String, String)
,结果放入 Intent
的 "msg"
字段中。
1 intent.putExtra("msg" , ThisIsTheRealOne.this .orThat(a, b, c));
发送广播 com.ctf.OUTGOING_INTENT
,附带权限 Manifest.permission._MSG
,表明接收者必须声明该权限才能收到该广播。
1 sendBroadcast(intent, Manifest.permission._MSG);
Frida环境搭建 播段小插曲,这个题需要用到frida,这里装一下,我测试17.x有bug
1 2 3 pip install frida==16.7.19 pip install frida-tools==13.7.1 pip install objection
然后下对应版本的frida-server
https://www.python.org/downloads/windows/
怎么传上去运行呢?用到了Android 设备和计算机之间通信的命令行工具,叫adb
adb链接mumu:https://blog.csdn.net/qq_51250393/article/details/133980958
如下代表链接上了
查看系统版本
1 adb shell getprop ro.product.cpu.abi
解压后通过adb push将frida server推到设备临时目录
1 adb push frida-server-xxx /data/local/tmp/
adb shell
进入设备shell环境,cd到临时目录下,这里需要以root权限运行frida server,先mumu模拟器把手机root选项开启
https://blog.csdn.net/redrose2100/article/details/129481321
然后adb shell里面su
,就会弹出以下窗口,同意即可
给frida-server文件设置可执行权限使其可以运行
1 2 chmod 777 frida-server-xx ./frida-server-xx
这样就算运行成功了
如果显示Aborted!那是你的frida server版本装错了
之后需要转发frida的服务端口,之前运行的frida-server窗口别关,新开一个cmd窗口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 PS C:\Users\19583> adb forward tcp:27042 tcp:27042 27042 PS C:\Users\19583> adb forward tcp:27043 tcp:27043 27043 # 列出设备 PS C:\Users\19583> frida-ls-devices Id Type Name OS -------------- ------ --------------- ------------------ local local Local System Windows 10.0.26100 127.0.0.1:7555 usb SM S9110 barebone remote GDB Remote Stub socket remote Local Socket # 列出进程 # PS C:\Users\19583> frida-ps -U PID Name ---- ------------------------------------------------ 1142 adbd 1810 android.ext.services 1125 android.hardware.audio.service 1126 android.hardware.bluetooth@1.1-service.sim 1127 android.hardware.camera.provider@2.4-service 1128 android.hardware.configstore@1.1-service ...
能运行说明OK了
Frida Hook题解 回到题目,一共三个类,如果静态分析,需要把三个类往msg放的orThat输出的字符串分析出来
但是我们顺其自然,直接想办法去触发这几个if条件,不就能获取到结果了吗
甚至我还有一个思路,把代码copy出来重新跑一遍就能得到结果,不过这学不到什么技术,在此略过
这里学一下Frida Objection动态Hook的思路
Frida注入实际上写的是JS脚本
Frida objection的使用 首先frida-ps -Uai
看下要注入的包名:为com.example.hellojni
1 2 3 4 5 6 7 8 9 10 11 12 13 C:\Users\19583>frida-ps -Uai PID Name Identifier ---- --------------- ----------------------- 5830 CTF Application com.example.hellojni 3549 文件 com.android.documentsui 2303 游戏中心 com.mumu.store 4633 设置 com.android.settings - 主题装扮 com.mumu.decorate - 图库 com.android.gallery3d - 应用分身 com.netease.mumu.cloner - 浏览器 com.android.browser - 相机 com.android.camera2 - 虚拟身份管理 com.nemu.oaidmanager
已注入Frida的情况下(客户端执行frida-server)使用objection,因为server已经链接上了client,所以直接在本机执行objection就行了
1 2 objection -g com.example.hellojni explore android hooking list activities
android hooking list activities可以看到有四个Activity
随便启动一个Activity
1 android intent launch_activity com.example.application.IsThisTheRealOne
android intent launch_activity com.example.application.IsThisTheRealOne
用于尝试启动一个 Activity
android intent launch_activity
:命令告诉 Objection 构造一个 Intent
并使用 startActivity()
来启动目标界面
com.example.application.IsThisTheRealOne
:是你想要启动的 Activity 的类全名(包名+类名)
实际上等于adb的:am start -n com.example.hellojni/com.example.application.IsThisTheRealOne
因为代码逻辑中,点击后实际上是调用一个sendBroadcast发送广播,所以并没有任何消息弹出,这个时候需要写一个JS去Hook Activity的结果
比如ThisIsTheRealOne,实际上程序运行的结果为orThat,我们可以直接用objection提供的demo改一下进行Hook
如下代码,就是hook了orThat,在函数开始和函数结束插入代码分别打印入参和return值
1 2 3 4 5 6 7 var ThisIsTheRealOneHandler = Java .use ('com.example.application.ThisIsTheRealOne' ) ThisIsTheRealOneHandler .orThat .overload ('java.lang.String' , 'java.lang.String' , 'java.lang.String' ).implementation = function (arg0, arg1, arg2 ) { console .log ('进入 ThisIsTheRealOneHandler 函数,参数1: ' + arg0 + " 参数2: " + arg1 + " 参数3: " + arg2) var ret = this .orThat (arg0, arg1, arg2) console .log ('完成 ThisIsTheRealOneHandler 函数,返回值:' + ret ) return ret }
我们这里调用了Java.use('com.example.application.ThisIsTheRealOne')
的时候,就会触发ThisIsTheRealOne的static块,也就实现了so的加载
这是一个通用的demo,以后写的frida也能从这基础上改
类似于java agentmain,frida实现的插桩可以位于native层,所以切面会广得多
类似的,去hook DefinitelyNotThisOne definitelyNotThis方法和IsThisTheRealOne perhapsThis方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function main ( ) { Java .perform (function ( ) { var DefinitelyNotThisOneHandler = Java .use ('com.example.application.DefinitelyNotThisOne' ) DefinitelyNotThisOneHandler .definitelyNotThis .implementation = function (arg0, arg1 ) { console .log ('进入 DefinitelyNotThisOneHandler 函数,参数1: ' + arg0 + " 参数2: " + arg1) var ret = this .definitelyNotThis (arg0, arg1) console .log ('完成 DefinitelyNotThisOneHandler 函数,返回值: ' + ret ) return ret } var ThisIsTheRealOneHandler = Java .use ('com.example.application.ThisIsTheRealOne' ) ThisIsTheRealOneHandler .orThat .overload ('java.lang.String' , 'java.lang.String' , 'java.lang.String' ).implementation = function (arg0, arg1, arg2 ) { console .log ('进入 ThisIsTheRealOneHandler 函数,参数1: ' + arg0 + " 参数2: " + arg1 + " 参数3: " + arg2) var ret = this .orThat (arg0, arg1, arg2) console .log ('完成 ThisIsTheRealOneHandler 函数,返回值:' + ret ) return ret } var IsThisTheRealOneHandler = Java .use ('com.example.application.IsThisTheRealOne' ) IsThisTheRealOneHandler .perhapsThis .overload ('java.lang.String' , 'java.lang.String' , 'java.lang.String' ).implementation = function (arg0, arg1, arg2 ) { console .log ('进入 IsThisTheRealOneHandler 函数,参数1: ' + arg0 + " 参数2: " + arg1 + " 参数3: " + arg2) var ret = this .perhapsThis (arg0, arg1, arg2) console .log ('完成 IsThisTheRealOneHandler 函数,返回值: ' + ret ) return ret } }) } setImmediate (main)
保存为agent.js,然后用frida去hook
1 frida -U -N com.example.hellojni -l agent.js
这个时候再利用objection进入到Activity,点击按钮后frida控制台就会打印相关内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PS C:\Users\19583> objection -d -g com.example.hellojni explore ... Agent injected and responds ok! _ _ _ _ ___| |_|_|___ ___| |_|_|___ ___ | . | . | | -_| _| _| | . | | |___|___| |___|___|_| |_|___|_|_| |___|(object)inject(ion) v1.10.2 Runtime Mobile Exploration by: @leonjza from @sensepost [tab] for command suggestions com.example.hellojni on (Samsung: 12) [usb]
IsThisTheRealOne得到flag CTF{IDontHaveABadjokeSorry}