- 对本书的赞誉
- 前言
- 基础篇
- 第 1 章 Android 中锁屏密码加密算法分析
- 第 2 章 Android 中 NDK 开发
- 第 3 章 Android 中开发与逆向常用命令总结
- 第 4 章 so 文件格式解析
- 第 5 章 AndroidManifest.xml 文件格式解析
- 第 6 章 resource.arsc 文件格式解析
- 第 7 章 dex 文件格式解析
- 防护篇
- 第 8 章 Android 应用安全防护的基本策略
- 第 9 章 Android 中常用权限分析
- 第 10 章 Android 中的 run-as 命令
- 第 11 章 Android 中的 allowBackup 属性
- 第 12 章 Android 中的签名机制
- 第 13 章 Android 应用加固原理
- 第 14 章 Android 中的 so 加固原理
- 工具篇
- 第 15 章 Android 逆向分析基础
- 第 16 章 反编译神器 apktool 和 Jadx
- 第 17 章 Hook 神器 Xposed
- 第 18 章 脱壳神器 ZjDroid
- 第 19 章 Native 层 Hook 神器 Cydia Substrate
- 操作篇
- 第 20 章 静态方式逆向应用
- 第 21 章 动态调试 smali 源码
- 第 22 章 IDA 工具调试 so 源码
- 第 23 章 逆向加固应用
- 第 24 章 逆向应用经典案例分析
- 第 25 章 Android 中常见漏洞分析
- 第 26 章 文件加密病毒 Wannacry 样本分析
22.4 用 IDA 解决反调试问题
到这里就结束了本章的破解旅程了?答案是否定的,因为上面的例子其实是自己构建的,先写了一个 apk,目的就是为了给大家演示。如何使用 IDA 来进行动态调试 so 文件,下面用一个操作动手的案例来讲解,这个案例解决了反调试问题。这是 2014 年阿里安全挑战赛的第二题:AliCrackme_2。
如图 22-28 所示,下面来看看破解流程。首先使用 aapt 命令查看其 AndroidManifest.xml 文件,得到入口的 Activity 类,如下所示:
图 22-28 破解程序
然后使用 dex2jar 和 jd-gui 查看它的源码类 com.yaotong.crackme.MainActivity,如下所示:
看到它的判断,securityCheck 方法是 native 层的,所以这时候去解压 apk 文件,获取它的 so 文件,使用 IDA 打开查看 native 函数的相对地址 11A8,如图 22-29 所示。
图 22-29 查看函数的相对地址
这里的 ARM 指令代码不分析了,大家自行查看即可,直接进入调试即可,再打开一个 IDA 进行关联调试,如图 22-30 所示。
图 22-30 附加进程
选择对应的调试进程,然后点击 OK,如图 22-31 所示。
图 22-31 查找需要调试的 so 文件
使用 Ctrl+S 键找到对应 so 文件的基地址 74EA9000,和上面得到的相对地址相加得到绝对地址 74EA9000+11A8=74EAA1A8。使用 G 键直接跳到这个地址,如下所示:
下个断点,然后点击 F9 运行程序,如图 22-32 所示。
图 22-32 运行程序
这时候会发现 IDA 退出调试页面了,再次进入调试页面,运行,还是退出调试页面了。似乎没法调试了。
其实这里是阿里做了反调试侦查,如果发现自己的程序被调试,就直接退出程序。那么怎么知道有人调试呢?这里限于篇幅,只是简单介绍一下原理。
前面说到,IDA 是使用 android_server 在 root 环境下注入到被调试的进程中,那么这里用到一个技术就是 Linux 中的 ptrace,关于 ptrace 这里也不解释了,大家可以自行的去搜一下 ptrace 的相关知识。那么 Android 中如果一个进程被另外一个进程 ptrace 了之后,在其 status 文件中有一个字段 TracerPid 可以标识是被哪个进程 trace 了,可以使用命令查看到被调试的进行信息 status 文件在:/proc/[pid]/status,如下所示:
这里的进程被 27445 进程 trace 了,在用 ps 命令看看 27445 是哪个进程:
果不其然是 android_server 进程。知道原理了,也大致猜到了阿里在底层做了一个循环检测这个字段如果不为 0,那么代表自己进程有人 trace,那么就直接停止退出程序,这个反调试技术用在很多安全防护的地方。
那么下面就来看看如何应对这个反调试?刚刚看到,只要一运行程序,就退出了调试界面,说明,这个循环检测程序执行的时机非常早,那么现在知道的最早的两个时机是:一个是.init_array,一个是 JNI_OnLoad。
.init_array 是一个 so 最先加载的一个段信息,时机最早,现在一般 so 解密操作都是在这里做的。JNI_OnLoad 是 so 被 System.loadLibrary 调用的时候执行,它的时机要早于哪些 native 方法执行,但是没有.init_array 时机早。知道了这两个时机,下面先来看看是不是在 JNI_OnLoad 函数中做的策略,所以需要先动态调试 JNI_OnLoad 函数。
既然知道了 JNI_OnLoad 函数的时机,如果把检测函数放在这里的话,不能用之前的方式去调试了,因为之前的那种方式时机太晚了,只要运行就已经执行了 JNI_OnLoad 函数,所以就会退出调试页面,幸好这里 IDA 提供了在 so 文件 load 的时机,只需要在 Debug Option 中设置一下就可以了,在调试页面的 Debugger 选择 Debugger Option 选项,如图 22-33 所示。
然后勾选 Suspend on library load/unload 即可,如图 22-34 所示。
图 22-33 设置 Debugger 选项
图 22-34 勾选 Suspend on library load/unload
这样设置之后,还是不行,因为程序已经开始运行,就在 static 代码块中加载 so 文件了,如图 22-35 所示。static 的时机非常早,所以这时候,需要让程序停在加载 so 文件之前即可。
图 22-35 static 代码块中加载 so 文件
那么想到的就是添加代码 waitForDebugger,这个方法就是等待 debug,还记得在之前的调试 smali 代码的时候,就是用这种方式让程序停在了启动初,然后等待去用 jdb 进行 attach 操作。
这一次可以在 System.loadLibrary 方法之前加入 waitForDebugger 代码即可,但是这里不这么干了。还有一种更简单的方式就是用 am 命令,am 命令本身可以启动一个程序,当然可以用 debug 方式启动:
这里一个重要参数就是-D,用 debug 方式启动:
运行完之后,设备是出于一个等待 Debugger 的状态,如图 22-36 所示。
图 22-36 程序处于等待调试状态
这时候,再次使用 IDA 进行进程的附加,然后进入调试页面,同时设置一下 Debugger Option 选项,然后定位到 JNI_OnLoad 函数的绝对地址,如图 22-37 所示。
但是发现,这里没有 RX 权限的 so 文件,说明 so 文件没有加载到内存中,想一想还是对的,因为现在的程序是 wait Debugger,也就是还没有走 System.loadLibrary 方法,so 文件当然没有加载到内存中,所以需要让程序跑起来,这时候可以使用 jdb 命令去 attach 等待的程序,命令如下:
图 22-37 定位到 JNI_OnLoad 绝对地址
其实这条命令的功能类似于前一章中说到用 Eclipse 调试 smali 源码的时候,在 Eclipse 中设置远程调试工程,选择 Attach 方式,调试机的 ip 地址和端口,还记得 8700 端口是默认的端口,但是运行这个命令之后,出现了一个错误:
无法连接到目标的 VM,那么这种问题大部分都出现在被调试程序不可调试。可以查看 apk 的 android:debuggable 属性:
果不其然,这里没有 debug 属性,所以这个 apk 是不可以调试的。需要添加这个属性,然后再回编译即可:
回编译:
签名 apk:
然后再次安装,使用 am 命令启动。
1)运行命令:
出现 Debugger 的等待状态。
2)启动 IDA 进行目标进程的 Attach 操作。
3)运行命令:
4)设置 Debugger Option 选项。
5)点击 IDA 运行按钮,或者 F9 快捷键,运行。
看到了,这次 jdb 成功的 attach 住了,debug 消失,正常运行了,但是同时弹出了一个选择提示,如图 22-38 所示。
图 22-38 so 文件选择框
这时候不用管它,全部选择 Cancel 按钮,然后就运行到了 linker 模块了,如下所示:
说明 so 已经加载进来了,再去获取 JNI_OnLoad 函数的绝对地址,如图 22-39 所示。
图 22-39 获取 JNI_OnLoad 函数的绝对地址
用 Ctrl+S 查找到了基地址 7515A000,用静态方式 IDA 打开 so 查看相对地址 1B9C,如下所示:
绝对地址加上相对地址为 7515A000+1B9C=7515BB9C,然后点击 G 键跳转,如图 22-40 所示。
图 22-40 跳转到运行的内存地址
跳转到指定的函数位置,如下所示:
这时候再次点击运行,进入了 JNI_OnLoad 处的断点,如下所示:
下面就开始单步调试了,但是每次到达 BLX R7 这条指令执行完之后,JNI_OnLoad 就退出了,如下所示:
经过好几次尝试都是一样的结果,发现这个地方有问题,可能就是反调试的地方了,再次进入调试,看见 BLX 跳转的地方 R7 寄存器中是 pthread_create 函数,这个是 Linux 中新建一个线程的方法。
阿里的反调试就在这里开启一个线程进行轮询操作,如图 22-41 所示,去读取/proc/[pid]/status 文件中的 TrackerPid 字段值,如果发现不为 0,就表示有人在调试本应用,在 JNI_OnLoad 中直接退出。关于反调试代码在前面已经详细介绍了。
图 22-41 反调试代码
问题找到了,现在怎么操作呢?只要把 BLX R7 这段指令干掉即可,如果是 smali 代码的话,可以直接删除这行代码即可。但是 so 文件不一样,它是汇编指令,如果直接删除这条指令的话,文件会发生错乱,因为本身 so 文件就有固定的格式,比如很多段的内容,每个段的偏移值也是保存的,如果这样去删除会影响这些偏移值,会破坏 so 文件格式,导致 so 加载出错的,所以这里不能手动去删除这条指令。还有另外一种方法,就是把这条指令变成空指令,在汇编语言中,nop 指令就是一个空指令,它什么都不干,所以这里直接改一下指令即可,arm 中对应的 nop 指令是:00 00 00 00;那么看到 BLX R7 对应的指令位置为:1C58,如下所示:
查看它的 Hex 内容是 37 FF 2F E1,如下所示:
可以使用一些二进制文件软件进行内容的修改,这里使用 010Editor 工具进行修改,如下所示:
直接修改成 00 00 00 00,如下所示:
保存修改之后的 so 文件,再次使用 IDA 进行打开查看,如下所示:
指令被修改成了 ANDEQ R0,R0,R0。
修改了之后,替换原来的 so 文件,再次重新回编译,签名安装。再次按照之前的逻辑给主要的加密函数下断点,这里不需要在给 JNI_OnLoad 函数下断点了,因为已经修改了反调试功能了,所以这里只需要按照如下简单几步操作即可:
1)启动程序。
2)使用 IDA 进行进程的 attach。
3)找到 Java_com_yaotong_crackme_MainActivity_securityCheck 函数的绝对地址。
4)打上断点,点击运行,进行单步调试,如下所示:
这里可以单步调试进来了,说明修改反调试指令成功了。
下面就继续用 F8 单步调试,如下所示:
调试到这里,发现一个问题,就是 CMP 指令之后,BNE 指令就开始跳转到 loc_74FAF2D0 处,那么就可以猜到了,CMP 指令比较的应该就是输入的密码和正确的密码。再次重新调试,看看 R3 和 R1 寄存器的值,如下所示:
这里的 R3 寄存器的值就是用寄存器寻址方式赋值字符串的,这里 R2 寄存器就是存放字符串的地址,看到的内容是 aiyou...但是这里肯定不是全部字符串,因为没看到字符串的结束符:'\0',点击 R2 寄存器,进入查看完整内容,如下所示:
这里是全部内容 aiyou,bucuoo。继续查看 R1 寄存器的内容,如下所示:
这里同样用寄存器寻址,R0 寄存器存储的是 R1 中字符串的地址,看到这里的字符串内容是 jiangwei,这个就是输入的内容,那么到这里就豁然开朗了,密码是上面的 aiyou,bucuoo;再次输入这个密码,就可见到破解成功。如图 22-42 所示。
图 22-42 破解成功效果图
提示:项目下载地址为
http://download.csdn.net/detail/jiangwei0910410003/9531957
注意:时刻需要注意 BL/BLX 等跳转指令,在它们执行完之后,肯定会有一些 CMP/CBZ 等比较指令,这时候就可以查看重要的寄存器内容来获取重要信息。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论