news 2026/8/28 18:27:37

从CTF赛题解析UAF漏洞与tcache poisoning堆利用实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从CTF赛题解析UAF漏洞与tcache poisoning堆利用实战

1. 从一道CTF赛题看堆利用的实战艺术

最近在复盘一些经典的CTF(Capture The Flag)题目,特别是关于二进制安全的Pwn方向,总能发现很多值得深挖的细节。今天想和大家聊聊一道来自2022年CISCN(全国大学生信息安全竞赛)初赛的题目——newest_note。这道题在当年引起了不少讨论,它本身是一个经典的堆(Heap)利用题目,涉及UAF(Use-After-Free)漏洞。但我想聊的不仅仅是解题步骤,而是通过这道题,拆解在真实漏洞利用场景下,我们如何像侦探一样,从零开始分析一个陌生的二进制程序,如何理解其背后的内存模型,并最终构建出稳定的利用链。这个过程,远比单纯记住几个漏洞利用模板要有价值得多。

对于刚接触Pwn的同学来说,堆利用常常是道坎。它不像栈溢出那样有直观的返回地址覆盖,堆的世界更“动态”,充满了mallocfreechunkbin这些概念。newest_note这道题就是一个非常好的“教学案例”。它没有复杂的混淆,漏洞点清晰,但需要你扎实地理解glibc堆管理器的行为,并灵活运用调试技巧。接下来,我会假设我们手头只有这个名为newest_note的二进制文件(以及可能附带的libc.so),一步步还原分析、调试和最终利用的全过程。

2. 逆向工程:理解程序逻辑与漏洞定位

拿到二进制文件,第一步永远是尝试运行它,看看它提供了什么功能。用file命令查看,它通常是一个64位的ELF可执行文件。直接运行,或者用./newest_note,程序可能会呈现一个简单的菜单。

1. New note 2. Show note 3. Edit note 4. Delete note 5. Exit

这是一个典型的“记事本”类程序模型,也是CTF堆题的经典框架。我们的目标就是通过逆向分析,找到这些功能背后的内存操作是否存在漏洞。

2.1 使用IDA Pro/Ghidra进行静态分析

我们需要用反汇编工具(如IDA Pro、Ghidra或Binary Ninja)打开它。这里以IDA Pro的免费版为例。加载后,首先找到main函数,然后定位到显示菜单和处理用户输入的循环部分。

关键是要分析每个功能函数:

  • New note:通常会调用malloccalloc分配一块内存来存储“便签”内容。我们需要关注它分配的大小是否用户可控、分配后是否初始化、以及分配的指针存储在哪里(通常是一个全局数组或链表)。
  • Show note:打印指定便签的内容。这里可能泄露关键信息,比如堆地址或libc地址。
  • Edit note:编辑指定便签的内容。这里是漏洞的高发区,可能存在堆溢出、off-by-one等漏洞。
  • Delete note:释放(free)指定便签的内存。这里需要关注在释放后,是否将存储指针的变量置空(即是否处理了“悬空指针”)。如果没有,就可能导致UAF。

newest_note这道题中,通过逆向分析,我们很快能发现一个明显的漏洞模式:在Delete note(即free操作)之后,程序并没有将指向该内存的指针置为NULL。而Show noteEdit note函数在操作前,并没有检查该指针是否有效(即是否已被释放)。这就构成了一个教科书式的Use-After-Free条件。

注意:UAF漏洞的精髓在于“使用已释放的内存”。攻击者可以在内存被free后、但程序逻辑仍通过旧指针访问它之前,想方设法操控这块已被释放内存的内容,从而改变程序行为。

2.2 漏洞的具体形态

假设逆向后我们得到如下伪代码逻辑(高度简化):

struct note { int size; char *content; }; note *notes[16]; // 全局数组,存储最多16个便签的指针 void delete_note() { int idx; printf("Index: "); scanf("%d", &idx); if (idx < 0 || idx >= 16 || !notes[idx]) { puts("Invalid index!"); return; } free(notes[idx]->content); // 释放内容缓冲区 free(notes[idx]); // 释放note结构体本身 // 漏洞点:没有执行 notes[idx] = NULL; } void edit_note() { int idx; printf("Index: "); scanf("%d", &idx); if (idx < 0 || idx >= 16 || !notes[idx]) { // 检查指针非空,但无法判断是否已free puts("Invalid index!"); return; } printf("Content: "); read_input(notes[idx]->content, notes[idx]->size); // UAF发生在这里! }

