漏洞分析
CVE-2019-10999 是 Dlink IP 摄像头的后端服务器程序 alphapd 中的一个缓冲区溢出漏洞,漏洞允许经过身份认证的用户在请求 wireless.htm 时,传入 WEPEncryption 参数一个长字符串来执行任意代码。具体描述以及受攻击的型号、固件版本可以查看参考链接,此处漏洞复现采用的是设备 dcs-932l 固件版本 1.14.04,固件下载链接查看参考链接。
对后端服务器程序 alphapd 进行分析,存在漏洞的函数是 sub_435DEC,开辟的栈帧大小是 0x48,其中该函数的返回地址保存在 sp + 0x40,存在溢出的缓冲区起始地址是 sp + 0x18。因此,只需要向缓冲区写入超过 0x28 个字节就可以溢出覆盖返回地址,劫持控制流。除此之外还可以控制 S0~S5 寄存器。
如下是 sub_435DEC 栈帧的开辟以及返回地址的存储汇编代码:
1 | .text:00435DEC li $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0 - .) |
如下是调用 strcpy 函数复制数据到栈上的缓冲区中,strcpy 的第一个参数 des 通过 a0 寄存器传入,由于跳转延迟槽,对 a0 的操作指令在 jalr 指令之后,但是先于跳转指令执行:
1 | .text:00435F98 loc_435F98: # CODE XREF: sub_435DEC+134↑j |
如下是函数执行完毕进行堆栈平衡,以及恢复 S0~S5寄存器、恢复 ra 寄存器到函数返回地址并跳转执行。
1 | .text:0004BF34 loc_4BF34: |
漏洞环境搭建
使用 qemu-system-static 搭建
漏洞环境搭建先是使用 qemu-mipsel-static 搭建。
1 | # 进入固件的根目录 复制 qemu-mipsel-static 到根目录 |
根据逆向中的反编译代码提示,是因为需要打开 /var/run/nvramd.pid 文件,那么在固件根目录创建 run 目录和 nvramd.pid 文件。
创建 pid 文件之后,继续运行依旧报错,无法创建 RSA 密钥,应该是缺少 urandom、random 设备造成的,手动在固件根目录创建。
1 | sudo chroot . ./qemu-mipsel-static ./bin/mknod -m 0666 ./dev/random c 1 8 |
报错 unable to write ‘random state’
OpenSSL 需要写入一些信息到 .rnd 文件,上面的错误可能是因为 .rnd 文件不存在,OpenSSL 不知道默认文件在何处,因为 RANDFILE 和 HOME 环境变量没有设置,那么解决方法就是创建 .rnd 文件并且设置环境变量指向这个文件。qemu 启动的时候设置这两个环境变量,解决了上面的问题。
1 | touch .rnd |
Can’t get lan ip from sysinfo
通过搜索字符串定位到在 websStartupServer 函数中,通过调用 getSysInfoLong 获取,在 getSysInfoLong 函数中是通过 /dev/gpio 设备获取到,可以通过 patch getSysInfoLong 函数,或者在 websStartupServer 中 patch 地址判定代码。此处选择 patch 后者,就可以让程序在 0.0.0.0:80 端口运行起来。
如下是 websStartupServer 的地址判定处反编译,以及 patch 的基本块:
1 | v3 = getSysInfoLong(30); |
重新运行如下:
使用 qemu-system-mipsel 搭建
发现使用 qemu 搭建的调试,使用 gdb-multiarch 连接不上去,于是采用了 qemu-system-mipsel 虚拟机来搭建,进入固件根目录。
1 | chroot . /bin/mknod -m 0666 /dev/random c 1 8 |
漏洞调试
漏洞触发
漏洞触发代码如下,也可以看到成功触发了 segment fault。
1 | import requests |
在 QEMU 虚拟机中开启 alphapd,然后使用 gdbserver attach 上 server 的进程,通过 12345 端口提供调试
使用 gdb 调试,如下,保存在栈上的函数返回地址被 BBBB 字符串覆盖
漏洞利用
接下来的步骤就是寻找环视 的 gadget,从栈中获取数据设置 system 函数传入的命令,并跳转到 system 函数执行。
这个地方需要说一下,不能在 alphapd 中去直接寻找 gadget,因为 alphapd 的代码段装载在低地址空间,其中的 gadget 地址高位前两位是 00,通过 url 传递地址会发生截断。因此可以先看 alphapd 装载了哪些 so 文件,从 so 中去寻找 system 函数和 gadget。此处选择了 libuClibc-0.9.28.so ,因为通过 ldd 查看 alphapd 装载的 so 文件,其中有 libc.so,libc.so 链接指向 libuClibc-0.9.28.so。
这个地方是在 QEMU 虚拟机中通过查看 map 文件获取 ibuClibc-0.9.28.so 的装载地址的,如果在实际应用中,需要能够进入设备,从设备上查看 so 的装载地址以及是否开启了随机化,但是一般低端路由器中都是比较老的 Linux 系统,没有地址随机化,那么在 QEMU 中也关闭了地址随机化。此处选择了第一个装载的 libc.so.0 的基址:0x77ed0000
然后获取到 system 函数相对装载地址的偏移是 0x0004BD20,得到 system 函数的地址为 0x77ed0000 + 0x0004BD20 = 0x77F1BD20
在 IDA 中,对 ibuClibc-0.9.28.so 使用 mipsrop.stackfinder(),找到如下的 gadget,同样计算出地址为 0x77ed0000 + 0x00050DE4 = 0x77F20DE4
1 | .text:00050DE4 addiu $s2, $sp, 0x1C8+var_D8 |
gadgets 的功能是将 sp + 0x1c8 - 0xd8 处数据传递给 a0,然后跳转到 S0 寄存器中去执行。通过前面对于缓冲区溢出的分析知道 S0 可控,写入 0x10 个字节开始控制 S0 寄存器,写入 0x28 个字节开始控制返回地址。那么整体的利用过程就是:
- 写入累计 0x10 个字节后,控制 S0 寄存器值为 system 函数地址
- 写入累计 0x28 个字节后,控制 ra 寄存器值为 gadget 地址
- 跳转到 system 函数,执行构造的字符串命令。此时已经恢复了堆栈, 从恢复的 sp + 0x1c8 - 0xd8 取出命令开始执行
exp 根据 poc 简单修改如下:
1 | import requests |
执行结果如下,执行命令 ls
小结
本文先分析了漏洞原理,然后分别从 qemu 的两种方式仿真将 alphapd 启动起来进行调试,然后通过 ret2libc 对漏洞实现利用。
漏洞原理还是比较简单的,仅仅是一个缓冲区溢出 + ret2libc的操作。但是实际利用的话,也许还需要获得设备,通过其他方式例如 UART 等先获取到一个 shell,然后看程序的 so 加载内存布局获取到基址。此外,漏洞是将路由器后端 server 发生了栈溢出的,触发 segment fault,如果路由器没有对 server 的守护进程或者看门狗,那么 server 就挂了。如果要稳定利用,可以考虑反弹一个 telnet 回来或者是在 exp 中通过 shellcode 重新启动 server。
参考链接
- [cve.mitre.org] CVE-2019-10999 概述
- [先知社区] CVE-2019-10999复现
- [github] CVE-2019-10999
- [[固件下载] DCS-932L_REVA_FIRMWARE_1.14.04.ZIP](https://us.softpedia-secure-download.com/dl/1987a9330e759255a393a795437da31e/61839716/300596532/drivers/network camera/DCS-932L_REVA_FIRMWARE_1.14.04.ZIP)
- How to fix openssl error “unable to write ‘random state’”?