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

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2015-10-10T04:20:00Z
Canonical: https://wepostx.com/topics/1095

> 乌云历史资料归档
>
> **原始作者：** 猎豹科学院
> **原始编号：** superkieran-wooyundrops:813
> **原始发布时间：** 2015-10-10 12:20
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

# 0x00 前言

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

# 0x01 对抗反编译

首先使用apktool进行反编译，发现该应用使用的加固方式会让apktool卡死，通过调试apktool源码（如何调试apktool可参见前文《Android应用资源文件格式解析与保护对抗研究》），发现解析时抛出异常，如下图：

![原文图片](/media/2016/06/21ee4e805c2e5ba77b85405d55520f14)

根据异常信息可知是readSmallUint出错，调用者是getDebugInfo，查看源码如下：

![原文图片](/media/2016/06/f24553da64d1d22aac45a69cdc63b04c)

可见其在计算该偏移处的uleb值时得到的结果小于0，从而抛出异常。 在前文《Android程序的反编译对抗研究》中介绍了DEX的文件格式，其中提到与DebugInfo相关的字段为DexCode结构的debugInfoOff字段。猜测应该是在此处做了手脚，在010editor中打开dex文件，运行模板DEXTemplate.bt，找到debugInfoOff字段。果然，该值被设置为了0xFEEEEEEE。

![原文图片](/media/2016/06/0cd134a088fbf43db36baa1f3e5fa0ba)

接下来修复就比较简单了，由于debugInfoOff一般情况下是无关紧要的字段，所以只要关闭异常就行了。
为了保险起见，在readSmallUint方法后面添加一个新方法readSmallUint_DebugInfo，复制readSmallUint的代码,if语句内result赋值为0并注释掉抛异常代码。

![原文图片](/media/2016/06/7e2753c1eafd1c77b71e4e34c3bcd7ae)

然后在getDebugInfo中调用readSmallUint_DebugInfo即可。

![原文图片](/media/2016/06/8da92f13506be95cd5383c23036d59d8)

重新编译apktool，对apk进行反编译，一切正常。
然而以上只是开胃菜，虽然apktool可以正常反编译了，但查看反编译后的smali代码，发现所有的虚方法都是native方法，而且类的初始化方法中开头多了2行代码，如下图：

![原文图片](/media/2016/06/67090c397cf510ebb6dbbd6ade4ef569)

其基本原理是在dex文件中隐藏虚方法，运行后在第一次加载类时通过在方法（如果没有方法，则会自动添加该方法）中调用ProxyApplication的init方法来恢复被隐藏的虚方法，其中字符串"aHcuaGVsbG93b3JsZC5NYWluQWN0aXZpdHk="是当前类名的base64编码。
ProxyApplication类只有2个方法，clinit和init，clinit主要是判断系统版本和架构，加载指定版本的so保护模块（X86或ARM）；而init方法也是native方法，调用时直接进入了so模块。

![原文图片](/media/2016/06/f1679668df6f743bb7872fb58d973be6)

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

# 0x02 动态调试so模块
如何使用IDA调试android的SO模块，网上有很多教程，这里简单说明一下。

## 1. 准备工作

### 1.1准备好模拟器并安装目标APP。

### 1.2 将IDA\dbgsrv\目录下的android_server复制到模拟器里，并赋予可执行权限。

```text
adb push d:\IDA\dbgsrv\android_server /data/data/sv adb shell chmod 755 /data/data/sv
```

### 1.3 运行android_server，默认监听23946端口。

```text
adb shell /data/data/sv
```

### 1.4 端口转发。

```text
adb forward tcp:23946 tcp:23946
```

## 2 以调试模式启动APP，模拟器将出现等待调试器的对话框。

```text
adb shell am start -D -n hw.helloworld/hw.helloworld.MainActivity
```

![原文图片](/media/2016/06/8f7416734f5f2d8fc62e21e8d4ed2b65)

## 3 启动IDA，打开debugger->attach->remote Armlinux/andoid debugger，设置hostname为localhost，port为23946，点击OK；然后选择要调试的APP并点击OK。

![原文图片](/media/2016/06/23c88bb079857062b897aaff055158ed)

这时，正常状态下会断下来：

![原文图片](/media/2016/06/780f546971d1f76087f8465dd4e73994)

然后设置在模块加载时中断：

![原文图片](/media/2016/06/064e46f33db5cd948b1c8f2b1dc38f40)

点击OK，按F9运行。
然后打开DDMS并执行以下命令，模拟器就会自动断下来：
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
（如果出现如下无法附加到目标VM的错误，可尝试端口8600）