delete_note函数,它释放了note结构体和其内部的content缓冲区,但没有清空notes[idx]这个全局指针。因此,在edit_note函数中,即使对应的note已经被释放,程序依然会通过这个“悬空指针”notes[idx]去访问内存,并调用read_inputnotes[idx]->content写入数据。此时,notes[idx]notes[idx]->content指向的内存区域都已经被归还给堆管理器,处于“空闲”状态。我们的写入操作,就是在向一块空闲的堆块(free chunk)里填数据,这为我们操控堆管理器的内部数据结构(如fdbk指针)创造了条件。

3. 堆风水:操控glibc堆管理器的内部状态

理解了漏洞在哪里,下一步就是思考如何利用它。UAF的常见利用目标是劫持程序控制流,比如覆盖函数指针、修改返回地址等。在堆利用中,一个强大的“跳板”是__free_hook__malloc_hook这类glibc的全局函数钩子。如果我们将__free_hook的值修改为system函数的地址,那么当下一次程序调用free()时,实际上就会执行system()命令。

但问题来了,我们如何修改__free_hook呢?它位于libc的数据区,我们需要一个任意的内存写原语(Arbitrary Write Primitive)。UAF配合堆风水(Heap Feng Shui)可以帮助我们实现这一点。

3.1 核心原理:tcache poisoning

在现代glibc(>= 2.26)中,引入了tcache(thread local cache)机制来提升堆分配效率。它对小内存块(默认64位下小于1032字节)的分配和释放有单独的缓存。一个被释放到tcache bin中的chunk,其用户数据区的开头8字节会被写入一个fd(forward pointer)指针,指向同一个tcache bin中的下一个chunk。

UAF漏洞允许我们在chunk被释放后,仍然能修改其用户数据区。如果我们释放一个chunk到tcache bin,然后通过UAF修改它的fd指针,让它指向一个我们想要控制的目标地址(比如__free_hook附近)。那么,当程序再次申请同样大小的内存时,堆管理器会顺着tcache链表分配,就有可能把我们伪造的“地址”当作一个“chunk”分配给我们。之后,我们向这个“chunk”写入数据,实际上就是在向__free_hook的位置写入数据!

这个过程叫做tcache poisoning,是当前堆利用中最主流、最稳定的技术之一。

3.2 针对newest_note的利用链构建

我们需要结合程序的具体逻辑来设计利用步骤。假设我们逆向得知:

  1. note结构体大小为0x20字节。
  2. content缓冲区的大小可在一定范围内自选(例如0x80字节)。
  3. 最多可以创建16个note。

那么,一个可能的利用链如下:

第一步:堆布局与地址泄露

  1. 创建若干个note,填满tcache bin,让后续释放的chunk进入unsorted binsmall bin。一个在unsorted bin中的chunk,其fdbk指针会指向libc中的main_arena结构,而main_arena的地址与libc基址有固定偏移。
  2. 利用Show note功能(如果它打印content),去读取一个处于unsorted bin中的chunk的内容,就能泄露出fd指针,从而计算出libc的基地址。进而得到__free_hooksystem的实际地址。

第二步:制造UAF并实施tcache poisoning

  1. 创建两个content大小为0x90的note(记为A和B)。0x90是考虑到chunk头对齐后的一个常用大小,它会进入tcache。
  2. 删除note A,再删除note B。此时,A和B的contentchunk都被释放,并链入0xa0大小(0x90用户大小+0x10 chunk头)的tcache bin。链表头是B,B的fd指向A。
  3. 此时,由于UAF漏洞,我们仍然可以通过edit_note功能去“编辑”已经被删除的note B(因为它的指针没被清空)。我们通过编辑,修改note B的content指针所指向的内存(即B的contentchunk的用户数据区)的前8个字节,也就是修改B chunk的fd指针。我们将其修改为__free_hook的地址。
  4. 现在,tcache链表变成了:链表头 -> B ->__free_hook地址(伪造的chunk)。

第三步:分配伪造chunk并写入目标地址

  1. 连续申请两个0x90大小的content缓冲区。第一次malloc会返回B chunk,第二次malloc就会返回我们伪造的、位于__free_hook处的“chunk”。
  2. 当我们向这第二个“chunk”写入数据时,就是在向__free_hook写入数据。我们将system函数的地址写进去。

第四步:触发shell

  1. 现在__free_hook已经被替换为system
  2. 创建一个新的note,其content内容设置为字符串/bin/sh\x00
  3. 删除这个note。程序会调用free(note->content),由于__free_hook被劫持,这实际上变成了system("/bin/sh"),从而获得一个shell。

