第一次在吾爱破解上发帖,很高兴能有这么一个技术分享的平台~
笔者也是初学者,虽然网上有很多frida使用教程,但是笔者实在不太习惯先学完所有理论然后实战的过程,在笔者看来,以实战促学才是比较有效的锻炼技术的方式,所以就动手一边学,一边记录下了这篇文章
或许十年,二十年之后笔者会忘记,然后回来看自己教自己呢hh
另外,笔者也有自己的小博客,欢迎各位指点:https://blog.928330.xyz/
okok,闲言少叙,进入正题
环境:雷电模拟器9 + python3.11 + node.js + vscode
准备工作 本机安装frida(客户端)Frida的工具包是基于Python的,所以我们使用pip来安装:
python -m pip install "frida==16.5.2" frida-tools注意这里我们选定的frida版本是16.5.2,这还多亏一位前辈的教诲:
[!note]
“不要用这么高版本的frida,不稳定而且api经常换”
当然你可以酌情选择别的版本
对于小白来说,安装不当后面就会产生很多很多的坑,比如我......
(所以之后看见截图版本是17.5.0不要奇怪,我不想改图了)
安装完成后,输入frida --version,如果能看到版本号,说明装好了
记住这个版本号
模拟器安装frida(服务端) adb连接模拟器最好新建一个模拟器,防止莫名其妙的bug
在设置里打开root和本地连接ADB:

进入安装目录(LDPlayer9/,此目录下会有adb.exe),打开cmd
查看当前设备:
adb devices[!tip]
如果这一步有问题,并且网上搜到的任何办法都不能解决的话,一定试试新建一个虚拟机!
adb连接,进入shell:
adb shell
保险起见,还是检查一下模拟器架构:
getprop ro.product.cpu.abi一般是x86_64:

可以暂时退出shell了
下载安卓模拟器的fridagithub下载:
https://github.com/frida/frida/releases
注意找之前本机对应的版本+模拟器架构的server,比如我的是16.5.2+x86_64:

这一步千万要看清楚别下错了!!!
之后使用7zip解压,或者别的什么,只要保证支持.xz格式,并且解压结果和下面一样就行
最后得到的是没有后缀的单个二进制文件(资源管理器把版本分隔符识别成后缀名了):

可以改个简单的名字(无需后缀),方便以后操作
在模拟器安装运行雷电模拟器,把文件传送到手机的/data/local/tmp/目录:
adb push 具体路径\frida-server /data/local/tmp/
进入adb shell,给它执行权限:
adb shell su # 切换到root用户(模拟器默认有root权限) chmod 777 /data/local/tmp/frida-server让frida服务在后台运行:
/data/local/tmp/frida-server &
Frida的服务器端默认监听端口27042,有时会转发27043以兼容某些场景或工具,或应对多通道通信
端口转发能让主机上访问localhost:<host-port>就相当于访问设备上的127.0.0.1:<device-port>,也就是可以让frida/frida-ps/frida-trace等工具直接连接到127.0.0.1:27042上
adb forward tcp:27042 tcp:27042 adb forward tcp:27043 tcp:27043(虽然现在暂时用不到)
测试本机cmd输入:
frida-ps -U如果输出了一个长长的进程列表(这些都是你模拟器里正在运行的进程),那就成功了:

使用OWASP的官方Crackmes页面下载的UnCrackable-Level1.apk进行练习:
https://mas.owasp.org/crackmes/
运行如图:

打开App点击OK后会直接退出,因为它在启动时执行了检查,发现是Root环境,然后调用了函数来退出程序
我们的目标就是使用Frida,找到这个退出函数,并劫持它,让它什么都不做
找到包名Frida启动App需要知道它的包名
这是App在Android系统里的唯一ID,可以使用frida-ps:
frida-ps -Uai
-U:连接USB设备
-a:只显示应用程序
-i:显示包名(Identifier)

也可以使用jadx查看:

在Android中,最常见的退出函数是java.lang.System.exit(),我们想办法对它进行操作
编写.js脚本:
// 告诉Frida,我们要对Java进行操作 Java.perform(function() { // 找到我们想Hook的类 var System = Java.use("java.lang.System"); // 找到这个类里的 "exit" 函数 var exit = System.exit; // "implementation" 允许我们重写这个函数的实现 exit.implementation = function(value) { // 当App调用System.exit(0)时... // 1. 我们在控制台打印一条消息,证明我们抓到它了 console.log("Hooked System.exit(" + value + "), bypassing..."); // 2. 我们什么都不做(不调用原始的exit函数),App就不会退出了 // (如果想调用原始函数,可以写:exit.call(this, value);) }; });这个脚本的意思是:
当App试图调用System.exit()时,Frida会拦截这个调用,转而执行我们写的代码(打印一条日志),然后就此打住,不让App真的退出
加载脚本之前我们使用过frida-ps -U,这是附加到一个已经在运行的进程
但现在我们遇到了一个问题:UnCrackable1一启动就闪退,我们根本来不及附加!
因此,我们需要一种新的模式:Spawn(启动)
Frida会以暂停状态启动这个App,在App的任何代码(包括我们想绕过的Root检测代码)运行之前,先注入我们的脚本,然后我们再恢复App的运行就行了
输入命令:
frida -U -l test.js -f owasp.mstg.uncrackable1
-U:连接USB设备
-l:load,加载我们的.js脚本
-f:使用Spawn模式启动这个包名
见证奇迹在上一步回车后,你会看到Frida在你的CMD里显示Spawning owasp.mstg.uncrackable1...:

