lab-03: 堆上漏洞及其利用
与栈上缓冲区溢出以及格式化字符串漏洞不同,堆漏洞无论是在检测、利用、还是防御上都更显复杂。在本次作业上,我们将以简单的堆场景“管中窥豹“地学习堆相关的知识,包括堆块的结构,堆上常见的漏洞成因以及复现。为了顺利完成本次作业,你需要掌握:
- 堆块的基本结构,其中的申请与释放
- 堆上常见的漏洞类型
- 堆上漏洞构建的原语 (primitive) 以及漏洞利用
- (可选) 堆上漏洞的防御
堆管理器基础 (40 points)
如课堂上介绍的,堆 (heap) 相比于栈而言,提供了动态内存管理的能力,其中动态表现在:
- 空间大小的动态。栈的大小在程序被编译时就已经划分,无法处理可变长度的数据对象。
- 生命周期的动态。栈上空间用于管理临时变量,当脱离对应的作用域时,栈上的空间即被回收,而堆则允许开发较为自由的申请和释放内存空间。
对于特定场景,只需要 (1) 的动态而不需要 (2) 中管理周期,堆的实现可以做的非常简单,如 Linux 内核启动过程中的简单堆,就只需要支持内存申请操作,而无需去考虑释放和管理。在多数一般场景下,往往存在大量的堆块申请和释放,如何使得这些堆操作高效便是堆管理器 (heap manager/allocator) 负责的任务。
不同的软件系统都可能会依赖不同的堆管理器,进而表现出不同的堆操作行为。例如
- dlmalloc (Doug Lea's Malloc): 最早的 C 程序堆管理器之一,ptmalloc 的前身。
- ptmalloc (Pthread Malloc): GNU Libc 的默认堆管理器。
- mallocng (Malloc Next Generation) Musl Libc 自 1.2.1 后的默认堆管理器。
- jemalloc: FreeBSD 下最早使用,也被 Android 5-7 版本下使用(自 Android 8 开始,scudo 成为默认堆管理器)
- ......
堆的维护和管理,效率以及安全性的兼顾都相当复杂,使得任何一个堆管理器本身就成为了「复杂的系统」,再加上各类堆管理器玲琅满目,多种多样,堆的知识并非几堂课几次作业就能深入的。本章作业将主要以多数 Linux 发行版使用的 ptmalloc 作为研究对象,尽可能通用的阐述堆的内容。如果同学们对其他类型的堆管理器感兴趣,也可以找到相关代码进行学习。
本次作业主要以 libc-2.31 版本的代码作为分析。源代码可以在该链接下载
示例
请查看仓库目录 lab-03/basic
,阅读 Makefile
以及 example.c
,该程序展现了一个典型的「目录」利用(相比于栈题而言)。此外,程序还应用了 glibc 中的 __malloc_hook
和 __free_hook
来记录堆操作。特别的,程序提供了函数 heap_debug
来把堆内容的数据转储。
进入到该目录,通过 make build
,可以完成对于这个程序的编译(注意,由于高版本的 glibc 已经移除了堆上的 hooking,所以 Makefile
中通过旧版本的 gcc docker 进行编译)。并且还使用了 patchelf
将目标程序默认的装载器路径与 glibc 路径都修改到了提供到的 2.31 版本上。
构建完后,执行 ldd
命令将看到程序使用了同级目录下的装载器和库代码
$ ldd example
linux-vdso.so.1 (0x00007ffdffb8a000)
./libc-2.31.so (0x000078bb465e7000)
./ld-2.31.so => /lib64/ld-linux-x86-64.so.2 (0x000078bb467db000)
如果使用宿主机本身的编译链 + 不做
patchelf
,因为 glibc 版本的不同,后续的堆分析可能也会跟示例的不一致。
见 test.py
,其通过 pwntools
工具与编译好的目标程序交互。脚本中简单定义了与目标堆目录程序操作一致的函数,如 handle_add
就对应了目标 C 程序中的 user_add
,编写此类交互程序中请特别注意 IO 上的长度,需不需要换行符等,并且通过合适使用 recvuntil
或者 sendafter
来保证发送的数据被预期的逻辑消耗。
运行 test.py
,脚本发送的数据将调用数次 user_add
以及 user_del
,并激活 heap_debug
打印了堆上数据到 dump*.bin
文件中。
$ python3 test.py
[DEBUG] Sent 0x2 bytes:
b'5\n'
[*] Process '.../example' stopped with exit code 0 (pid 28518)
$ ls
dump_772000_0.bin example example.c ld-2.31.so libc-2.31.so Makefile test.py
这里将示例分析这段 dump 内容的头部,可以用各自熟悉的二进制阅读工具
# NOTE: 由于程序是 docker 内构建的,所以转储的内容可能无读权限
$ sudo chmod 444 dump_772000_0.bin
$ hexdump -C dump_772000_0.bin | less
00000000 00 00 00 00 00 00 00 00 91 02 00 00 00 00 00 00 |................|
00000010 00 00 00 00 00 00 07 00 00 00 07 00 00 00 00 00 |................|
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
000000a0 00 00 00 00 00 00 00 00 10 23 77 00 00 00 00 00 |.........#w.....|
000000b0 00 00 00 00 00 00 00 00 a0 22 77 00 00 00 00 00 |........."w.....|
000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000290 00 00 00 00 00 00 00 00 71 00 00 00 00 00 00 00 |........q.......|
000002a0 60 23 77 00 00 00 00 00 10 20 77 00 00 00 00 00 |`#w...... w.....|
000002b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000002c0 31 32 33 34 35 36 00 00 00 00 00 00 00 00 00 00 |123456..........|
000002d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000002e0 10 23 77 00 00 00 00 00 74 72 79 20 65 76 65 72 |.#w.....try ever|
000002f0 79 74 68 69 6e 67 00 00 00 00 00 00 00 00 00 00 |ything..........|
00000300 00 00 00 00 00 00 00 00 51 00 00 00 00 00 00 00 |........Q.......|
00000310 d0 23 77 00 00 00 00 00 10 20 77 00 00 00 00 00 |.#w...... w.....|
00000320 6d 20 62 6f 62 00 00 00 00 00 00 00 00 00 00 00 |m bob...........|
00000330 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
分析之前,复习一下课堂上已经学习的内容:
- ptmalloc2 下的堆块,都通过结构
malloc_chunk
进行表述,后文简称 chunk,又或称其为元数据
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
// ...
如源代码中的注释所述,这个结构其实容易让人误解,建议同学阅读这个结构定义下 Colin Plumb 陈述的注释,这里只做简单的叙述。
-
- chunk 的结构基于 boundary tag 设计,及包含了本 chunk 的大小以及上一个 chunk 大小,这样的设计允许开发者可以高效的向上或向下遍历堆中目前存在的 chunks。
-
- 一个申请出来的 chunk 如下结构(
fd
,bk
,fd_nextsize
,bk_nextsize
都是给释放后的 chunk 维护使用(这种元数据的空间复用可以减少存储代价,但也带来了安全风险)
- 一个申请出来的 chunk 如下结构(
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
malloc
拿到的指针,是上图中的 mem
而不是 chunk
。mchunk_prev_size
和 mchunk_size
竭力做到了对用户透明。
- - 在 64 位下,malloc
申请到的实际内存大小为 chunk_size = max(( request_size + 8 ) # 16, 32)
即请求的大小加上 8 然后再向上对齐到 16 的倍数 (但一定要大于等于 32 字节的元数据)。
OK,有了这些知识,我们可以将之前 hexdump
的内容先划分出三个堆块,可以简单的通过下面的 excel 图呈现
根据遗留的数据,我们可以判断出,位于 0x290
偏移出的堆块是已经被释放掉的属于 Bob
的 user_info
(通过 123456
的 password
成员反推)。与其相邻的则是堆上申请的属于 Bob
的 introduction
堆块(统括 m bob
的内容反推)。通过红色箭头,我们可以将 metadata 中的指针指向关系表征出来,而绿色箭头则表示了用户程序逻辑中的堆上指针。
但将图上数据,套用到前文中提到的 malloc_chunk
,会发现有些地方不符预期:
0x300
偏移处的prev_size
本应该记录上个被释放堆块的大小的,但这里却仍然为0
- 即使
fd
指针看起来没啥毛病,但原本bk
指针的位置,却不像是构建了双向链表:可以看到偏移0x2a8
和0x318
处的bk
都指向了 heap 顶部一个0x290
大小的,并不是由我们程序申请的一个 chunk
这些差异来自于 ptmalloc 堆上名为 tcache 的优化,该优化自 glib 2.26 引入,默认启用。请回顾课上内容,或者阅读文档末的拓展链接了解 tcache 的细节,这里仅简单的展示:
- 堆顶上
0x290
的堆块是堆管理器为主线程分配的,类型为tcache_perthread_struct
# define TCACHE_MAX_BINS 64
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
- tcache 为从 24 字节用户数据到 1032 字节的 64 类不同确定大小的堆块提供了一个线程粒度的「单向链表」的无锁快速缓存,每个缓存最大容纳 7 个 free 的结构。
- 图中相当于有
tcache_perthread_struct.counts[3] = 7 & tcache_perthread_struct.entries[3] = 0x772310
;tcache_perthread_struct.counts[5] = 7 & tcache_perthread_struct.enries[5] = 0x7720a0;
- 图中相当于有
- 对于每一个释放后存储在 tcache 链表的对象,不用
fd
和bk
,而是用tcache_entry
中的next
和key
,其中key
指向 tcache 管理结构tcache_perthread_struct
的堆块,即图中最上部的0x772110
。
如此一来,我们就理解了 dump*.bin
中所有释放对象的内容元数据所代表的含义。
实践
- 阅读
example.c
代码,在报告中简述这个目录程序的逻辑;通过make build
完成对程序的编译和 patch,提供 ldd 执行后的截图;(10 points) - 阅读和运行
test.py
代码,分析打印的dump*.bin
的内容。要求类似示例图一样将所有申请和释放的对象标记出来,特别标注出 tcache 单向链表管理的对象);(20 points) - 将
test.py
中注释的两行handle_del
取消注释,再次运行,新产生的dump*.bin
和之前的相比有何变化?多释放的属于William
和Joseph
的堆块由什么结构管理,还位于 tcache 链表上么?请复习课堂上的内容,在报告中进行回答;(10 points)
堆上常见漏洞 (30 points)
简单分类,我们可以将堆上常见的漏洞划分为如下三种类型
1. 未初始化读
见 lab-03/uninit
目录下的 uninit
,对比之前的 example.c
可以找到其中的未初始化读问题。
2. 堆溢出(读写)
见 lab-03/overflow
目录下的 overflow.c
,对比之前的 example.c
可以找到其中的堆溢出问题。
特别注意,堆溢出根据能够溢出到的区域,可以分为
- 堆块内溢出,这类溢出能力较弱,但往往难以检测,其利用需要对相应结构体内不同成员有关的代码逻辑进行分析。
- 跨堆块溢出,借用该溢出,我们往往可以获取/破坏堆管理器有关的数据。
在此基础上,有两类特殊的溢出,称为
off-by-one
和off-by-null
,表述仅仅能进行一字节溢出的场景(实际上,因为编程中“从0开始数”的特性,这种一单元的溢出是非常常见的。
3. 释放后使用 (use-after-free)
见 lab-03/uaf
目录下的 uaf.c
,对比之前的 example.c
可以找到其中的释放后使用问题。
特别注意,释放后使用往往也可以根据“使用”的不同进行如下划分
- 释放后使用读(use-after-free read),类似未初始化读,这类读能力可以进行信息泄露,如果目标对象是已释放的堆块,则可以读取到如果
fd
指针所代表的堆地址等信息,用于构建更强的攻击原语。- 释放后使用写(use-after-free write),如果目标对象是已释放的堆块,则可以破坏堆管理器为 free 堆块所布置的元数据,进而构建更强的攻击原语。
- 两次释放(double free),在读写之上,如果攻击者能再次释放已经被释放的堆块,则可以完全破坏堆管理器对于 free 堆块的管理。
这里提到的攻击原语(primitives),将在后文进行简单的介绍
实践
- 找到
uninit/uninit
中的未初始化读漏洞,在报告中给出分析;编写攻击脚本 ,完成对于堆上 flag 内容的窃取; (10 points)- 远程环境位于 IP:
8.154.20.109
, PORT:10400
- 远程环境位于 IP:
- 找到
overflow/overflow.c
中的堆溢出漏洞,编写攻击脚本触发该漏洞;(10 points) - 找到
uaf/uaf
中的释放后使用漏洞,编写攻击脚本触发该漏洞;(10 points)
堆上漏洞的利用 (30 points)
堆管理器的复杂性在前文已经有所讨论,那么,不同的堆管理器自然也会有不同的利用技巧,更精确的,通用堆管理器下,针对不同的管理结构,如 ptmalloc2 下的 tcache 和各类 bin 上的技巧也纷繁复杂。本次作业中,将简单介绍两类在多数堆管理器上都可以构建的利用原语:
劫持 freelist 实现恶意地址分配
根据前文介绍,我们知道多数堆管理器都通过链表(单链表/循环链表)来维护已经释放的堆块,这类链表统称为 freelist,当 freelist 中存在合适大小的堆块时,管理器就无需去堆顶上再申请更多内存,返回链表上已有的堆块即可。那么,假设攻击者可以通过堆上漏洞(跨堆块溢出或者释放后使用写)去破坏 freelist 链表上的指针,如此这般,就可以将下个要申请的堆块布置到攻击者计划的地址上。该攻击流程可以通过如下图展示:
图中假设攻击者能将链表上最后一个成员的指针从 NULL 修改到目标 exit$GOT
,那么,接着连续进行分配,就可以获得一个指向目标 GOT 的堆块。恶意地址分配原语往往用于实现任意地址的读写。
类型混淆
针对 freelist 破坏的保护措施现在已经层出不穷,常见的方式包含
- 将 FD 指针放置到不那么容易被破坏的位置。如内核堆中,freelist 指针就被放在对象的“中间”而不是开头;
- 将 FD 指针进行“加密”保护,如在 glibc 2.32 版本后,堆上无论是 freelist 的指针,还是一些其他的指向堆块的指针(如
key
指针),都会有一层异或加密。这使得攻击者想伪造一个 freelist 指针的难度大大提升;
因此,目前堆上攻击最主流的方式是通过 UAF 构建类型混淆原语。该类攻击方式往往不用去管堆管理器实现的细节,而只需要考虑用于做混淆的堆块上具备的能力。
比如有堆结构 A 可以提供对自身堆块数据的读写能力;而堆结构 B 中会将自身某个成员指针解引用做数据读,那么我们就可以通过如下图所示的混淆,构建一个任意地址读的能力:
实践
- 利用
overflow/overflow.c
中的堆溢出漏洞,通过劫持 freelist 的方式(10 points),写 exit GOT 表数据将执行流劫持到backdoor
函数,从而完成弹 shell,执行flag.exe
取得 flag(5 points)- 远程环境位于 IP:
8.154.20.109
, PORT:10401
- 远程环境位于 IP:
- 利用
uaf/uaf
中的释放后使用漏洞,通过类型混淆的利用方式构建任意地址读写原语(10 points),进而通过内存破坏实现弹 shell,执行flag.exe
取得 flag;(5 points)- 远程环境位于 IP:
8.154.20.109
, PORT:10402
- 注:相比于上个目标,这个程序开启了 RELRO 保护,故无法破坏 GOT 表内容;
- 提示:回到最开始的
example.c
,位于 libc 内存中有其他的攻击目标可以作为写的对象来实现控制流劫持。
- 远程环境位于 IP:
Bonus - 堆上保护机制 (extra 20 points)
更高版本的 glibc,对于 freelist 进行了指针上的保护,见目录 protect
下的代码和目标程序。在这个情况下,是否还能通过类似的方式完成漏洞利用呢?
- 远程环境位于 IP:
8.154.20.109
, PORT:10403
拓展阅读
堆有关的攻击丰富多样,针对 glibc 的不同版本和不同 bin 前人已经总结出大量的 House Of XXX 原语。本次作业仅简单的展现了其中几个,感兴趣的同学可以通过更过竞赛题目和 writeups 进行了解
- CTFWiki, ptmalloc2
- Shellphish, how2heap
- Azeria, Understanding the GLIBC Heap Implementation