今早看了一个视频,深得我心,与我更系列文章几乎是一样的理由,不过我想的没有那么透彻。不同的人生阶段会有不同的想法,重新开启一个新的学习旅程对现在的我来说,是一个值得尝试的东西。曾有段时间觉得自己除了写代码,啥都不会,现在发现还能做点事情,输出的应该也算有点价值。
视频链接:https://www.bilibili.com/video/BV1SH4y1q7k6/
本文讨论一下动态调试的一些其他技巧。
trace 使用
调试fork进程
调试 Android JNI 的 init_array 中的函数或JNI_OnLoad函数
trace 是用来记录一个进程在执行过程中发生的特定事件。这些事件会被记录到一个缓存区或者文件中。
我们以一个例子来讲解 trace 的作用,材料已经上传到了 p19。
首先,使用 IDA 打开目标文件,看其函数窗口,发现只有很少的几个函数:
这是因为该程序里面的花指令太多了,干扰了IDA的静态分析结果。对于花指令,有些文章有比较详细的介绍:
https://www.anquanke.com/post/id/208682
https://www.anquanke.com/post/id/236490
有时间的可以看看,毕竟搞破解的都是搞开发的玩剩下的,出题的总是要高于解题的。
这个程序里面的花指令多到IDA都分析不出来啥东西,我们一个个的去看肯定也不现实,那么根据文章中说的:花指令大致可以分为可执行花指令和不可执行花指令两类。所以我们可以先使用 Trace 来找出不可执行的花指令。
进入调试模式,在 start 方法最前面打上断点,打开菜单项 [Debugger] → [Tracing] → [Tracing Options] 配置 Trace 选项:
Trace buffer size,如果分析的程序较大,这个数值可以填大一些。
Trace text file,想要将Trace记录保存为文件,可以填入一个文件路径。
Highlight,这个一定要选上,它会将执行过的指令染色,非常的有用。
其他更细节的选项说明可以去查一下文档或者书籍,这里就不细说了。
Trace 的粒度:
第一个是指令级别的trace,就是记录的是每条指令的指令。
第二个是基本块的trace,基本块的概念 ollvm 里面有,可以找下文档。
第三个是函数级别的trace,仅仅记录函数的调用。
调试启动进程后,触发断点,这3个选项就可以选择了,我们选择指令trace,然后让程序继续运行,等待进入下一个断点或者程序结束,这里我们没有设置下一个断点,所以程序会运行直到结束或者等待输入之类的。
可以看到,有一片黄色出现了,这就表示这些指令是执行到了的指令。
由于指令 trace 会极大的损耗性能,因为它要监视每条指令,所以会等待的时间比较长。
等到进程出现这个界面的时候,说明程序运行了一段逻辑了,它要等待用户输入,我们可以先分析从程序开始到这里的 trace,后面的再说。
所以,我们停止调试,打开 trace 窗口看看,打开菜单项 [Debugger] → [Tracing] → [Tracing Window] 查看 Trace 记录:
这里有 18w 行指令,还是非常多的。所以一般我们是使用脚本来处理这些指令,但是我们还没有学习如何编写脚本,所以这里就只是简单介绍一下该怎么搞。
上图中,在 Address 这一行,我们右键,发现有个选项是默认勾选了的,如果是我们自己看,这个是有帮助的,但是如果要给 python 处理,这个地址的名字就很不方便,所以我们去掉勾选,看看效果:
这个时候,地址格式就统一了。
我们简单的观察一下这些指令,发现一个规律,就是这些指令基本可以分为一块一块的,每个块里面的指令会比较对称:
它的 push 与 pop 都是对称的,观察 rsp 寄存器的值也可以。花指令的一个原则,就是不干扰程序的正常运行。所以从这个方面入手,我们是要找出那些执行了也没啥乱用的指令块即可。
当然,我对花指令的研究也不深入,刚入行,这里也只是说了下我自己的看法。花指令的分析不在本系列范围内,所以暂不深入研究了。
将这些数据导出,可以直接右键 export trace to text file
,以文本模式导出来,这样可以直接用 python 处理,比较方便。
在上一篇课程中,我们调试过一个目标apk(blackbox.apk),还分析过它的 Java_com_pandaos_idacourse_MainActivity_enc
函数,它的反汇编代码有这样的一段:
if ( !fork() )
{
__android_log_print(3, "pandaos", "try DEBUG ME!");
init();
init();
init();
init();
init();
init();
init();
init();
init();
init();
init();
__android_log_print(3, "pandaos", "tell me special_key.");
exit(0);
}
这个目的就很明显了,是想让我们找出子进程里面生成的 special_key。那么我们该如何调试这个子进程呢?看代码逻辑,这个子进程执行了一个循环就结束了,根本没有机会 attach 上去。
这个有一个技巧,就是将子进程的第一行指令 patch 为一个死循环,等断点附加上去之后,再将指令还原继续运行即可。
我们来操作一下,先看看子进程要执行的第一条指令:
.text:00000000000170D0 78 71 00 94 BL .__android_log_print
这行指令的意思就是跳转到__android_log_print
里面去执行。我们要将这行指令改为一个死循环要如何做呢?
在 arm 指令集里面,跳转指令有2种常用的:
B
BL
B指令是ARM中最基本的跳转指令,它的使用方法如下:
B label
表示跳转到 label 所指向的位置,我们看一个例子:
我们可以利用这个指令来做一个死循环,看了一下指令的十六进制表示,没有发现B指令的操作符是啥,有点奇怪。
BL指令跟B不同:在跳转之前,会先将当前指令的下一条指令地址保存到LR寄存器中,然后才跳转到标号执行。这样做的好处是:当我们想从标号地方返回时,可以直接将LR寄存器中的返回地址赋值给PC,程序就可以返回到原来的程序中继续执行了。我们并不想更改寄存器,所以不用BL。
有了跳转指令基础,我们就可以 patch 了,首先在 fork 这里加上断点,然后附加上进程,修改函数调用为死循环:
使用了,keypatch 插件后,我们就不用自己计算偏移了,只需要填目标地址就好了。
点击 patch,再看看伪代码,发现变成了死循环:
我们让程序继续运行,这样fork出来的进程就会进入死循环,这个时候,我们退出主进程的调试(Detach from process),重新附加到子进程上:
可以看到,这个时候就出现了两个进程,28827 就是子进程,我们先附加上去,再给循环加上断点:
有需要的可以进行指令还原,但是这里不需要,所以我们直接 set ip,让进程执行下面的 init 代码:
按 F8 步过所有 init 调用,看看 special key 的值:
发现全部是 0,这样谜题就解出来了。
这里我们需要借助 frida,因为 frida 有一个 pause 模式,这个模式就是让 app 启动,没有完全启动,因为此时它还没加载自己的任何库。
由于app自己的库是有 linker 进行加载的,其 init_array 段也是 linker 主动调用的(有兴趣的可以翻翻源码),所以我们可以先找到 linker 中对应的函数,加上断点就能调试到 so 加载的时候执行的 init_array 函数。
我们先使用 frida,让 app 进入暂停模式:
我是用的是 15.2.2 版本,所以默认就是进入暂停模式,frida 还提示我们,使用 %resume 可以让程序继续,这个时候我们看app窗口就是一个白屏了。
我们使用 ida 附加到进程,在 modules 窗口找到 linker:
点进去,找到它的 call_array 函数:
这里可以对比android的源码来看,其中v11的调用是会触发 so 加载时的 init_array 方法的调用。
我们在这里加上断点:
在so加载时执行的 init 函数里面也加上断点:
断点加好了之后,在 frida 里面输入%resume
,让程序继续运行,就能断点到 init 函数里面了:
可以看到,确实进入了该函数。