0x00 前言
遇到了一个APP,存在Frida检测,原本是想直接对壳程序进行逆向分析来找到检测点的,但是尝试许久无果,想到可以尝试进行脱壳修复。绕过壳检测,但是一整个过程中遇到了非常多的坑,以及检测点,于是写了这篇文章来记录一下自己的工作。
0x01 脱壳
拿到APP的第一步一般来说我都会看看是否检测了frida。可以看到就算是用了魔改版的frida也无法过检测。

因为存在frida检测,所以我常用的frida-dexdump已经无法使用了,而且这么硬的壳,就只能上Fart了。记得一定要给APP文件操作权限。然后直接运行APP即可。

基本稳定之后可以在Fart中看到,已经脱下来了许多的dex文件

0x02 修复
修复Dex文件
将该文件夹直接adb pull 到电脑中,其中会有一些txt以及bin文件,我们不需要,可以丢到其他目录中去。

然后呢,首先需要对dex文件进行一下重命令来更规范,我写了这个脚本
1 2 3 4 5 6 7 8 9 10 11 12
| #!/bin/bash
counter=1
for file in *.dex; do mv "$file" "$counter.dex" ((counter++)) done echo "所有 .dex 文件已重命名完成。"
|
执行结果如下

之后需要对dex文件进行一下修复,先去Github中找到DexRepair.jar文件,下载下来。放到当前文件夹,然后执行以下脚本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #!/bin/bash
if [[ ! -f "DexRepair.jar" ]]; then echo "DexRepair.jar 未找到,请确保它在当前目录中。" exit 1 fi
for file in *.dex; do java -jar DexRepair.jar "$file" echo "已处理文件: $file" done echo "所有 .dex 文件处理完成。"
|
执行结果如下


会生成一些新的dex文件,就需要将原来的文件删除,执行以下脚本即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #!/bin/bash
for file in [0-9]*.dex; do if [[ -f "${file%.dex}_repair.dex" ]]; then rm "$file" echo "已删除文件: $file" else echo "未找到对应的修复文件,保留: $file" fi done
echo "所有原始 .dex 文件处理完成。"
|

最后需要对dex进行转换成smali文件,需要先下载一个baksmali.jar,放到当前文件夹下,来执行以下脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #!/bin/bash
if [[ ! -f "baksmali-2.5.2.jar" ]]; then echo "baksmali-2.5.2.jar 未找到,请确保它在当前目录中。" exit 1 fi
for file in *.dex; do java -jar baksmali-2.5.2.jar d "$file" echo "已处理文件: $file" done
echo "所有 .dex 文件处理完成。"
|
执行结束后会生成一个out文件夹,其中都是smali文件,然后这里我删除了一些没有用的文件夹,比如dalvik,jdk,junit,libcore,sun等等,其实不删除也可以,因为这个是脱壳下来的系统dex,没有必要重打包回APK中,造成不必要的资源浪费。

反编译原APK
想要重打包APK,首先要对原APK进行反编译,使用apktool,最后会生成一个result文件夹。
1
| apktool d -f -o result xxx.apk
|

接下来就是两个工作
- 修改smali文件夹中的的smali代码
- 修改AndroidManifest.xml中的程序入口
可以看到代码如下,现在程序的入口类是com.Proxy.ShellApplication,这里也就是壳程序的入口,我们要讲将这个标签的内容修改成真正的程序入口类,也就是下边的<meta-data android:value/>中的内容
1 2
| <application android:allowBackup="false" android:appComponentFactory="xxx" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:name="com.Proxy.ShellApplication" android:networkSecurityConfig="@xml/network_security_config" android:requestLegacyExternalStorage="true" android:supportsRtl="true" android:theme="@style/AppTheme"> <meta-data android:name="_app_name" android:value="com.xxx.applicaion.JMXApplication"/>
|

修改好之后保存退出即可。然后呢将刚才反编译出来的out文件夹下的内容,放置到smali文件夹下。修改之前是这样的

替换好之后就是如下图所示

这个时候就需要使用apktool来进行回编译了。(这里会可能出现OOM的错误,读者可以自行查询一下增加JVM内存大小,我这里给了20G内存)。
笔者这里遇到过这个问题,当时困扰了我许久。