4. 动态调试:使用GDB与pwndbg验证每一步

理论规划好了,但在实际利用中,内存布局可能因为各种原因(如分配顺序、对齐、线程)而偏离预期。动态调试是必不可少的。

4.1 调试环境搭建

推荐使用pwndbggef插件来增强GDB。它们提供了直观的堆命令,如heapbinsvis等,能让你清晰地看到堆块的状态和bins链表。

gdb ./newest_note pwndbg> r

在程序运行时,我们需要在关键操作(如malloc/free前后)下断点,观察内存变化。

4.2 关键断点与观察点

  1. mallocfree函数入口下断点b malloc,b free。但更有效的是在程序调用这些函数的地方下断点,这需要结合静态分析找到调用指令的地址。
  2. 观察tcache状态:在疑似完成tcache poisoning后,使用heap bins tcachetcachebins命令查看对应大小的tcache链表,确认fd指针是否被成功修改为我们伪造的地址。
  3. 验证写入:在向伪造的chunk写入system地址后,使用x/gx &__free_hook命令查看__free_hook处的值是否已被更改。
  4. 内存布局可视化:频繁使用vis_heap_chunksheap命令,图形化地查看堆内存的分配和释放情况,确保布局符合我们的预期。

提示:在实际比赛中,题目通常提供libc.so.6。在调试时,需要用patchelf修改二进制文件的解释器和库链接,或者使用pwntoolsgdb.debug功能,确保调试环境使用的libc与题目一致。地址偏移的计算必须基于题目提供的libc。

4.3 一个常见的坑:tcache double free检测

glibc对tcache有简单的double free检测:它不会将同一个chunk连续两次插入同一个tcache bin的链表头部。但在newest_note这类题目中,我们的利用链通常不依赖于对同一个chunk的double free,而是利用UAF修改已被释放chunk的fd指针,所以这个检测一般不影响我们。但如果你的利用链涉及double free,可能需要通过先放入其他bin等方式来绕过。

5. 编写自动化利用脚本:使用pwntools

手动操作菜单来利用漏洞是繁琐且容易出错的。CTF选手通常使用Python的pwntools库来编写自动化攻击脚本(Exploit)。这不仅能提高效率,也便于调整和测试。

下面是一个极简化的脚本框架,展示了如何与newest_note程序交互并实施上述利用链:

from pwn import * context.arch = 'amd64' context.log_level = 'debug' # 启动进程 p = process('./newest_note') # 如果题目远程,用 remote('host', port) # p = remote('node4.buuoj.cn', 12345) # 加载题目提供的libc,用于计算偏移 libc = ELF('./libc.so.6') def new_note(size, content): p.sendlineafter(b'>> ', b'1') p.sendlineafter(b'Size: ', str(size).encode()) p.sendafter(b'Content: ', content) def show_note(idx): p.sendlineafter(b'>> ', b'2') p.sendlineafter(b'Index: ', str(idx).encode()) # 这里需要根据实际输出格式来解析返回的数据 def edit_note(idx, content): p.sendlineafter(b'>> ', b'3') p.sendlineafter(b'Index: ', str(idx).encode()) p.sendafter(b'Content: ', content) def delete_note(idx): p.sendlineafter(b'>> ', b'4') p.sendlineafter(b'Index: ', str(idx).encode()) # 1. 堆布局,泄露libc地址 # ... 创建、释放一系列note,触发unsorted bin ... # show_note(x) 泄露地址 # libc_base = leak_addr - libc.sym['main_arena'] - 0x60 # 具体偏移需根据libc版本确定 # free_hook = libc_base + libc.sym['__free_hook'] # system_addr = libc_base + libc.sym['system'] # 2. Tcache Poisoning # new_note(0x88, b'A'*0x88) # note 0 # new_note(0x88, b'B'*0x88) # note 1 # delete_note(1) # delete_note(0) # 此时0的content chunk在tcache链首 # 通过UAF编辑note 1(其指针未清空),修改其content chunk的fd # edit_note(1, p64(free_hook)) # new_note(0x88, b'fill') # 取回原note 1的chunk # new_note(0x88, p64(system_addr)) # 取回伪造的free_hook chunk,并写入system地址 # 3. 触发 # new_note(0x88, b'/bin/sh\x00') # note 2 # delete_note(2) # 触发 system("/bin/sh") p.interactive()

