返回介绍

22.4 用 IDA 解决反调试问题

发布于 2024-10-10 22:32:22 字数 10223 浏览 0 评论 0 收藏 0

到这里就结束了本章的破解旅程了?答案是否定的,因为上面的例子其实是自己构建的,先写了一个 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文