后续发现原因是,单个dex里面的方法数不能超过65535个,而这里我回编译的代码中的方法数量过多,可以看到图中的方法已经是192405个了。遇到这个问题的解决办法是:将smali文件拆分成多个文件夹,然后编译成多个dex文件。最后修改成这样文件夹才成功回编译。

重签名Apk
参考葫芦娃大佬的文章:https://mp.weixin.qq.com/s/xlgtU4xqoMqv2oXCMko7yQ

最后会生成一个新的APK,尝试安装新的APK。可以成功安装
0x03 签名检测
因为是对App进行重签名的所以如果App中存在检测签名信息的话就会出现这种情况,这也是验证应用完整性检测的一种常见手段。

其实可以使用frida来绕过检测,但是这里我实测,即使是壳脱了。依然存在frida检测….(真的硬)。猜测是还有子线程启动来循环检测ART环境。

哎,于是只能xp来尝试Hook但是笔者其实不会用xp,就找到了**算法助手**这个工具。安装好之后呢,需要在LSP中打开模块,然后勾选要Hook的APP(这里如果是没有脱壳修复的,勾选之后就会一直卡启动页,再次吐槽这个APP的检测)。

然后点开APP界面时候继续打开要Hook的APP

点进APP之后存在一个读取应用签名监听

打开之后运行APP,然后呢就会闪退。但是我们进入日志功能呢,就会看到有日志

查看调用栈的时候我们可以看到调用的方法,分别是LauncherActivity.singCheck()方法去调用的utils.SignCheck的构造方法,之后会调用到getCertificateSHA1Fingerprint方法。

因为我们已经有了脱壳之后的APP,所以直接去代码中看逻辑就好了。

构造函数中调用了获取证书的函数

这里调用了getPackageManager().getPackageInfo()等等函数来获取当前APP的证书,这个是常用的代码。所以我们其实Hook这个函数的返回值来返回80dcaf98bb0fdbece2ffe05918c37ca3这个即可。
1 2 3 4 5 6 7
| public String getCertificateSHA1Fingerprint(){ try{ return Md5Utils.getMD5String(this.byte2HexFormatted(MessageDigest.getInstance("SHA1").digest(CertificateFactory.getInstance("X509").generateCertificate(new ByteArrayInputStream(this.context.getPackageManager().getPackageInfo(this.context.getPackageName(), 64).signatures[0].toByteArray())).getEncoded())).trim()); }catch(java.lang.Exception e0){ return "80dcaf98bb0fdbece2ffe05918c37ca3"; } }
|
好消息是算法助手直接可以自定义Hook,让我这个不会写XP的人可以直接Hook了。

保存后使其生效即可。这个时候就成功的进入APP了。(呼,真的累,终于到这一步了….)

0x04 Frida检测对抗
当然我们不忘初心,想到就算是给壳脱了,APP本身也还是有Frida检测就真的很让人难绷。接下来就是找到检测Frida检测的位置的步骤了。通常Frida检测是在SO层的。那么首先可以尝试看看加载了什么so文件之后应用闪退的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function hook_dlopen() { Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) { var path = ptr(pathptr).readCString(); console.log(path); } } } ); }
setImmediate(hook_dlopen);
|
执行结果如下,所以可能检测函数就在这两个之中。

那么我们只能首先使用IDA来分析libmsaoaidsec.so这个文件,一般来说呢,frida检测是会使用pthread_create函数来新建一个子线程来检测的,我们可以尝试直接搜索该函数。确实是存在的

查看一下交叉引用,是有三处位置使用了该函数

分别图如下,所以其实静态分析的时候可以看到是偏移量0x163DC,0x16D30,0x175F8这三个函数被创建了子线程。

但是并不能直接相信静态信息的结果要动态调试才可以,于是尝试frida,(这里可以使用frida的原因是,在被检测之前frida是可以使用的,我们现在一直在Hook的就是检测前的函数。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function hook_pthread_create() { var soAddr = null; var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create"); Interceptor.attach(pthread_create_addr, { onEnter: function (args) { var funcAddr = args[2]; var soName = Process.findModuleByAddress(funcAddr).name; soAddr = Module.findBaseAddress(soName); if(soName.indexOf("libmsaoaidsec.so") >= 0 ){ console.log(soName + " ==> " + soAddr); console.log("function Address ==> ", funcAddr); } }, onLeave: function (retval) {
} }) }
|
执行结果如下

