一次借助 GDB 逆向工程的实践

1 引子

最近又做了一次逆向工程的工作,整个过程用到了一些新的工具和思路,而且估计在很长一段时间不会再用到,故记录于此,防止它们遗失在记忆的深处。

2 背景

目标设备是一台运行嵌入式 Linux 系统的设备,需要解密的是一个本地的 html 文件,该文件会被设备的浏览器解密并显示。

借助搜索引擎,已经解决了如下问题:

  1. 拿到设备 root 权限,并可通过 SSH 访问。
  2. 找到了目标文件的解密算法,但是需要从目标系统中找到密钥。

因此最终的目标是找到解密的密钥。

3 第一次尝试:静态反编译

3.1 锁定加密代码

已知目标文件会被浏览器解密并加载,因此需要先定位到解密的程序代码,并将对应的二进制文件拉到本地进行反编译,看看能不能找到传入密钥的地方。

先来分析目标文件,在它的文件头部分,有个固定的 Magic Number :***FILE_HEADER

按照常规的编程思路,解密程序会先判断文件是否包含这个 Magic Number,以判断是否需要做解密,所以解密程序中一定包含这个文件头的字符串。

为了验证这件事情,我写了一个脚本,它会执行如下工作:

  1. 找到浏览器进程。
  2. 查看浏览器进程加载的所有 so 文件。
  3. 检查 so 文件是否包含特定的字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/sh

browser_name="Web******"
magic_number="***FILE_HEADER"

# Pick a render process ID
proc_id=$(ps -ef | grep $browser_name | grep "type=renderer" | head -n 1 | awk '{print $2}')
echo "Browser renderer process ID: $proc_id"

# Find all so files loaded by process
so_files=$(lsof | grep $proc_id | grep -F ".so" | awk -F'\t' '{print $3}')

# Scan each so file
for so_file in $so_files; do
echo -n "Checking $so_file ... "
strings $so_file | grep -q $magic_number
if [ $? -eq 0 ]; then
echo "Yes"
break
else
echo "No"
fi
done

幸运的是,在目标设备上运行后,这个文件被找到了。

1
2
3
4
5
6
7
8
9
10
root@*********:/tmp# ./scan-libs.sh
Browser renderer process ID: 2954
Checking /usr/lib/libhalgal.so.0.1.0 ... No
Checking /usr/lib/libharfbuzz.so.0.10000.3 ... No
Checking /usr/lib/libfribidi.so.0.3.6 ... No
Checking /usr/lib/libboost_system.so.1.61.0 ... No
......
Checking /usr/lib/libmali.so.0.1 ... No
Checking /usr/lib/libjson-c.so.2.0.1 ... No
Checking /usr/lib/lib***_agent.so.1.0.0 ... Yes

通过 scp 命令将其拉到本地后,就可以开始反编译了。

3.2 静态反编译

反编译工具依然是使用 Cutter,免费又好用。

用 Cutter 反编译后,发现函数比较多,调用关系又比较复杂,短时间无法梳理清楚……

不过没关系,反编译后面还会用到,这条路暂时不往下走了,回头找找别的思路。

4 第二次尝试:动态调试

4.1 准备工具

要远程调试二进制程序,能想到的工具就是 GDB 了,它通过 gdbserver 提供了远程调试的能力。

不过…… 需要自己编译。

编译过程中踩到了几个小坑,记录一下:

1、单独编译 gdbserver

编译整个 gdb 源码包碰到了一些错误,懒的逐一解决了,最后找到了单独编译 gdbserver 的方法:

1
2
3
4
# In GDB directory
make configure-gdbserver
cd gdbserver
make

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
2
gdb -q
(gdb) target remote <设备IP>:8899

由于远程调试可能会被系统信号中断,先执行如下命令,让 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 函数打印出来,这里又有了两个解题思路:

  1. ***_Log 下断点,每次断点被激活的时候查看寄存器。
  2. 强行把 Log 的内容打印出来,并重定向到文件,最后直接查看日志文件。

思路 1 比较容易想到,但是操作复杂,这里就不赘述了。只说一下思路 2,因为这里用到了一些有趣的手段。

5.3 最终解决方案

强行打开日志,并重定向输出,需要解决两个问题:

  1. ***_Log 内部会判断一个标志位,若为 0 则直接返回,没有任何输出。
  2. 目标程序已经将输出重定向到了 /dev/null

先解决问题 2:

1
2
3
4
5
6
(gdb) p (int) open("/tmp/browser-out.log", 1089, 0777)
$1 = <log-file-fd>
# $1 为日志文件的文件描述符
(gdb) p (int) dup2(<log-file-fd>, 1)
$2 = 1
# 返回1表示执行成功

再解决问题 1,因为目标库直接提供了一个函数修改标志位,省去了我们改内存的操作:

1
2
(gdb) p (void) ***_EnableLog(1)
$3 = void

最后执行 c 命令,让目标进程恢复运行。再次让浏览器打开目标文件,查看日志文件:

1
2
3
4
5
6
7
8
9
10
11
12
[***_Agent] *** file fp:0 fd:115
[***_Agent] _***_GetLicenses, CID of ***File:088D************************5909
[***_Agent] _***_GetLicenses, All of CID count:4
[***_Agent] CID of DB : 088D************************5909
[***_Agent] [*] find License of 088D************************5909
[***_Agent] CID of DB : 11A8************************E94E
[***_Agent] CID of DB : 06EF************************846A
[***_Agent] CID of DB : 7ECC************************065E
// 此处设置密钥
[***_Agent] ***_GetLicenseHandle, cek set to *** file : 12 e6 43 04 ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** 51 b8 49 63
[***_Agent] ***_GetLicensesHandle, getLicenses() ret:00000000
[***_Agent] ***_GetLicensesHandle Out---

密钥 GET !

用解密程序验证了一下密钥,成功解密出了目标文件。

至此,破解完成。

6 参考资料