6

Android应用方法隐藏及反调试技术浅析 | WooYun知识库

 6 years ago
source link:
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Android应用方法隐藏及反调试技术浅析

0x00 前言


Android应用的加固和对抗不断升级,单纯的静态加固效果已无法满足需求,所以出现了隐藏方法加固,运行时动态恢复和反调试等方法来对抗,本文通过实例来分析有哪些对抗和反调试手段。

0x01 对抗反编译


首先使用apktool进行反编译,发现该应用使用的加固方式会让apktool卡死,通过调试apktool源码(如何调试apktool可参见前文《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方法,而且类的初始化方法中开头多了2行代码,如下图:

其基本原理是在dex文件中隐藏虚方法,运行后在第一次加载类时通过在方法(如果没有方法,则会自动添加该方法)中调用ProxyApplication的init方法来恢复被隐藏的虚方法,其中字符串"aHcuaGVsbG93b3JsZC5NYWluQWN0aXZpdHk="是当前类名的base64编码。

ProxyApplication类只有2个方法,clinit和init,clinit主要是判断系统版本和架构,加载指定版本的so保护模块(X86或ARM);而init方法也是native方法,调用时直接进入了so模块。

那么它是如何恢复被隐藏的方法的呢?这就要深入SO模块内部一探究竟了。

0x02 动态调试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运行。

然后打开DDMS并执行以下命令,模拟器就会自动断下来:

jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

(如果出现如下无法附加到目标VM的错误,可尝试端口8600)

此时,可在IDA中正常下断点调试,这里我们断JNI_OnLoad和init函数。

由于IDA调试器还不够完善,单步调试的时候经常报错,最好先做一个内存快照,然后分析关键点的函数调用,在关键点下断而不是单步调试。

0x03 反调试初探


一般反调试在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文件末尾的附加数据,得到事先保存的所有类的方法属性,然后根据传入的类名查找该类的被隐藏方法,并恢复对应属性字段。 执行完init函数,当前类的方法已经恢复了。然后转到dex文件的内存地址

dump出dex文件,保存为dump.dex。

0x04 恢复隐藏方法


对比一下原始dex文件,发现dex文件末尾的附加数据被解密出来了:

仔细分析一下附加数据的数据结构可以发现,它是一个数组,保存了所有类的所有方法的method_idx、access_flags、code_off、debug_info_off属性,解密后的这些属性都是uint类型的,如下图:

其中黄色框里的就是MainActivity的各方法的属性,知道这些就可以修复dex文件,恢复出被隐藏的方法了。下图就是恢复后的MainActivity类:

0x05 总结


以上就是通过实例分析展示出来的对抗和反调试手段。so模块中的反调试手段比较初级,可以非常简单的手工patch内存指令过掉,而隐藏方法的这种手段对art模式不兼容,不推荐使用这种方法加固应用。总的来说还是过于简单。预计未来通过虚拟机来加固应用将是一大发展方向。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK