DIR-815 栈溢出漏洞复现
前言
这是我选择复现的第一个iot漏洞,总是对栈溢出有种特别的执念,有种学了一大圈又绕回来的感觉
同时这个固件也是Mips架构的,正好加强一下自己对mips的理解
官方的漏洞报告:DIR-815
| 0 | 准备
固件下载地址:下载 (需要挂梯子)
binwalk
安装:sudo apt install binwalk
sasquatch
安装:https://github.com/devttys0/sasquatch
| 1 | 固件提取
将下载的bin文件传送到虚拟机中,使用如下命令
1 | binwalk -e --preserve-symlinks firmware_name |
解包完成后进入文件夹
这个是一个完整的文件系统,
先找到官方漏洞报告中所说的hedwig.cgi
文件,其路径为/htdocs/web/hedwig.cgi
,通过find
命令看一下:
发现这是一个指向上级目录中elf文件的软链接
我们将这个文件提取出来,顺便将/lib/中的libc文件也提取出来
此时便可以使用IDA对elf进行分析
| 2 | 逆向分析
可以看到main
函数会根据*argv字符串进入不同的解析函数,找到目标hedwigcgi_main
首先可以看到,请求方法必须为POST
然后进入cgibin_parse_request
,这个函数会使用一参的函数指针对url进行处理
会提取出几个环境变量,这几个变量对和栈溢出关系不大,但是不能没有,留着后面再分析
然后走到sess_get_uid
中
首先对cookie
做提取,然后进入一个while(1)循环中
循环的作用是将cookie以”=”为界分割为两段,前一段存贮在v2结构体中,后一段存储在v4结构体中
然后比较第一部分是不是”uid”,如果是的话将第二部分拼接到a1中(a1为在main函数中通过sobj_new()
创建的结构体,初始内容为空)
返回后,main函数调用了sprintf,没有检查cookie的长度,造成了第一次栈溢出
,但是我们接着往下看会发现在两个检测后还有一个sprintf的栈溢出,且在这个过程中strings的内容是没有发生变化的.由于第二次栈溢出时sprintf的拼接方法第一次相差很大,所以我们需要绕过这两个检查,在第二次的溢出点精确控制返回地址.
条件绕过
1.路径检查
1 | v7 = fopen("/var/tmp/temp.xml", "w"); // first check |
第一个条件要求在文件系统中存在/var/tmp
的路径,在实战(路由器中)肯定是存在这样的路径的,但是在我们的仿真固件中并没有这个路径,所以我们要在/var/
路径下创造一个/tmp/
的路径
2.全局变量检查
1 | if ( !haystack ) // second check |
我们双击haystack
,去寻找main对它的交叉引用
其中有四个交叉引用都是对haystack
进行lw(加载)
操作,只有一处是对它的sw(保存)
说明只有最后一处是对haystack
的赋值
只要这个函数被调用,haystack
就会被赋值,所以我们现在开始查找对这个函数的交叉引用
只发现了一处交叉引用,就是在先前的cgibin_parse_request
中
那么我们就要仔细分析cgibin_parse_request
的逻辑了
对cgibin_parse_request
逻辑的分析
我将此函数的一参命名为function_ptr
,现在着重搜索关于这个参数的交叉引用
可以发现和function_ptr
有关的部分在这个嵌套了if-else
和while
循环的代码块中
首先v9
不能是-1,可以看到只要环境变量REQUEST_URI
不为空就可以满足这个条件
h
这之后是从内存中取出一块硬编码的字符串和长度整形,和环境变量CONTENT_TYPE
做比较,如果相同则可以更加靠近对function_ptr
的交叉引用.
可以看到是将CONTENT_TYPE
与字符串"application/"
,以长度0xc做比较,如果比较成功,则v16=1
接着会以数组(&off_42C014)[3 * v16 - 1])
的形式调用(&off_42C014)[2])
的函数,这个函数是sub_403B10
a4是&v14[v17]
,即CONTENT_TYPE[0xc]
,环境变量未被比较的那部分,如果与"x-www-form-urlencoded"
相同,则调用sub_402FFC(a1, a2, a3)
按照直觉,这里的比较应该需要成功,因为sub_402FFC(a1, a2, a3)
的a1
来自于sub_403B10
的a1
,也就是function_ptr
进入这个函数后,我们没有发现很直接的对function_ptr
的交叉引用
但是调用了sub_402B40
,它的参数是对sub_403B10
中的int变量
的取地址,那么它就有可能会访问到sub_403B10
中的其他参数
我们接着往里面看
终于出现了对函数指针的调用,其中a1
就是sub_403B10
中的int变量
的地址,果然它以此为基址,3为偏移进行了解引用
此时我们回到sub_403B10
中观察几个变量的相对位置,会发现
(&v11)[3]刚刚好就是function_ptr
的位置,终于我们找到了function_ptr
的交叉引用
综上只要我们满足了CONTENT_TYPE
="application/"
+"x-www-form-urlencoded"
="application/x-www-form-urlencoded"
就可以对haystack
进行赋值,绕过检查
这就是我们最终需要利用的地方了
3.POST_content内容的检查
在sub_402FFC
中有对POST_content的内容的读取,读取后在sub_402B40(&v11, (int)buf, v8)
中处理(`POST_content被读入了buf)
这里对POST_content
的处理方式与对cookie的处理方式相似,也是初始化了两个结构体,将=
前后的内容分别读入两个结构体
然后判断这两个结构体中的内容是否不为空
所以POST_content
必须满足^(.+?)=(.+)$
| 3 | 脚本编写
虽然我们可以直接通过IDA分析出溢出长度,但总归用gdb调试一下会更靠谱
首先我们将0x400个字符a(padding
)放入一个payload文件中,然后写一个shell脚本
1 | !/bin/bash |
此时我们在另一个端口开gdb连接1234端口,即可开始对./htdocs/cgibin
进行远程调试
此时我们进入了调试界面
这个设备是没有开ALSR保护,所以libc固定,我们想要获取libc可以通过查询已经完成延迟绑定的got表计算libc_base
将其与libc中free的地址进行相减就可得到libc_base
然后将断点设置在hedwigcgi_main
返回时,查看栈溢出情况,最终确定了payload的基本架构
1 | payload =b'0'*0x3cd |
此处记录一些mips架构的相关知识
函数特性
叶子函数
- 当一个函数内部,没有调用其他函数时,这个函数称为叶子函数
- 叶子函数的返回地址在被调用时便存储在
$ra
中,函数返回时 直接通过jr $ra
跳回
非叶子函数
- 当一个函数调用了其他函数时,这个函数称为非叶子函数
- 非叶子函数的返回地址通过
sw
被存贮在栈上,在函数返回时会通过lw
操作将返回地址弹回$ra
,再jr $ra
返回 - 只有非叶子函数可以通过栈溢出覆盖返回地址
相似的,如果某些函数在调用过程中使用了$ *
寄存器,它也会先通过sw
操作将这些寄存器原本的值存在栈上,在返回时lw
恢复寄存器的值
所以通过栈溢出我们不仅可以控制返回地址,还可以控制大量的寄存器
流水线指令集特性
- 在 MIPS 架构中,当你执行一条 跳转(如
j
/jalr
)或条件分支(如beq
/bne
)指令时,**跳转/分支发生后,**仍然会执行紧接着的下一条指令(也就是那一行代码!),无论是否跳转成功! - 在 MIPS 架构中,由于其支持
数据缓存(D-Cache)
和指令缓存(I-Cache)
,这两个缓存是连个独立的,对内存的映射,当我们将shellcode写入栈中,数据缓存中的内容已经被更新成了我们写入的shellcode,但是指令缓存中的内容仍然是栈中原本的数据,如果此时就跳转会导致shellcode未被执行.保险起见,我们可以调用sleep
函数给予这两个缓存足够的时间同步
mips架构下的rop
1 | Arch: mips-32-little |
这是这个elf的保护,没有开启NX导致我们可以直接在栈上注入shellcode,但在跳转到shellcode前我们还要写一个ROP,包含了调用sleep使两个缓存同步以及跳转到shellcode上两步
MIPSROP
在IDA中有个十分好用的插件:mipsrop
比起ROPgadget
寻找的速度更快,更灵活
按照README安装好后,我们进入IDA界面
先点击这个
再点击这个,此时mipsrop插件启动成功,我们可以在python栏中输入相关语法查找所需的gadget
我们的调用链为sleep(1)--->shellcode
我们首先需要做的是将$a0
设置为1
此时使用以下指令寻找
可以找到将$a0
设置为1
的gadget,同时可以通过$s0
继续跳转,由于我们可以控制大量的寄存器,所以我们仍然能通过控制$s0
继续控制执行流
接下来我们就要调用sleep函数了,不过我们不能直接跳转过去,因为我们还没有设置返回地址($ra
),我们需要寻找到一段能控制$ra
且不能通过$ra
进行跳转的gadget,将ROP
布置在$ra
中才能使执行流一直得到控制
我们可以通过$s2
控制程序流,同时也可以控制$ra
,我们将$s2
设置为sleep
的地址,在调用sleep
时$ra
会被sw
在栈上
此时内存缓冲与指令缓冲同步完毕,我们需要将执行流跳转到栈上,此时的gadget可以通过mipsrop.stackfinder
寻找
此时的$s0
变成了栈上的地址,我们只需要通过调试在payload
中合适的位置填充上shellcode,并寻找一个能跳转到$s0
的gadget就可以执行shellcode
了
由于本题使用的溢出函数是sprintf
,存在\x00
截断,所以shellcode可以直接使用pwntools生成的mips.sh()
| 4 | EXP
qemu-usr
1 | from pwn import * |
顺利开启shell
qemu-system
[正在施工]