固件解压
漏洞分析
漏洞的产生是因为 hnap_main 函数中在拼接字符串的时候,没有对源字符串和目的字符串的大小进行限制,导致栈溢出。如下是 strcat 的函数原型。
1 | char *strcat(char* dest, const char *src); |
- dest:目的字符串指针
- src:源字符串指针
strcat 函数将 src 指向的字符串复制到 dest 字符串尾部,dest 原本末尾的 NULL 结束符被覆盖,并在连接完 src 字符串后重新加上 NULL 字符串。
1 | strcat(v74, v4); |
漏洞发生在如上代码,其中 v74 是栈上的内存空间,起始地址为 sp + 0xB30,v4 是获取到的环境变量 HTTP_SOAPACTION 的地址,存放在 _start 函数的栈中。在逆向 hnap_main 函数,初始化的过程是将调用返回地址(存放在 ra 寄存器中)保存到栈上 sp + 0xD34 + 0x20。通过计算,可以得出 v74 起始地址相对于栈上的返回地址偏移是 0x224 也就是 548 个字节大小,那么通过控制环境变量字符串的大小,就可以覆盖掉返回地址。
环境搭建 FirmAE
对于 IoT 尤其是路由器仿真环境的搭建,这个地方强烈安利一个框架那就是 FirmAE。FirmAE 是一个仿真成功率比较高的框架,也是基于 Firmadyne 开发的,开发者声称可以达到 79.36% 的成功率,而以往使用比较多的 Firmadyne 在相同固件测试集是 16.28%。这些成功率都分别是各自的论文数据支撑,有感兴趣的师傅可以去翻看一下。
FirmAE 的安装步骤可以参考 GitHub 上的帮助文档,此处不多说了,主要是想说一下他的一个 debug 选项,可以极大减少环境搭建时间。平常手动搭建 qemu 系统级仿真环境,主要是解压固件,配置网络,然后上传对应架构的 gdbserver,如果是程序对于硬件、网络的一些依赖,还需要手动去 patch。但是 FirmAE 框架中对这些操作进行了集成。下面说一下具体的使用方法。
如果是仿真的话,先将固件(未加密可被 binwalk 正常解压)复制到 FirmAE 根目录的 firmwares 文件夹中,然后使用 run.sh 进行操作,例如下面是对 dir-850 的仿真步骤:
需要说明一下 -r 后的参数指的是固件的品牌,例如 dir 系列、netgear 系列等等。然后使用浏览器访问 192.168.0.1 就可以访问到路由器界面了。
仿真的流程通过脚本的输出可以看到,和手动搭建系统级仿真环境类似,是解压固件获取相关信息,创建虚拟机和宿主机的桥接网络,然后仿真。
如上是单纯搭建仿真环境而已,接下来要说一下 FirmAE 提供的一个超级实用的技能,那就是调试选项 -d,如下:
1 | sudo ./run.sh -d dir firmwares/DIR850LB1_FW207WWb05 |
如上图,-d 选项提供了连接到 socat、shell、tcpdump 甚至 gdbserver 也集成到里面了,那就不需要再像以前那样手动去搭建系统级环境。为了调试的方便,我写了一个 shell 脚本上传到虚拟机中,设置调试 cgibin 所需要的环境变量,循环检测 gdbserver 是否已经挂掉等等。
1 | !/firmadyne/sh |
运行的时候让脚本在后台运行,这样如果是因为 gdbserver 报错或者是卡住了,可以直接杀掉进程。
1 | /firmadyne/checkgdb.sh > ./log 2>&1 & |
然后在宿主机中,使用 gdb-multiarch + pwndbg 设置远程调试,就可以愉快进行调试了。顺便说一下,pwngdb + tmux 是天作之合。
动态调试
当时在调试的时候,遇见的问题如下:
cgibin
在 cgibin 的 main 函数中,先获取第一个运行参数到 v4,然后通过 strrchr检测 v4 中 / 的位置并赋值给 v6。如果 / 存在,那么将 v4 指向 / 后的一个地址。为了方便,直接将 cgibin 程序名改为了 hnap,这样在调试的时候就直接在 main 函数中比较字符串跳转到调用 hnap_main 中了。关键反编译代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13v4 = *argv;
v6 = strrchr(*argv, '/');
if ( v6 )
v4 = v6 + 1;
......
v36 = strcmp(v4, "hnap");
v8 = envp;
if ( !v36 )
{
v9 = (int (*)())hnap_main;
v10 = argc;
return ((int (__fastcall *)(_DWORD, _DWORD, _DWORD))v9)(v10, argv, v8);
}gdbserver 设置环境变量
一开始自己是在 gdb-multiarch 中去设置环境变量,但是这样是设置到了宿主机的 gdb-multiarch 运行环境,正确的做法是设置到 gdbserver 的运行环境中,也就是上面在调试脚本中先设置了环境变量,然后再启动 gdbserver。简单说一下路由器环境中 cgi 调用的参数传递,一般都是在路由器的 server 例如 httpd、lighttpd 等通过设置环境变量的方式传递参数,然后调用 cgi 读取环境变量进行数据处理,再通过 stdout 将结果返回到 server中。
设置好环境变量后,就可以开始进行调试了
通过分析可以知道如果要运行到存在漏洞 strcat 函数调用处,也就是 0x414A14 处如下,基本块的执行顺序是:
1 | .text:00414A14 move $a0, $s2 # dest |
1 | 0x4141C4 -> 0x4141E0 -> 0x4141FC -> 0x41431C -> 0x414970 -> 0x414978 -> 0x414998 -> 0x4149B4 |
通过 cyclic 获取一个长度为 560 字节的字符串,然后赋值给 HTTP_SOAPACTION 环境变量,启动调试,运气比较不错直接可以执行到 0x4149B4。再打一个断点到最后 hnap_main 的结束处,就可以看到发生了栈溢出,返回地址已经被覆盖然后赋值给 ra 寄存器,PC 再从 ra 寄存器中取出来执行发生错误。
此处的计算出来的偏移是 538,因为 v74 是先将 v6(HTTP_HNAP_AUTH)通过空格分隔然后将第二部分固定的 10 个字节复制进去,然后才是通过 v4(HTTP_SOAPACTION)拼接,造成缓冲区溢出。
1 | strncpy(v58, v27 + 4, 0xAu); |
writeup
分析完毕了,其实就是比较简单的一个缓冲区溢出漏洞,可以开始编写利用脚本了。
目标程序没有开启堆栈不可执行,就直接在栈上写代码吧,而且自己太菜,没有找到合适的控制 $r0 的 rop 控制链。
1 | .text:00415BCC jr $ra |
参考链接
- https://f5.pm/go-4502.html
- [路由器漏洞挖掘之 DIR-805L 越权文件读取漏洞分析](路由器漏洞挖掘之 DIR-805L 越权文件读取漏洞分析)