同时,模拟器上的App会自动启动,并卡在启动屏,这是Frida在注入脚本
在在Frida的CMD里输入%resume并按回车(告诉frida恢复app运行):

然后回到app点击ok,这次没有闪退了,成功进入app了:

并且Frida控制台也输出了我们的信息:

App现在虽然能运行了,但我们的最终目的还没有达到:我们还不知道那个secret是什么
随便输入会报错:

比较合理的猜想是,当我们点击VERIFY时,app内部会把我们输入的字符串和正确值进行比较
使用jadx能清楚看见这个逻辑:

既然是通过String.equals函数进行的比较,那我们可以修改这个函数,让它在运行过程中打印出比较的双方,从而带出真正的secret:
// find_secret.js console.log("Hooking java.lang.String.equals()..."); Java.perform(function() { // 找到 "java.lang.String" 类 var StringClass = Java.use("java.lang.String"); // 找到 "equals" 方法 var equals = StringClass.equals; // 重写 "equals" 方法的实现 equals.implementation = function(anObject) { // ** 'this' 就是调用.equals()的那个字符串 (字符串A) // ** 'anObject' 就是传入equals()的参数 (字符串B) // 先调用原始的equals函数,得到比较结果 (true 或 false) var result = equals.call(this, anObject); // 这就是我们的“窃听”代码 // 为了防止控制台被刷屏,我们只打印我们关心的比较 // 我们猜测“秘密”的长度肯定不短(比如大于8) var strA = this.toString(); var strB = anObject ? anObject.toString() : "null"; // 过滤掉无关的短字符串比较 if (strA.length > 8 || strB.length > 8) { console.log("---[ String.equals comparison ]---"); console.log(" String A: " + strA); console.log(" String B: " + strB); console.log(" Result (A==B?): " + result); console.log("------------------------------------"); } // 把原始的比较结果返回给App return result; }; }); 联合注入现在我们有两个脚本,必须同时加载他们,才能让App在不闪退的情况下,被我们窃听
很简单,用两次 -l来加载两个脚本就行了:
frida -U -l test.js -l secret.js -f owasp.mstg.uncrackable1随意输入一个长一些的、比较有特色的字符串进行比较,此时Frida控制台就会打印出很多信息:

因为在看不见的地方,String.equals方法被调用了很多次,所以我们需要选个有辨识度的字符串方便找结果
总之,现在我们知道了secret的值:
I want to believe
我们Hook了String.equals,这个方法虽然有效,但你可能也发现了,它太吵了!
我们的Frida控制台会打印出App中所有的字符串比较,我们还得在里面费劲地找出想要的东西!
所以,更理想的办法是hook一个特定的函数,最好是只有在VERIFY按钮按下才会触发的函数
侦查还是使用jadx
首先在sg.vantagepoint.uncrackable1.MainActivity中找到点击按钮的逻辑:
if (a.a(string)) { alertDialogCreate.setTitle("Success!"); str = "This is the correct secret."; } else { alertDialogCreate.setTitle("Nope..."); str = "That's not it. Try again.";于是我们能在sg.vantagepoint.a.a这个类里面找到解密逻辑:
bArrA = sg.vantagepoint.a.a.a(b("8d127684cbc37c17616d806cf50473cc"), Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0));但是同一个名称的函数可能出现很多次,我们还需要更加精确地定位
可以看到它接受两个参数,而b(...)的返回值是byte[],Base64.decode(...)的返回值也是byte[]
所以,我们的目标是a(byte[], byte[])!
编写脚本 console.log("Hooking the PRECISE overloaded decryption function..."); Java.perform(function() { // 找到真正的解密类 var DecryptionClass = Java.use("sg.vantagepoint.a.a"); // 找到 'a' 方法, 并且指定它的签名 (Overload) // 在Frida中, '[B' 代表 byte[] (字节数组) // 所以我们找的是 a(byte[], byte[]) var decryptionMethod = DecryptionClass.a.overload('[B', '[B'); // 重写实现 decryptionMethod.implementation = function(key_bytes, data_bytes) { console.log("Precise function called!"); // 调用原始函数,让它正常解密 var decrypted_bytes = decryptionMethod.call(this, key_bytes, data_bytes); // 转换结果 // 原始返回值是 byte[],我们需要把它转成 String 才能看懂 var StringClass = Java.use("java.lang.String"); // 使用 new String(byte[]) 构造函数 var secret = StringClass.$new(decrypted_bytes); console.log("---[ Precise Decryption Hooked! ]---"); // 打印参数 (字节数组的可读性不强,但可以看看) // console.log(" Input Key: " + key_bytes); // console.log(" Input Data: " + data_bytes); console.log(" Output (Secret): " + secret); console.log("--------------------------------------"); // 把原始的 byte[] 返回值还给App,否则App会崩溃 return decrypted_bytes; }; });为什么是[B?
Frida使用Java的JNI类型签名来描述参数类型:
Z -> boolean
I -> int
Ljava.lang.String; -> String
[B -> byte[] ( [ 代表数组,B 代表 byte)
之后会讲到的
再次hook frida -U -l test.js -l secret.js -f owasp.mstg.uncrackable1我们甚至这次无需输入任何东西,直接点击VERIFY按钮就能看见输出:

第一次就到这吧,主要是先接触一下,直到frida这玩意是用来干什么的,下一次将正式进入教学~












查看全部评分