这个脚本只是一个骨架,实际编写时需要根据逆向出的具体菜单提示符、输入格式以及泄露出的地址精确计算偏移来填充细节。pwntoolssendlineafterrecvuntil等方法能很好地处理这种交互。

6. 举一反三:UAF漏洞的变种与防御

通过newest_note这道题,我们完整地实践了从逆向、漏洞分析、利用链设计、调试到脚本编写的全过程。UAF作为一种内存安全漏洞,其变种和组合形式非常多。

  • 结合类型混淆:如果释放的是一个包含虚函数表指针的C++对象,UAF后修改其内存,可能导致程序调用被篡改的虚函数,实现代码执行。
  • 结合堆溢出:UAF提供了读写已释放内存的能力,如果再有一个堆溢出可以修改相邻堆块的关键数据,利用方式会更加灵活。
  • 在浏览器引擎中的利用:UAF是浏览器漏洞的常客,攻击者通过精心构造的JavaScript代码触发DOM对象的释放与重用,最终实现沙箱逃逸。

从防御角度,开发者应该:

  1. 及时清空指针:在free之后,立即将指针变量置为NULL。这是最简单有效的习惯。
  2. 使用智能指针:在C++中,使用std::unique_ptrstd::shared_ptr等RAII机制管理资源,可以很大程度上避免手动管理内存带来的问题。
  3. 启用安全机制:现代编译器和操作系统提供了很多缓解措施,如ASLR(地址空间布局随机化)、DEP(数据执行保护)、Stack Canaries、以及glibc自身的保护(如tcache double free检测、safe linking等)。虽然攻击者仍在不断寻找绕过方法,但这些机制极大地提高了利用门槛。

对于安全研究者或CTF选手而言,理解这些漏洞的本质和利用技巧,不仅是为了攻击,更是为了深刻理解计算机系统底层的工作原理,从而能设计出更安全的软件。每一次成功的利用,都像完成了一次精密的逻辑拼图,这种成就感正是二进制安全研究的魅力所在。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/28 18:26:13

Codex不是聊天机器人:从安装到实战,拆解108倍效率背后的真相

最近一个朋友给我看了一组数据&#xff1a;a16z 的调研里提到&#xff0c;律师在使用 Codex 之后&#xff0c;某些任务的处理速度提升了大约 108 倍。他看完第一反应是“要不我也试试”。我提醒他&#xff0c;先把这“108 倍”放一放&#xff0c;因为这类数据在传播过程中&…

作者头像 李华
网站建设 2026/8/28 18:25:32

C语言游戏编程从入门到精通?别扯了,先玩着学才爽!这破书真能让你变大神?

这一篇是着重介绍了c语言基础入门, 也就是从零开始教你运用C语言去编写游戏这一内容, 借助具体的内容来进行展示, 期望能够对c语言开发的学习持有一定的帮助作用。作为游戏玩家的咱们, 有没有想过设计一个归属于自身的游戏? 喜欢玩乃是人的本性, 而C语言是咱们计算机专业都得去…

作者头像 李华
网站建设 2026/8/28 18:19:45

YOLO11课堂行为检测实战指南:轻量变体、真实数据与教师级GUI

简介&#xff1a;YOLO&#xff08;You Only Look Once&#xff09;作为主流单阶段目标检测框架&#xff0c;其核心价值在于实时性与精度的工程平衡。YOLOv8作为当前广泛落地的稳定版本&#xff0c;常被用于教育、安防等边缘场景&#xff1b;而YOLO11并非官方发布的新代际&#…

作者头像 李华
网站建设 2026/8/28 18:18:39

做骨分化的经典小鼠前成骨细胞 MC3T3-E1,怎么养出好状态

搞骨组织工程、骨质疏松或者成骨分化的同学&#xff0c;MC3T3-E1 基本是绕不开的一株。它是小鼠颅顶来源的前成骨细胞&#xff0c;最大的价值是能在体外分化成骨、形成钙化组织&#xff0c;是研究骨形成的标准模型。MC3T3-E1 是小鼠颅顶前骨细胞&#xff0c;从 C57BL/6 小鼠颅骨…

作者头像 李华
网站建设 2026/8/28 18:18:23

MultiGlobeQA多语言地理空间推理评测:从流程搭建到短板定位

如果你做过多语言问答评测&#xff0c;应该能感受到一个问题&#xff1a;很多 benchmark 换了几种语言&#xff0c;思路还是英语中心&#xff0c;题目也偏向欧美地名和地理结构。MultiGlobeQA 这个基准不太一样&#xff0c;它的定位是 multilingual and globally diverse&#…

作者头像 李华