0×00、前言
Android应用的加固和对抗不断升级,单纯的静态加固效果已无法满足需求,所以出现了隐藏方法加固,运行时动态恢复和反调试等方法来对抗,本文通过实例来分析有哪些对抗和反调试手段。
0×01、对抗反编译
首先使用apktool进行反编译,发现该应用使用的加固方式会让apktool卡死,通过调试apktool源码(如何调试apktool可参见发表于FreeBuf的前文《 Android应用资源文件格式解析与保护对抗研究 》),发现解析时抛出异常,如下图:
根据异常信息可知是readSmallUint出错,调用者是getDebugInfo,查看源码如下:
可见其在计算该偏移处的uleb值时得到的结果小于0,从而抛出异常。
在前文《 Android程序的反编译对抗研究 》中介绍了DEX的文件格式,其中提到与DebugInfo相关的字段为DexCode结构的debugInfoOff字段。猜测应该是在此处做了手脚,在010editor中打开dex文件,运行模板DEXTemplate.bt,找到debugInfoOff字段。果然,该值被设置为了0xFEEEEEEE。
接下来修复就比较简单了,由于debugInfoOff一般情况下是无关紧要的字段,所以只要关闭异常就行了。
为了保险起见,在readSmallUint方法后面添加一个新方法readSmallUint_DebugInfo,复制readSmallUint的代码,if语句内result赋值为0并注释掉抛异常代码。
然后在getDebugInfo中调用readSmallUint_DebugInfo即可。
重新编译apktool,对apk进行反编译,一切正常。
然而以上只是开胃菜,虽然apktool可以正常反编译了,但查看反编译后的smali代码,发现所有的虚方法都是native方法,而且类的初始化方法<clinit>中开头多了2行代码,如下图:
其基本原理是在dex文件中隐藏虚方法,运行后在第一次加载类时通过在<clinit>方法(如果没有<clinit>方法,则会自动添加该方法)中调用ProxyApplication的init方法来恢复被隐藏的虚方法,其中字符串 "aHcuaGVsbG93b3JsZC5NYWluQWN0aXZpdHk=" 是当前类名的base64编码。
ProxyApplication类只有2个方法,clinit和init,clinit主要是判断系统版本和架构,加载指定版本的so保护模块(X86或ARM);而init方法也是native方法,调用时直接进入了so模块。
那么它是如何恢复被隐藏的方法的呢?这就要深入SO模块内部一探究竟了。
0×02、动态调试so模块
如何使用IDA调试android的SO模块,网上有很多教程,这里简单说明一下。
1、准备工作
1.1、准备好模拟器并安装目标APP。
1.2、将IDA/dbgsrv/目录下的android_server复制到模拟器里,并赋予可执行权限。
adb push d:/IDA/dbgsrv/android_server /data/data/sv adb shell chmod 755 /data/data/sv
1.3、运行android_server,默认监听23946端口。
adb shell /data/data/sv
1.4、端口转发。
adb forward tcp:23946 tcp:23946
2、以调试模式启动APP,模拟器将出现等待调试器的对话框。
adb shell am start -D -n hw.helloworld/hw.helloworld.MainActivity
3、启动IDA, 打开debugger->attach->remote Armlinux/andoid debugger,设置hostname为localhost,port为23946,点击OK;然后选择要调试的APP并点击OK。
这时,正常状态下会断下来:
然后设置在模块加载时中断:
点击OK,按F9运行。
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
(如果出现如下无法附加到目标VM的错误,可尝试端口8600)
此时,可在IDA中正常下断点调试,这里我们断JNI_OnLoad和init函数。
由于IDA调试器还不够完善,单步调试的时候经常报错,最好先做一个内存快照,然后分析关键点的函数调用,在关键点下断而不是单步调试。
0×03、反调试初探
一般反调试在JNI_OnLoad中执行,也有的是在INIT_ARRAY段和INIT段中早于JNI_OnLoad执行。可通过readelf工具查看INIT_ARRAY段和INIT段的信息,定位到对应代码进行分析。
INIT_ARRAY如下:
其中函数sub_80407A88的代码如下,通过检测时间差来检测是否中间有被单步调试执行:
sub_8040903C函数里就是脱壳了,首先读取/proc/self/maps找到自身模块基址,然后解析ELF文件格式,从程序头部表中找到类型为PT_LOAD,p_offset!=0的程序头部表项,并从该程序段末尾读取自定义的数组,该数组保存了被加密的代码的偏移和大小,然后逐项解密。
函数check_com_android_reverse里检测是否加载了com.android.reverse,检测到则直接退出。
JNI_OnLoad函数中有几个关键的函数调用:
call_system_property_get检测手机上的一些硬件信息,判断是否在调试器中。
checkProcStatus函数检测进程的状态,打开/proc/$PID/status,读取第6行得到TracerPid,发现被跟踪调试则直接退出。
通过命令行查询进程信息,一共有3个同名进程,创建顺序为33->415->430->431。其中415和431处于调试状态:
进程415被进程405(即IDA的android_server)调试:
进程431被其父进程430调试:
要过这种反调试可在调用点直接修改跳转指令,让代码在检测到被调试后继续正常的执行路径,或者干脆nop掉整个函数即可。
检测调试之后,就是调用ptrace附加自身,防止其他进程再一次附加,起到反调试作用。
修改跳转指令BNE(0xD1)为B(0xE0),直接返回即可。
当然,更加彻底的方法是修改android源码中bionic中的libc中的ptrace系统调用。检测到一个进程试图附加自身时直接返回0即可。
上面几处反调试点在检测到调试器后都直接调用exit()退出进程了,所以直接nop掉后按F9执行。然后就断在了init函数入口,顺利过掉反调试:
init函数在每个类加载的时候被调用,用于恢复当前类的被隐藏方法.首次调用时解密dex文件末尾的附加数据,得到事先保存的所有类的方法属性,然后根据传入的类名查找该类的被隐藏方法,并恢复对应属性字段。
dump出dex文件,保存为dump.dex。
0×04、恢复隐藏方法
对比一下原始dex文件,发现dex文件末尾的附加数据被解密出来了:
仔细分析一下附加数据的数据结构可以发现,它是一个数组,保存了所有类的所有方法的method_idx、access_flags、code_off、debug_info_off属性,解密后的这些属性都是uint类型的,如下图:
其中黄色框里的就是MainActivity的各方法的属性,知道这些就可以修复dex文件,恢复出被隐藏的方法了。下图就是恢复后的MainActivity类:
0×05、总结
以上就是通过实例分析展示出来的对抗和反调试手段。so模块中的反调试手段比较初级,可以非常简单的手工patch内存指令过掉,而隐藏方法的这种手段对art模式不兼容,不推荐使用这种方法加固应用。总的来说还是过于简单。预计未来通过虚拟机来加固应用将是一大发展方向。
*本文作者:猎豹科学院(企业账号),转载须注明来自FreeBuf黑客与极客(FreeBuf.COM)