![原文图片](/media/2016/06/2c578ed8d384ead95fc36861e55d60a6)

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

![原文图片](/media/2016/06/4ccc8a5e0e71069250c449f78d2ce922)

![原文图片](/media/2016/06/05865d8803718d4d56f4ae06fa0fea77)

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

# 0x03 反调试初探

一般反调试在JNI_OnLoad中执行，也有的是在INIT_ARRAY段和INIT段中早于JNI_OnLoad执行。可通过readelf工具查看INIT_ARRAY段和INIT段的信息，定位到对应代码进行分析。

![原文图片](/media/2016/06/3cd2c24ba4c56239089fa8c34f85c201)

INIT_ARRAY如下：

![原文图片](/media/2016/06/3ed9eb2f8e1487bdf9d10acf5c038f2c)

其中函数sub_80407A88的代码如下，通过检测时间差来检测是否中间有被单步调试执行：

![原文图片](/media/2016/06/6e9471179238df438bbeb0bc740bf84e)

sub_8040903C函数里就是脱壳了，首先读取/proc/self/maps找到自身模块基址，然后解析ELF文件格式，从程序头部表中找到类型为PT_LOAD，p_offset!=0的程序头部表项，并从该程序段末尾读取自定义的数组，该数组保存了被加密的代码的偏移和大小，然后逐项解密。

![原文图片](/media/2016/06/717e7b578c51259e1c3620ebf6278ef4)

函数check_com_android_reverse里检测是否加载了com.android.reverse，检测到则直接退出。

![原文图片](/media/2016/06/548f1af15850a6d91ee85b84711c49c8)

JNI_OnLoad函数中有几个关键的函数调用：

![原文图片](/media/2016/06/473bf1a2d133d5943535ea7a82122a71)

call_system_property_get检测手机上的一些硬件信息，判断是否在调试器中。

![原文图片](/media/2016/06/daa485b45b18f103dd90588e7b96146a)

checkProcStatus函数检测进程的状态，打开/proc/$PID/status，读取第6行得到TracerPid，发现被跟踪调试则直接退出。

![原文图片](/media/2016/06/dd5f59124664d85dfc923a6c4de73cf3)

通过命令行查询进程信息，一共有3个同名进程，创建顺序为33->415->430->431。其中415和431处于调试状态：

![原文图片](/media/2016/06/2340d4fea1c8b015f57654317223b84c)

进程415被进程405（即IDA的android_server）调试：

![原文图片](/media/2016/06/e95225f0719d2cac79b16ee61a3569a3)

进程431被其父进程430调试：

![原文图片](/media/2016/06/83926be548813c78110904090ee3b988)

要过这种反调试可在调用点直接修改跳转指令，让代码在检测到被调试后继续正常的执行路径，或者干脆nop掉整个函数即可。 检测调试之后，就是调用ptrace附加自身，防止其他进程再一次附加，起到反调试作用。

![原文图片](/media/2016/06/f4b17f77db31ae70c485ebbc9aea4053)

修改跳转指令BNE（0xD1）为B(0xE0)，直接返回即可。

![原文图片](/media/2016/06/d8feee1ed5dac4116385c55027bdc75b)

当然，更加彻底的方法是修改android源码中bionic中的libc中的ptrace系统调用。检测到一个进程试图附加自身时直接返回0即可。
上面几处反调试点在检测到调试器后都直接调用exit()退出进程了，所以直接nop掉后按F9执行。然后就断在了init函数入口，顺利过掉反调试:

![原文图片](/media/2016/06/0e16397672d80443d2111484d3194ad1)

init函数在每个类加载的时候被调用，用于恢复当前类的被隐藏方法.首次调用时解密dex文件末尾的附加数据，得到事先保存的所有类的方法属性，然后根据传入的类名查找该类的被隐藏方法，并恢复对应属性字段。 执行完init函数，当前类的方法已经恢复了。然后转到dex文件的内存地址

![原文图片](/media/2016/06/77cf3839ed84139bffdfea26c8174708)

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

![原文图片](/media/2016/06/f0bf37f092818b37c82d5249c35d8c49)

# 0x04 恢复隐藏方法

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

![原文图片](/media/2016/06/c8ff62e1b0c52375cb8efdf4acafd789)

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

![原文图片](/media/2016/06/ed7d525963b4ecdf3822d369ee7922ba)

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

![原文图片](/media/2016/06/258c1fbfefcea97b915564fecdb65db8)

# 0x05 总结

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

## Replies
