一次借助 GDB 逆向工程的实践(续)
1 前言
没想到这篇文章还会有后续。
自上次解密完一个 html 文件后,“想要更多” 的需求就被提了出来。
回想前一篇文章的操作步骤,如果针对每个 html 都如此操作,还是挺费时间的。
那么有没有什么快捷的方法呢?—— 当然是有的,于是有了这篇文章。
2 思路
前一篇文章抓取密钥的方法,通过 GDB 操作浏览器进程,强行开启特定函数库的日志,并把输出重定向到文件。
那么如果我直接写个程序,模拟浏览器加载文件的步骤,并开启日志,然后把日志输出到 STDOUT 不就行了?
为实现这个目标,需要如下前置条件:
- 能编译出可以在目标设备运行的程序的工具链。
- 找到目标浏览器加载文件的关键函数。
第一点已经具备,因为在前一篇文章中已经用工具链编译了 gdbserver,并成功运行在目标设备上。
第二点也通过前一次的下断点,找到了关键函数和所在 so 库:
也就是 ***_OpenDrmFile
这个函数。
那么接下来就是编写一个程序,依次调用启用日志的函数和加载文件的函数即可。
3 实现
3.1 分析函数参数
现在只知道函数的名字,并不知道它的调用参数,因此先得用 Cutter 分析一波:
可以看到,Cutter 判断这个函数只有两个参数。
为了保险起见,自己再对目标函数做一轮分析,验证一下参数数量。
推理过程如下:
- 目标程序为 ARM 32 位程序,会通过寄存器
r0
~r3
传递前 4 个参数。 - 在
0x000053a6
这个位置,r2
的寄存器的值被修改(这是为了0x000053ac
这处调用设置参数)。 - 在
r2
寄存器被修改之前,发生过三次调用:0x00005382
,0x00005390
,0x00005390
。 - 通过跟进前面三次调用的代码,并未发现读取
r2
和r3
寄存器的值。
因此可以得出结论:r2
和 r3
寄存器的值并未被此函数使用,它只使用来自 r0
和 r1
寄存器的两个参数。
知道参数的数量后,还需要知道这两个参数的意义,这样才能传入正确的值。
分析参数意义,主要通过以下两条路:
- 反编译调用函数的代码,看参数的赋值;
- 反编译函数和下层函数的代码,看参数的实际作用;
略过繁琐的分析,最终得到的结论如下:
- 参数 1 为一个可写指针,
***_OpenDrmFile
会把一个数据写到其中。 - 参数 2 为一个字符串,应该是文件路径;
3.2 验证函数参数
最后再通过 GDB,对上述分析做一个佐证。
连接到 GDB server 后,对此函数下断点:
1 | (gdb) handle all noprint nostop |
然后打开目标文件,等待断点被唤醒,执行 info r
查看寄存器,然后分别查看 r0
和 r1
寄存器的值:
从 GDB 得到的信息来看,对 r1
的推测完全正确,r0
极大可能是一个 4 字节的缓冲区。
3.3 模拟程序 V1
模拟程序只需要做两件事情:
- 调用
***_EnableLog
开启日志。 - 调用
***_OpenDrmFile
读取加密的文件。
这里可以根据前面推测的函数原型,编写头文件,然后通过静态连接方式调用这个函数。或者用 dlsym
找到函数地址,直接调用。
由于后者更容易一些,这里就采用这个方案。
直接上代码:
1 |
|
编译后在目标设备上运行,虽然输出了一部分日志,但是接下来就提示了 Segmentation fault
并退出了。
3.4 模拟程序 V2
先用 GDB server 调试了一下模拟程序,发现错误出在 ***_OpenDrmFile
函数内部,具体原因是往空指针写东西。
重新使用 Cutter 反编译目标库,发现它导出了 ***_Initialize
函数,并在此函数内初始化了对应的内存,也就是说还要先调用这个初始化函数。
修改后的程序片段如下:
1 | typedef void (* Initialize)(); |
再次编译后运行,终于拿到了想要的输出,而且程序也不会崩溃了。
3.5 模拟程序 V3
既然已经能够完全模拟加载加密文件的过程,不如一步到位,直接读取解密后的内容。
目标库还有个 ***_ReadDrmFile
函数,看起来就是读文件内容的。
使用前面的手段分析出它的参数后,最终写成了完全脱离浏览器的解密程序。
这部分没有用到什么新鲜的手段,就不再赘述了,留给读者探索吧。