1 2 3
| libmsaoaidsec.so ==> 0x6e32f41000 function Address ==> 0x6e32f585f8 function Address ==> 0x6e32f57d30
|
这就代表着只有0x175F8和0x16D30这两个函数被创建了子线程。于是我们尝试Hook这两个子线程将其替换成空
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 hook_pthread_create() { var soAddr = null; var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create"); Interceptor.attach(pthread_create_addr, { onEnter: function (args) { var funcAddr = args[2]; var soName = Process.findModuleByAddress(funcAddr).name; soAddr = Module.findBaseAddress(soName); if(soName.indexOf("libmsaoaidsec.so") >= 0 ){ console.log(soName + " ==> " + soAddr); console.log("function Address ==> ", funcAddr); var offset = funcAddr.sub(soAddr); if (offset==0x175F8) { Interceptor.replace(funcAddr, new NativeCallback(function () { console.log(funcAddr + " replace"); }, 'void', [])); } if (offset==0x16D30) { Interceptor.replace(funcAddr, new NativeCallback(function () { console.log(funcAddr + " replace"); }, 'void', [])); } } }, onLeave: function (retval) {
} }) }
|
执行结果如下,可以看到成功的替换了函数。

理论上说现在应该就可以正常Hook了,但是~~我只能惊呼牛逼(是真NM硬啊,还有检测??到这里是不可能放弃的,已经变成私人恩怨了。)

那么可能就是存在在libpro.so中了,同样的我们可以来看看IDA中,那么吊诡的情况出现了。这个so文件中根本就没有pthread_create函数的调用。(我只能说:6)

这根本难不倒他,还是上frida直接动态调试试试。
1 2 3 4
| if (soName.indexOf("libpro.so") >= 0) { console.log(soName + " ==> " + soAddr); console.log("function Address ==> ", funcAddr); }
|

其实是可以看到libpro.so中的调用位置的
1 2 3 4
| libpro.so ==> 0x6e382f8000 function Address ==> 0x6e3835b3d4 function Address ==> 0x6e38353b64 function Address ==> 0x6e3837163c
|
于是也是创建了三个子线程函数0x7963C,0x633D4,0x5BB64。那么现在必须去看看这是怎么回事。

直接看代码是pthread_create被混淆成了v26

其他的也是不是直接使用pthread_create调用的

无妨,我们直接继续Hook这几个偏移地址就好
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 42 43 44 45 46 47 48 49
| function hook_pthread_create() { var soAddr = null; var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create"); Interceptor.attach(pthread_create_addr, { onEnter: function (args) { var funcAddr = args[2]; var soName = Process.findModuleByAddress(funcAddr).name; soAddr = Module.findBaseAddress(soName); if (soName.indexOf("libmsaoaidsec.so") >= 0 ) { console.log(soName + " ==> " + soAddr); console.log("function Address ==> ", funcAddr); var offset = funcAddr.sub(soAddr); if (offset==0x175F8) { Interceptor.replace(funcAddr, new NativeCallback(function () { console.log(funcAddr + " replace"); }, 'void', [])); } if (offset==0x16D30) { Interceptor.replace(funcAddr, new NativeCallback(function () { console.log(funcAddr + " replace"); }, 'void', [])); } } if (soName.indexOf("libpro.so") >= 0) { console.log(soName + " ==> " + soAddr); console.log("function Address ==> ", funcAddr); var offset = funcAddr.sub(soAddr); if (offset==0x5BB64) { Interceptor.replace(funcAddr, new NativeCallback(function () { console.log(funcAddr + " replace"); }, 'void', [])); } if (offset==0x633D4) { Interceptor.replace(funcAddr, new NativeCallback(function () { console.log(funcAddr + " replace"); }, 'void', [])); } if (offset==0x7963C) { Interceptor.replace(funcAddr, new NativeCallback(function () { console.log(funcAddr + " replace"); }, 'void', [])); } } }, onLeave: function (retval) {
} }) }
|
woc终于可以自由的用frida了。

0x05 后记
后续实测了其实是libpro.so中的0x633d4和libmsaoaidsec.so中的0x175f8这两个子函数在校验,其余的子线程应该不是做frida检测用的,所以只Hook这两个就够了。
搞完这一波这段时间是不想做逆向了,真的累。但是有成就感。:)