一次借助 GDB 逆向工程的实践
1 引子
最近又做了一次逆向工程的工作,整个过程用到了一些新的工具和思路,而且估计在很长一段时间不会再用到,故记录于此,防止它们遗失在记忆的深处。
2 背景
目标设备是一台运行嵌入式 Linux 系统的设备,需要解密的是一个本地的 html 文件,该文件会被设备的浏览器解密并显示。
借助搜索引擎,已经解决了如下问题:
- 拿到设备 root 权限,并可通过 SSH 访问。
- 找到了目标文件的解密算法,但是需要从目标系统中找到密钥。
因此最终的目标是找到解密的密钥。
3 第一次尝试:静态反编译
3.1 锁定加密代码
已知目标文件会被浏览器解密并加载,因此需要先定位到解密的程序代码,并将对应的二进制文件拉到本地进行反编译,看看能不能找到传入密钥的地方。
先来分析目标文件,在它的文件头部分,有个固定的 Magic Number :***FILE_HEADER
。
按照常规的编程思路,解密程序会先判断文件是否包含这个 Magic Number,以判断是否需要做解密,所以解密程序中一定包含这个文件头的字符串。
为了验证这件事情,我写了一个脚本,它会执行如下工作:
- 找到浏览器进程。
- 查看浏览器进程加载的所有 so 文件。
- 检查 so 文件是否包含特定的字符串。
1 |
|
幸运的是,在目标设备上运行后,这个文件被找到了。
1 | root@*********:/tmp# ./scan-libs.sh |
通过 scp
命令将其拉到本地后,就可以开始反编译了。
3.2 静态反编译
反编译工具依然是使用 Cutter,免费又好用。
用 Cutter 反编译后,发现函数比较多,调用关系又比较复杂,短时间无法梳理清楚……
不过没关系,反编译后面还会用到,这条路暂时不往下走了,回头找找别的思路。
4 第二次尝试:动态调试
4.1 准备工具
要远程调试二进制程序,能想到的工具就是 GDB 了,它通过 gdbserver 提供了远程调试的能力。
不过…… 需要自己编译。
编译过程中踩到了几个小坑,记录一下:
1、单独编译 gdbserver
编译整个 gdb 源码包碰到了一些错误,懒的逐一解决了,最后找到了单独编译 gdbserver 的方法:
1 | # In GDB directory |
2、运行提示错误
1 | /usr/lib/libstdc++.so.6: version `GLIBCXX_3.4.29' not found |
出现该错误是因为运行环境的 C++ 库版本比编译环境低,因为我用来编译的工具链不是目标平台官方提供的,而是有爱的网友们自制的。
这个问题有几个解决方案:
- 方案 1:降低编译环境的库版本,但是可能会影响工具链中的其他库,可能要替换一批库。
- 方案 2:提升运行环境的库版本,同样可能影响到运行环境中的其他库和程序。
我最终选择了方案 3—— 把 C++ 库静态编译到 gdbserver 中。
- 方案 3:找到编译脚本,添加 link flag:
-static-libstdc++
4.2 动态调试
用 scp 命令把 gdbserver 上传到目标设备上。启动 gdbserver,附加到浏览器进程:
1 | ./gdbserver :8899 --attach <Browser Process ID> |
注意:调试其他进程需要有 root 权限。
之后在本地执行如下命令,连接到 gdbserver,开始远程调试:
1 | gdb -q |
由于远程调试可能会被系统信号中断,先执行如下命令,让 GDB 跳过所有信号:
1 | (gdb) handle all noprint nostop |
这次的目标是一个加密的文件,程序在解密前一定要先读取这个文件,因此在 fopen 函数上加上断点:
1 | (gdb) b fopen |
在目标设备上,通过浏览器打开目标文件。跳过不是目标的中断,当程序来到打开目标文件的中断时,输入 bt
命令,查看调用栈:
从调用栈中,得到了一个非常重要的信息,也就是谁调用了解密的库:
1 | #3 0x71917386 in **_******_OpenDrmFile () from target:/usr/lib/lib**_******.so.1 |
5 未曾设想的解题思路
5.1 再次静态反编译
重新回到静态反编译的路上,不过这次反编译的目标变成了另一个库。
通过读它的反编译代码,看懂了对解密库中函数的调用流程,再去有的放矢地读解密库的函数,就很容易理解了。
再次阅读解密库中对应函数的反编译代码,发现了一个可喜的事情:
里面有非常多的对 ***_Log
函数的调用,甚至调用它打印了密钥。
5.2 再次动态调试
既然知道密钥会被 ***_Log
函数打印出来,这里又有了两个解题思路:
- 对
***_Log
下断点,每次断点被激活的时候查看寄存器。 - 强行把 Log 的内容打印出来,并重定向到文件,最后直接查看日志文件。
思路 1 比较容易想到,但是操作复杂,这里就不赘述了。只说一下思路 2,因为这里用到了一些有趣的手段。
5.3 最终解决方案
强行打开日志,并重定向输出,需要解决两个问题:
***_Log
内部会判断一个标志位,若为 0 则直接返回,没有任何输出。- 目标程序已经将输出重定向到了
/dev/null
。
先解决问题 2:
1 | (gdb) p (int) open("/tmp/browser-out.log", 1089, 0777) |
再解决问题 1,因为目标库直接提供了一个函数修改标志位,省去了我们改内存的操作:
1 | (gdb) p (void) ***_EnableLog(1) |
最后执行 c
命令,让目标进程恢复运行。再次让浏览器打开目标文件,查看日志文件:
1 | [***_Agent] *** file fp:0 fd:115 |
密钥 GET !
用解密程序验证了一下密钥,成功解密出了目标文件。
至此,破解完成。