OneShell

I fight for a brighter tomorrow

0%

CVE-2017-3193 dir-850l缓冲区溢出

固件解压

漏洞分析

漏洞的产生是因为 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 的仿真步骤:

undifined

需要说明一下 -r 后的参数指的是固件的品牌,例如 dir 系列、netgear 系列等等。然后使用浏览器访问 192.168.0.1 就可以访问到路由器界面了。

undifined

仿真的流程通过脚本的输出可以看到,和手动搭建系统级仿真环境类似,是解压固件获取相关信息,创建虚拟机和宿主机的桥接网络,然后仿真。

如上是单纯搭建仿真环境而已,接下来要说一下 FirmAE 提供的一个超级实用的技能,那就是调试选项 -d,如下:

1
sudo ./run.sh -d dir firmwares/DIR850LB1_FW207WWb05

undifined

如上图,-d 选项提供了连接到 socat、shell、tcpdump 甚至 gdbserver 也集成到里面了,那就不需要再像以前那样手动去搭建系统级环境。为了调试的方便,我写了一个 shell 脚本上传到虚拟机中,设置调试 cgibin 所需要的环境变量,循环检测 gdbserver 是否已经挂掉等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/firmadyne/sh
export REQUEST_METHOD=POST
export HTTP_HNAP_AUTH="BBD0605AF8690024AF8568BE88DD7B8E 1482588069"
export HTTP_COOKIE="uid=OLnLaWBI8S"
export HTTP_REFERER="http://192.168.0.1/info/Login.html"
export HTTP_SOAPACTION="aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaaf"
ProcNumber=`ps | grep gdbserver | grep -v grep | wc -l`
source /firmadyne/setenv.sh
while :
do
echo $ProcNumber
if [ $ProcNumber -le 0 ];then
echo "gdbserver not run"
/firmadyne/gdbserver 0.0.0.0:12345 hnap hnap
else
echo "gdbserver is running"
sleep 5
fi
done

运行的时候让脚本在后台运行,这样如果是因为 gdbserver 报错或者是卡住了,可以直接杀掉进程。

1
/firmadyne/checkgdb.sh > ./log 2>&1 &

然后在宿主机中,使用 gdb-multiarch + pwndbg 设置远程调试,就可以愉快进行调试了。顺便说一下,pwngdb + tmux 是天作之合。

undifined

动态调试

当时在调试的时候,遇见的问题如下:

  1. 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
    13
    v4 = *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);
    }
  2. gdbserver 设置环境变量

    一开始自己是在 gdb-multiarch 中去设置环境变量,但是这样是设置到了宿主机的 gdb-multiarch 运行环境,正确的做法是设置到 gdbserver 的运行环境中,也就是上面在调试脚本中先设置了环境变量,然后再启动 gdbserver。简单说一下路由器环境中 cgi 调用的参数传递,一般都是在路由器的 server 例如 httpd、lighttpd 等通过设置环境变量的方式传递参数,然后调用 cgi 读取环境变量进行数据处理,再通过 stdout 将结果返回到 server中。

设置好环境变量后,就可以开始进行调试了

通过分析可以知道如果要运行到存在漏洞 strcat 函数调用处,也就是 0x414A14 处如下,基本块的执行顺序是:

1
2
3
4
5
.text:00414A14 move    $a0, $s2         # dest
.text:00414A18 lw $gp, 0xD34+var_D14($sp)
.text:00414A1C la $t9, strcat
.text:00414A20 jalr $t9 ; strcat
.text:00414A24 move $a1, $s1 # src
1
0x4141C4 -> 0x4141E0 -> 0x4141FC -> 0x41431C -> 0x414970 -> 0x414978 -> 0x414998 -> 0x4149B4

通过 cyclic 获取一个长度为 560 字节的字符串,然后赋值给 HTTP_SOAPACTION 环境变量,启动调试,运气比较不错直接可以执行到 0x4149B4。再打一个断点到最后 hnap_main 的结束处,就可以看到发生了栈溢出,返回地址已经被覆盖然后赋值给 ra 寄存器,PC 再从 ra 寄存器中取出来执行发生错误。

undifined

此处的计算出来的偏移是 538,因为 v74 是先将 v6(HTTP_HNAP_AUTH)通过空格分隔然后将第二部分固定的 10 个字节复制进去,然后才是通过 v4(HTTP_SOAPACTION)拼接,造成缓冲区溢出。

1
2
3
4
5
strncpy(v58, v27 + 4, 0xAu);
v28 = strtok(v6, " "); // v6 为 HTTP_HNAP_AUTH 环境变量,以空格分隔
v29 = strtok(0, " "); // v28 第一个部分
strcpy(v74, v29); // v29 第二个部分
strcat(v74, v4); // 上面的 strcpy 也存在缓冲区溢出

writeup

分析完毕了,其实就是比较简单的一个缓冲区溢出漏洞,可以开始编写利用脚本了。

目标程序没有开启堆栈不可执行,就直接在栈上写代码吧,而且自己太菜,没有找到合适的控制 $r0 的 rop 控制链。

1
2
3
.text:00415BCC jr      $ra
.text:00415BD0 addiu $sp, 0x1A8
x/4c $sp + 0xD34 + 0x20

参考链接

  • https://f5.pm/go-4502.html
  • [路由器漏洞挖掘之 DIR-805L 越权文件读取漏洞分析](路由器漏洞挖掘之 DIR-805L 越权文件读取漏洞分析)