总结一下 Heap 的相关内容,便于后续调用
Github: ptmalloc 源码(只有 malloc 部分)
source code compilation && all libc debs
[0] 目录
- Patchelf的使用方式
- 详解 ‘申请与释放 chunk’
- chunk Extend and Overlapping
- Use After Free
- Double Free
- Unlink
- Series of Bin
- Series of House
[1] Patchelf的使用方式
在本地调试堆题时,不同的 libc 版本会有不同的 heap 管理器来管理 heap,因此我们需要将本地的 elf 文件的链接部分 patch 一下,使其与远程链接的 libc 文件保持一致。
[1-1] 确认libc
有时题目只给一个 libc.so.6,此时 patchelf 后程序会因为缺少 ld 文件而无法正常运行,因此我们需要通过这个 libc.so.6 来确认程序使用的 libc 版本,然后自己下载对应 libc 包并 patch
1 | from pwn import * |
将打印出来的三个偏移量的后三位,拿到 libcSearcher 去查即可
[1-2] libc 包的下载
patchelf没有的包可以在这里下载
1 | 以下操作均在文件夹 glibc-all-in-one 中完成 |
有时我们需要的 libc 包不在 list 中,我们可以自己尝试解构命令来下载。
1 | 访问https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/,查看所有可以下载的 libc 版本 |
[1-3] patch elf文件
识别需要 patch 的文件
1
2
3
4
5
6 [path/to/glibc-all-in-one/libs/libc6_2.23-0ubuntu11.3_amd64]
└─$ ls
......
找到以下文件
ld-linux-x86-64.so.2 # ld-2.23.so 也行
tip1:高版本的libc库没有 ld-2.23.so
这种文件,但它与 ld-linux-x86-64.so.2
等价,都指向相同的动态链接器文件,他们实际上是同一个文件的不同名称。
tip2:
1 | 被patch的elf文件: Pwn |
[1-4] 恢复 debug symbol
[1-4-1] patchelf list 中有相关包
其实没有相关包的话用最近版本的 libc 也能.debug
1 | 下载对应 libc 包 |
[1-4-2] list 中没有相关包
查看这个教程编译源码来恢复符号
[1-5] 恢复源码(与patchelf关系不大)
[2] 详解 ‘申请与释放 chunk’(未完成)
[2-1] 申请 chunk
如果是第一次申请,则 malloc 一块内存来存放
tcache_perthread_struct
查看要申请的 chunk 的大小,记作
SIZE
检查各种 bin
- (libc2.26及之后)
SIZE
属于 [0x0,small bin size),检查 tcachebin,有合适的 chunk 则返回 - 根据版本有不同选择
- (libc2.26之前)
SIZE
属于 [0x0,0x78],检查 fastbins,有合适的 chunk 则检查 size 域是否正确,正确则返回 - (libc2.26及之后)
SIZE
属于 [0x0,0x78],将对应 fastbin 一整条链挪进 tcachebin 的对应链上,并取出 newest_chunk 返回
- (libc2.26之前)
- smallBin largeBin 还没学
- 检查 unsortedBin,如果
SIZE
小于 “unsortedbin 的某个 chunk 的 size”,则:- 以0x10为基本单位切割出申请的 chunk 并返回,剩下的部分叫做 last remainder chunk
- (libc2.26及之后)
- 检查 topchunk,如果
SIZE
小于 “topchunk 的 size”,则:- 以0x10为基本单位切割出申请的 chunk 并返回
- 用 Brk 再拉几页内存出来,还没学
[2-2] 释放 chunk
- 查看要释放的 chunk 的大小,记作
SIZE
1 | # 猜测,尚不确定 |
[3] chunk Extend and Overlapping
[3-1] 分类
本质是通过修改 chunk_header 来实现用 chunk1 控制 chunk2 的内容的效果。分为前向和后向两种
[3-1-1] 后向 Overlap
堆区模型:1
2---chunk1(0x21)---
---chunk2(0x21)---
本质:修改低地址 chunk1 的 size 域,在修改 chunk1 内容时会越界修改掉 chunk2 的内容
[3-1-2] 前向 Overlap
堆区模型:1
2
3
4
5---chunk1(0x81)---
---chunk2(0x21)---
---chunk3(0x21)---
---chunk4(0x81)---
---chunk5(0x21)---防止 top_chunk 合并
本质:修改高地址 chunk4 的 pre_inuse 域和 prev_size 域,通过 free 时 bins 的机制来进行前向合并 chunk1 ,从而再次 malloc 时可以控制中间的 chunk2 与 chunk3
[3-2] 具体利用手法
[3-2-1] off-by-null
[3-2-1-1] 概述
off-by-null
指的是程序在写入堆的时候,会在输入字符的最后用'\x00'
截断
通过申请 xxx8h 大小的 chunk,当我们输入 xxx8 个字节时,程序会将下一个 chunk 的 size 域的低一个字节覆盖为0,
这样的操作会让 prev_inuse 位置零,使程序误以为前一个 chunk 已经被释放,从而与前 n 个 chunk 发生合并
[3-2-1-2] POC
1 | alloc(0x80) # 0 0x91 |
[4] Use After Free
[4-1] 概述
libc2.26 - libc2.31,主要是 tcachebin UAF
见 Kinds of Bin -> Tcachebin -> UAF
[5] Double Free
[5-1] 概述
libc2.27之前,主要是 fastbin double free
见 Kinds of Bin -> Fastbin
libc2.27-2.28,主要是 tcachebin double free
libc2.29-libc2.31,tcachebin加入了检查机制,所以仍然考虑用 fastbin/smallbin
见 Kinds of Bin -> Tcachebin
[6] Unlink
[6-1] 利用条件
free 的
chunkC
前后 chunk 的 fd/bk 可以被修改UAF,free 以后修改 fd/bk
堆溢出,伪造 chunk,修改 fd/bk 的同时修改
chunk
的 prev_size 和 prev_inuse
没见过太多案例不太会概括这一点
- pie 没开,bss 段的 heap_list 可以被拿来当做 fd/bk
[6-2] 原理
ctfwiki 上的 FD BK fd bk 感觉写的乱七八糟的,重新整理一下思路
unlink 概括来讲,就是利用 free 时的机制,实现任意地址写非任意值的技术
[6-2-1] 触发 unlink
在释放 size 大于 smallbin 最小值的 chunkC 时,ptmalloc 会检查 chunkC 物理相邻的前后两个 chunk 是否正在被使用,如果没被使用,则会触发前向/后向合并,从而触发 unlink
前向合并:chunkC 的 prev_size 为前一个 chunk 的 size,且 chunkC 的 prev_inuse 位置零
后向合并:chunkC 的后一个 chunk 已经在 binlist 当中
[6-2-2] 古早版本
假设在一个双向链表 bin 中,有 BK <=> CUR <=> FD
三个 chunk 的关系为
1 | BK.fd == &(CUR.prev_size) |
在古早的版本中,unlink 的具体过程为1
2CUR.bk->fd = CUR.fd # CUR 的后一个 chunk 的 fd 指针指向 CUR 的前一个 chunk
CUR.fd->bk = CUR.bk # CUR 的前一个 chunk 的 bk 指针指向 CUR 的后一个 chunk
如果我们修改 CUR 的 fd 和 bk 指针,就可以实现 Any address write
1 | // SIZE 在32位为 4,64位为8 |
[6-2-3] 加入 check 以后
check1
check
1
2
3
4
5
6
7
8if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
// 省流版:
// CUR 的 fd 指向的 chunk 的 bk 必须存的是 CUR 的地址
// CUR 的 bk 指向的 chunk 的 fd 必须存的是 CUR 的地址
if ( !( CUR.fd->bk == &(CUR.prev_size) && CUR.bk->fd == &(CUR.prev_size) ) )
malloc_printerr(...)bypass
1
2
3
4
5
6
7
8
9
10
11// 让 CUR 的 fd 和 bk 指针都指向 CUR 自己
CUR.fd = &(CUR.prev_size)
CUR.bk = &(CUR.prev_size)
// check 时
CUR.fd->bk = (&(CUR.prev_size))->bk = CUR
CUR.bk->fd = (&(CUR.prev_size))->fd = CUR
// unlink
CUR.fd->bk = (&(CUR.prev_size))->bk = &(CUR.prev_size) + 2*SIZE
CUR.bk->fd = (&(CUR.prev_size))->fd = &(CUR.prev_size) - 3*SIZE这样虽然不能实现任意地址写,但是也可以让 CUR 的 fd 和 bk 指针指向不正确的位置
[7] Series of Bin
[7-1] Tcachebin
[7-1-1] 概述
- Tcachebin 为 LIFO 单向链表,如下
1 | Tcachebin: (头节点)newest_chunk(first out) -> ... |
[7-1-2] Leak
[7-1-3] Write
[7-1-3-1] UAF
Libc: 2.26 - 2.31
应用条件:
- UAF
相关特性:
- Tcachebin 跟筛子没什么区别,通过 UAF 修改 newest_chunk 的 fd 指针为 Any address,再通过两次 malloc 就可以在 Any address 处申请到一个 chunk 供我们使用
POC:
1 | 1. alloc(chunk0) |
例题:
[7-1-3-2] Stash Double Free
Libc: 2.27 - 2.31
应用条件:
7个 tcachebin chunk,2个 fastbin chunk,bins 构造完成后至少可以 alloc 10次
free 后还能再 free 同一个 chunk
相关特性:
2.27之后,tcachebin 加入了 key 值检验,因此直接劫持 tcachebin 链比较困难(需要伪造 key 值)。
(具体是 stash 机制,我是按下面这样理解的)但当 tcachebin 某一条链的 chunk 全部取出,且对应大小的 fastbin 上仍有 chunk,ptmalloc 会将 fastbin 对应链上的全部 chunk 取出,并按 fastbin 顺序装载到 tcachebin 中。此时 ptmalloc 不会检测 fastbin 的全部 chunk 是否合法,并且 ptmalloc 还会为每个 chunk 构造合法 key 值,所以我们可以先劫持 fastbin 链,然后清空 tcachebin 后劫持 tcachebin 链。
在检查 key 值之后,从 tcachebin 中申请 chunk 的检测机制比较薄弱,不需要考虑 fd 指向的地址的 size 域是否合法
POC:
申请9个同样大小的 chunk,依次释放,再释放一次 chunk7
1
2
3
4
5
6
7for i in range(9):
alloc(i) # 0-6 tcahcebin chunks || 7,8 fastbin chunks
for i in range(9):
free(i)
free(7) # fastbin: chunk7 -> chunk8 -> chunk7申请7个 chunk,清空 tcachebin 链。再申请 chunk7,将 fastbin 中的所有 chunk 转移到 tcachebin 中.
1
2
3
4
5for i in range(7):
alloc(i)
alloc(idx=7,content=address_to_alloc)
# 此时 tcachebin: chunk8 -> chunk7 -> address_to_alloc申请3个 chunk,此时我们就可以通过改变 chunk9 的值,来 write 任意位置了.
1
2
3alloc(8)
alloc(7)
alloc(9,content=anything_to_write)
例题:
[7-2] Fastbin
[7-2-1] 概述
- Fastbin 为 LIFO 单向链表,如下
1 | fastbin: (头节点)newest_chunk(first out) -> ... |
[7-2-2] Leak
[7-2-3] Write
[7-2-3-1] Double Free
Libc: 2.27之前
应用条件:
- free 后还能再 free 同一个 chunk
fastbin 相关特性:
free
: 在 free chunk 到 fastbin 的过程中,会有以下 check:- 检测这个 chunk 和尾节点是否是同一个 chunk,如果是,则触发
Error in './vuln': double free or corruption (fasttop): 0x17170c0
- 检测这个 chunk 和尾节点是否是同一个 chunk,如果是,则触发
malloc
: 从 fastbin 申请 chunk 时,会有以下 check:- 检测这个 chunk 的 fd 指向的内存的 size 是否符合所属 fastbin,如果不属于,则触发
Error in './vuln': malloc(): memory corruption (fast): 0x7f4cf12d6afe
- 检测这个 chunk 的 fd 指向的内存的 size 是否符合所属 fastbin,如果不属于,则触发
POC:
构建 fastbin: chunk1 -> chunk2 -> chunk1
1
2
3
4
5alloc(1)
alloc(2)
free(1)
free(2)
free(1)malloc(chunk1),同时修改 chunk1 的 fd, 此时 fastbin 中构造为 chunk2 -> chunk1 -> address(address 有如下选择)
- libc.sym[‘__malloc_hook’] - 0x23
1
2
3pd = libc_base + libc.sym['__malloc_hook'] - 0x23
# 注意不要把 '\n' 也当做 payload 传过去了
alloc(1,payload=pd)
- libc.sym[‘__malloc_hook’] - 0x23
malloc(chunk2), malloc(chunk1)
1
2alloc(2)
alloc(1)malloc(address),同时修改 address 处的内容,对应
'2.'
中的不同选择,有不同的填充方式- payload = b’a’*19 + one_gadget
1
2
3
4
5
6mallochook-0x23 prev_size 0x000000000000007f
mallochook-0x13 0x6161616161616161 0x6161616161616161
mallochook-0x03 0xXXXXXXXXXX616161 0x0000000000XXXXXX
^
|
正好是__malloc_hook的位置
- payload = b’a’*19 + one_gadget
例题
[7-3] Unsortedbin
[7-3-1] leak
Libc: Any
应用条件:
UAF
alloc 至少不会覆盖 bk
当unsortedbin中有且仅有一个chunk时,
该chunk的fd和bk会指向 &main_arena + 96,
貌似libc2.31是96,libc2.23是88,
具体做题用 pwndbg 看最低四位是 0x8 还是 0x0,0x8->88,0x0->96
而这个地址可以用ida查看对应libc的 malloc_trim 函数找到,从而帮助计算 libc_base.
当然这个地址也是 &__malloc_hook + 0x10,具体看下面的代码注释.
如果我们把 unsortedbin 中唯一一个 chunk 记作 chunk0,则:
1 | # 内存中的 main_arena 地址,一般为0x7f... |
bypass
ssize_t read(int fd, void *buf, size_t nbytes)
会用’\x00’填充不够 nbytes 的部分,因此我们在alloc
或者edit
的时候注意 nbytes = 0x08 即可,这样不会覆盖 bk 的 &main_arena
[7-3-2] write
[7-3-2-1] Direct
Libc: 2.23
相关特性:
- 低 libc 版本的 unsortedbin 可以像 tcachebin 一样,通过伪造链来申请一个 fake_chunk
POC:
1 | # 初状态 unsortedbin --> chunk_victim |
[7-4] Largebin Attack
[7-4-1] 概述
Largebin 是 FIFO 的双向链表,chunk 结构为👇1
2
3
4
5
6
7
8
9
10+-------------------------------------------------+
| prev_size | size |0|0|1||
|------------------------|------------------------|
| fd | bk |
|------------------------|------------------------|
| fd_nextsize | bk_nextsize |
|------------------------|------------------------|
| user_data |
| . . . |
+-------------------------------------------------+
[7-4-2] leak
[7-4-3] write
[7-4-3-1] 对 libc 有要求
libc: 2.30 及以前?
[7-4-3-2] 对 libc 没要求
libc: Any
概述:
通过修改 largebin 中的 bk_nextsize 为 target_addr - 0x20,可以在 target_addr 处写一个堆地址
POC:1
2
3
4
5
6
7
8
9
10alloc(0x500) // chunk1
alloc(0x10) // gap1 防止合并
alloc(0x510) // chunk2
alloc(0x10) // gap2 防止合并
free(chunk1) // chunk1 进入 unsortedbin
alloc(0x520) // chunk1 进入 largebin
free(chunk2) // chunk2 进入 unsortedbin
chunk1.bk_nextsize = target_addr - 0x20 // UAF等方法修改 chunk
alloc(0x520) // chunk2 进入 largebin,触发 largebin attack
// target_addr 处被写入 chunk2 的地址
[8] Series of House
[8-1] House of Orange
[8-1-1] 概述
HouseofOrange是在程序没有可以操控的free时,利用ptmalloc的管理机制强行制造出一个unsortedbin中的chunk的技术。
[8-1-2] 伪造chunk需求
top_chunk的结束地址必须页对齐
- 一般情况下,ptmalloc设置top_chunk为0x21000,我们申请一个0x10的chunk0后,chunk0加上chunk_head是0x20大小,此时top_chunk切割后还剩下0x20fe0大小,为了页对齐,我们伪造top_chunk的大小为0xfe0即可
top_chunk.size >= MINSIZE
top_chunk.size < chunk_size + MINSIZE
top_chunk.prev_inuse == 1
- 第一点中说的0xfe0要变为0xfe1
[8-2] House of Force (HOF)
[8-2-1] 概述
House of Force 是通过 topchunk 来实现任意地址写的操作。
具体来说,我们先修改 topchunk 的 size 域,接着用 malloc(c_size) 从 topchunk 切割一个 chunkF,在切割前,通过构造 malloc chunkF 时的 c_size,能改变 main_arena 中指向 topchunk 的指针为任意值,从而在切割时能在任意地址申请一个 chunk,进而实现任意地址写。
[8-2-2] 具体原理
从 topchunk 申请 chunk 的具体实现是这样的,其中的代码可以这样理解(我没读源码,只是从应用角度逆推原理):
⚠注:本段代码的数据全为 unsigned,也就是说,我们要申请的 chunk 的 size 即 nb 会被转化为无符号,topchunk 的 size 也是无符号的
victim: 获取指向目标 chunk 的指针,在这是 topchunk
chunksize(victim): 获取 victim 指向的 chunk 的大小
nb: chunk 的实际大小,malloc(size) 时 nb = request2size(size)
这个地方我也没搞清楚,主要有的时候nextchunk的prev_size域也被拿来当作chunk的一部分,就导致我不是很清楚对齐这一块怎么做的,后面仔细读一下源码再来订正
chunk_at_offset(victim, nb): return(victim + nb)
1 | // 获取当前的top chunk,并计算其对应的大小 |
House of Force 逻辑如下:
首先通过堆溢出之类的手段,改变 topchunk 的 size 域为 -1,即0xffffffffffffffff
在 chunkFakealloc(size)
时,通过构造 size(由于nb = request2size(size)
,所以构造 nb 的本质就是构造 size),切割程序会运行到如下代码
1 | remainder = chunk_at_offset(victim, nb); |
此时,remainder 会被赋值为 victim + nb,而下一行代码 av->top = remainder
使得 main_arena 中指向 topchunk 的指针被修改
值得注意的是,若 nb 为负数,victim + nb 会溢出,从而将 victim 修改为比 victim 自己指向的地址更低的地址。
也就是说,通过构造 nb,我们可以将 av->top 改写为 Any Address
而通过再一次的 chunkNewalloc(nb)
,我们就可以在 Any Address
处申请一个 chunk,进而实现任意地址写.
总而言之,在改变 topchunk 的 size 域为 -1 之后,只要我们能够精心构造 malloc(nb)
时的 nb,就可以实现任意地址写。
那么接下来的问题很显然,如何构造 nb ,也就是如何构造 size?
size 的构造
malloc 的过程中会遇到如下检查,req
就是我们申请的 chunk 的大小,就是前文提到的 request2size的参数
1 | // MINSIZE = 2 * SIZE_SZ |
其中,SIZE_SZ
在 64 位是 0x08,32 位为 0x04,因此 MINSIZE
在 64 位中是 0x10,32 位为 0x08
由于 -2 * MINSIZE 被转化为了无符号数,拿 64 位举例,req 很难超过 0xfffffffffffffff0 这么大的数字,所以这个检测是很好绕过的,或者说根本不用 care 这个检测。
接下来,req
会经过如下函数,转化为要申请的 chunk 的 真实 size,也就是 nb
1 | //MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1 |
其中,MALLOC_ALIGN_MASK
在 64 位是 0xF 即 1111b,32位为 0x7 即 111b.
这里有点没想明白 request2size 的过程,我先假设在 malloc(size) 时,nb = request2size(req) = request2size(size)
由于 nb 都是对齐的,所以我们可以不用考虑 & ~MALLOC_ALIGN_MASK
,所以 size = req = nb - SIZE_SZ - MALLOC_ALIGN_MASK
至此,我们构造出了 size,HOF结束.
[8-3] House of botcake
libc: 2.29 -
[8-3-1] 概述
由于 libc 2.29 之后加入了 tcachebin 检查机制,所以 tcachebin double free 变得没有那么好利用
但由于放入 unsortedbin 中的 chunk,再被 free 进 tcachebin 的时候检测相当薄弱
所以我们可以先把 chunk free 进 unsortedbin,再 free 进 tcachebin,当我们切割 unsortedbin 时就能修改 tcachebin 里重叠的 chunk 的 fd/bk,造成 tcachebin poisoning
[8-3-2] POC
1 | for i in range(9): |
[8-3-3] 一些疑问
不构造 chunk 7,直接把 chunk8 放进 unsortedbin 再放进 tcachebin 中是完全可行的,但当我切割 chunk8 时就会报错 malloc(): unsorted double linked list corrupted\n
1 | tcachebins |
目前猜测是: 直接把 chunk8 放进 tcachebin 会导致原本存在 chunk8 fd/bk 处的 &main_arena+88 被替换,导致 malloc 错误
[8-4] House of banana
具体细节可以看这篇文章
[8-4-1] 概述
当程序显式调用exit()
函数时,程序会通过exit -> _dl_fini ->((fini_t) array[i]) ()
这条调用链调用 array[i]()
。
而 array[i]()
是通过 _rtld_global._dl_ns._ns_loaded.l_next->l_next->l_next
定位的
通过 largebin attack,我们可以篡改_rtld_global._dl_ns._ns_loaded.l_next->l_next->l_next
指针,将其值修改为我们可以控制的堆地址,
而通过在堆上伪造一个 link_map 结构体,我们可以欺骗程序,使其执行 array[i]()
时执行在结构体里放入的提权函数,进而提权
[8-4-2] POC
1 | # largebin attack 修改 _rtld_global._dl_ns._ns_loaded.l_next->l_next->l_next |
[8-4-3] 如何构造 fake_chunk?
见 Pwn - IO_File and ld.so exploit summary