#coincoin7@双螺旋
0x00 起因与事件回顾
去年发生的一件大事吧,维基解密披露了一套关于CIA黑客工具的文档vault7,vault7档案中也记载了各种各样的从CIA那里搞出来的吊炸天的工具,但就都只是描述,没有把真正的工具发出来。然后其中提到有一款工具叫Chimay-Red,这是一款针对Mikrotik网络设备厂商的工具,专门拿来打Mikrotik的路由器等其他使用了RouterOs作为OS的网络产品,比如下面这货:
然后直到现在吧,网上已经有了Chimay-red的公布出来的exp,但目前也是只有mipsbe版本和x86版本的,并且关于这个的漏洞分析类的文章目前国内貌似也一篇都没有,不过国外的倒是有几个质量比较高的值得参考的,比如下面的链接中的:
https://github.com/BigNerd95/Chimay-Red
https://github.com/seekintoo/Chimay-Red
https://wikileaks.org/ciav7p1/cms/page_16384604.html
https://wikileaks.org/ciav7p1/cms/page_16384512.html
于是我就想着写一篇关于这个洞的,然后又因为我亲自调试了几个不同架构的Mikrotik设备,正好我也想着借助这个洞来讨论一下不同架构下的栈溢出实践时的一些坑坑洼洼,以及分享一下经验与心得,于是就有了这篇文章。
0x01 什么是RouterOs
首先:RouterOs是几乎所有Mikrotik自家网络产品搭载的OS,然而其实就是厂商给自家产品的固件,体系化的取了个名字,叫RouterOs
主要组成:Linux Kernel3.3.5 + Squashfs
文件系统:Squashfs-> router app -> (www, mproxy, sermgr, busybox ......)
C库与开发语言:uClibc , C/C++
特色:支持众多CPU架构,其实就是有多个架构版本的RouterOs固件
0x02 Chimay-Red的基本情况
CVE编号:没有
漏洞类型:栈溢出
针对设备:Mikrotik的路由器等
影响版本:RouterOs<= 6.38.4
数量:暴露在公网上有200W++,并且到现在都仍然有不少设备没有更新固件,现在再从shodan或fofa上去搜索看,都仍然能够感受得到当时漏洞刚出来时的一半的影响力。
0x03 漏洞位置
漏洞所在进程:/nova/bin/www
/nova/bin/www是RouterOs中的web后台进程
0x04 漏洞原理
web后台进程www中的readPostData()函数,在处理http Post数据包时,没有过滤http头中的content-length的值,且将content-length的值直接代入了alloca()函数。下面是我简化了的漏洞位置的C代码。
其中,重点除了content-length的没有过滤,还有一个重点就是alloca()函数的使用,这是一个直接从栈上返回空间指针的函数!
原型:void*alloca(size_t size);
返回:指定大小的栈空间指针!
注意:在当前栈帧中分配指定大小的栈空间!
注意:传入size如果过大,则返回指针可能会越过栈边界,若再引用指针则会引发段错误!
0x05 漏洞验证与POC
需要:
1.触发readPostData() 函数 --> Post /jsproxy
2. Content-Length = -1 --> -1 = 4294967295
3. alloca(4294967295) --> Crash......
所以,只是简单的触发漏洞与验证漏洞的话,其实只需要发送一个如下这样的很简单的POST包就可以了。
也可以像如下这样不停的发POST包,让RouterOs的web拒绝服务。
0x06 利用目标
以RouterOs 6.38.4为例
目标1:远程代码执行,可get shell
目标2:适用多种CPU架构如Mipsbe、Powerpc、tile-gx
0x07 可利用点1:基于多线程的web并发处理
首先,/nova/bin/www 使用多线程的方式实现web的并发处理,那么每一个访问web的socket,www进程中都会申请一个线程并在线程中调用readPostData()函数来处理http post数据包的接收。而每一个线程都会在栈中占用128KB(0x20000 Bytes)的空间,且线程之间是相邻的。
在RouterOs 6.32.2之上线程栈大小由www进程自定义
如下图,当同一时刻有两个socket连接RouterOs的web时,www进程的栈中会呈现如下的内存布局:
0x08 可利用点2:alloca(size)函数传入的size是可控的
alloca()返回的是当前栈帧中的栈空间!
alloca()分配的栈空间大小可控!
alloca()实际上会直接修改掉当前sp的值!
0x09 可利用点3:istream::read()
istream::read()会将http post包的正文数据写到alloca()返回的栈空间上!
0x0A 利用思路
将上面提到的可利用点整理在一起,基本上就可以得到一条利用思路了。
步骤一,新建两个socket s1与s2,并同时连接目标的web,使得www进程在总栈中生成两个thread stack。
步骤二,利用可控的alloca(),使得:(alloca_size_socket1) > (alloca_size_socket2 + 0x20000),这样的话,就会有如下的变化,本来指向thread1线程栈空间的alloca_sp1,可以控制其指向thread2的线程栈空间:
步骤三,利用istream::read()能将post包正文数据写入栈空间的能力,使用s1发送payload,此时s1的payload便会被写入到s2的线程栈空间中!
于是,就可以自由溢出s2的线程栈空间啦!
步骤四,算好偏移,使用s1发送的payload溢出s2的thread_stack,覆盖s2的thread_stack中的返回地址,比如istream::read()的返回地址!
0x0B 利用实践,For Mipsbe RouterOs
1.需要提前知道与准备的:
理论:
mips是大多数iot设备以及中小型传统网络设备的常用CPU架构
精简指令集,寄存器传参,但返回地址在大多数情况下仍然需要压栈
动手:
动态调试:mips-gdb / mips-gdbserver
ROP链的构造:IDA + mipsRop插件
/nova/bin/www的运行时的保护机制的开启情况!(重点关注NX(DEP)与ASLR的开启情况!)
2.查看mipsbe架构情景下的RouterOs的保护机制:
用工具从静态的ELF中看:
在真机中从运行时环境看:
总论: NX disable,ASLR enable on level1
3.编写exp:
1 -> 构造好Payload(RopChain + Shellcode)
2 -> 新建socket1与socket2,并connect目标的web
3 -> s1 Post Content-length=0x29000, s2 Post Conten-length=0x8000
4 -> s1 Send Payload 溢出Thread2的栈
将Payload布置在Thread2的栈上
覆盖Thread2的栈中的istream::read()的返回地址
5 -> s2.close()
s2断开连接, 让Thread2从阻塞的istream::read()中返回
6 -> EIP已被控,通过RopChain跳到栈上执行shellcode
4.演示:
0x0C 利用实践,For Powerpc RouterOs
1.需要提前知道与准备:
理论:
ppc,也是网络设备中经常出现的CPU架构,特别是CISCO的设备!
精简指令集,寄存器传参,返回地址需要压栈
动手:
动态调试:powerpc-gdb / powerpc-gdbserver
ROP链的构造:IDA + ROPgadget/ ropper
/nova/bin/www的运行时的保护机制的开启情况!(重点关注NX(DEP)与ASLR 的开启情况!)
2.查看powerpc架构情景下的RouterOs的保护机制:
用工具从静态的ELF中看:
在真机中从运行时环境看:
总论: NX enable,ASLR enable on level1,多了NX的开启,但是,动态库实际上没有随机化!!!
3.深水坑:
真机调试中发现,内存布局上s1与s2对应的Thread1_Stack与Thread2_Stack的内存位置反过来了!
解决:将通过s1与s2发送的数据与相应动作也反过来。
4.其他主要区别:
比起mipsbe, powerpc多了NX的启动!
解决:ASLR实际上没有开启,那么使用代码段与动态库的代码构造调用execve()的RopChain,栈上只放置execve()所需的参数数据,最终调到代码段执行execve(“/bin/bash”, ["/bin/bash", "-c", "shellCmd"], NULL)
5.编写exp:
1 -> 构造好Payload (RopChain + Shellcode)
2 -> 新建socket1与socket2,并connect目标的web
3 -> s1 Post Content-length=0x1000, s2 Post Conten-length=0x21300
4 -> s2 Send Payload 溢出Thread1的栈
将Payload布置在Thread1的栈上
覆盖Thread1的istream::read()中的select()函数的返回地址
5 -> s1.close()
s1断开连接, 让Thread1从istream::read()中的select()函数返回
6 -> EIP已被控,通过RopChain执行execve('/bin/bash', .....)
6.演示:
0x0D 利用实践,For Tile RouterOs
1.需要提前知道与准备:
理论:
tile???什么?
非常少见的CPU架构,资料也极少,但是tile官方有提供交叉编译工具链!
动手:
动态调试:tilegx-gdb / tilegx-gdbserver
/nova/bin/www的运行时的保护机制的开启情况!(重点关注NX (DEP)与ASLR 的开启情况!)
2.查看tile架构情景下的RouterOs的保护机制:
用工具从静态的ELF中看:
在真机中从运行时环境看:
总论: NX enable,ASLR enable on level2,NX与ASLR完全开启!
3.深水坑:
连IDA都无法反汇编的架构??那RopChain又怎么办呢?
解决:使用官方提供的工具链中的tilegx-unknown-linux-gnu-objdump去反汇编,去寻找与构造RopChain。(可能会有更好的方法?)
4.其他主要区别:
同时开启了NX与ASLR!
解决:NX的绕过可以和powerpc的利用实践同理,而ASLR则可以通过爆破动态库基地址的方法来应对。
因为:
1. 因为有watchdog的进程监控,可以任性的无限打崩/nova/bin/www进程
2. 打崩/nova/bin/www并使其重启后,又有利于让www的进程内存布局复位
3. 多次重启/nova/bin/www发现其libc的基地址在一定范围内,可能值不多(0x778b0000 ... 0x77970000 ... 0x779b0000,16个可能值左右)
5.编写exp:
While(True):
libc_base=0x77970000
基于libc_base=0x77970000去构造好Payload (RopChain + Shellcode)
新建socket1与socket2,并connect目标的web
s1 Post Content-length=0x21300, s2 Post Conten-length=0x1000
s1 Send Payload 溢出Thread2的栈
将Payload布置在Thread2的栈上
覆盖Thread2的istream::read()中的select()函数的返回地址
s2.close()
s2断开连接, 让Thread2从istream::read()中的select()函数返回
EIP被控,通过RopChain执行execve('/bin/bash', .....)
6.演示():
0x0E 总结与分享
1. 架构等设备环境的不同,往往意味着保护机制也会不同。
2. 灵活运用工具链 -> 交叉编译gdb,使用objdump等。
3. 应对一个陌生的架构,不要死记汇编语法,汇编语法可类比其他架构,都差不多的。重要的是了解清楚调用约定,传参方式,栈帧结构这些。
4. 真机调试,最能直接的发现问题与最能高效的解决问题!!!
点击阅读原文,了解JSRC乌托邦沙龙高校行之成都站,文末有ppt下载哦。