pwn.kr-Echo2:FSB+UAF链式劫持
pwn.kr-Echo2:FSB+UAF链式劫持
原理
- UAF 漏洞原理:悬空指针的错误使用。 程序在释放堆内存后继续访问旧指针,使攻击者能够利用堆重用机制覆盖关键数据结构。
- tcache 重用机制:已释放 chunk 的优先返回。 小尺寸 chunk 在 tcache 中以 LIFO 管理,被释放后会在下一次 malloc 时被重新分配,从而为 UAF 提供可控写入机会。
- FSB 泄露原理:格式化字符串任意读取。 程序使用用户输入作为格式串,使攻击者能够使用 %p 等格式符泄露栈地址,从而绕过 ASLR。
- 控制流劫持原理:覆盖函数指针跳转到 shellcode。 攻击者通过 UAF 改写堆中的函数指针,将其替换为 shellcode 地址,使程序在执行回调时跳转到恶意代码。
详解
一、挑战目标
我们选择的是Level 2的Echo2题目。Echo2是一个远程菜单驱动程序,我们的目标是通过漏洞利用获取远程Shell并读取flag。
二、总体分析
首先分析一下程序:在main函数中中首先为指针o分配40字节的内存块,之后询问用户名,赋给24字节字符串v6,再将v6赋给o的前24字节,o的后16字节分别是greetings函数和byebye函数。 接着进入一个菜单循环:
- 1选项没有功能
- 2选项调用echo2,接收用户输入再打印
- 3分配内存块接收输入再打印
- 4选项退出,退出时释放o指向的内存块。
程序中可利用的漏洞:程序提供BOF、FSB、UAF、Exit四项功能,其中echo2()函数存在格式化字符串漏洞可以泄露内存信息,cleanup()函数中将o指向的内存块释放掉之后没有清理指针,使得指针成为悬空指针。
而echo3()从堆中申请内存给指针s,同时又使用o指向的内存块中的函数指针调用函数,因此存在UAF漏洞,可以劫持程序流运行其他地址代码。
三、探究原理:UAF与悬挂指针
本次实验的核心漏洞类型为 Use-After-Free(UAF)。该漏洞产生于程序在释放堆内存后仍继续使用对应指针,导致指针悬挂(野指针)。一旦攻击者能够重新掌控这块被回收的内存,即可利用堆的重用机制将原本的合法数据替换为恶意内容,从而劫持程序流程。本节将从 堆管理基础、悬空指针的形成机制以及利用方式 三个方面展开分析。 1、堆管理基础:chunk与tcache chunk结构:用户申请的内存块称为chunk,由用户数据区和包含size、prev_size及标志位的头部组成,释放后按大小进入不同bins。 tcache机制:小于0x400字节的chunk优先放入tcache,tcache采用LIFO单链表,最近释放的chunk位于链表头,下次malloc时最先被返还。
2、悬空指针的产生:
在程序流程中选择退出时会调用cleanup()函数,会释放指针o所指向的内存块,这个chunk会被插入到tcache的队首,下次申请内存时优先使用,而o仍然指向这个被释放的chunk。
3、悬空指针的利用:
o指向一个40字节的chunk,其中前24个字节存储用户输入的用户名,其后的8个字节存储greetings()函数的地址,最后八个字节存储byebye()函数的地址。在echo3()函数中通过o指向chunk中的函数指针调用这两个函数,同时还申请了内存块对其写入(s与o指向同一个chunk)。因此,可将shellcode放置在用户名的位置(v6中),在chunk最后放置shellcode(用户名)的地址。由此就使程序转而运行shellcode。

四、漏洞利用与执行攻击
我们的攻击步骤可总结如下:
- 让程序执行我们输入的 shellcode;
- 需要先把 shellcode 写进一个可控区域(用户名 v6);
- 需要泄露该区域在内存中的真实地址(FSB);
- 需要覆盖函数指针,使程序跳到 shellcode(UAF)。 ①输入Shellcode 题目中用户名的输入限制了24字节,这就对我们的shellcode长度增加了现在。Exploit-DB是一个公开的漏洞利用数据库,我们可以找到我们可以利用的shellcode,其中最短shellcode,长度仅23字节,刚好满足我们的需求。
②利用FSB获取Shellcode地址
Echo2()函数中的printf(format)允许使用%p格式符泄露栈,可直接读出栈上保存的name指针。通过gdb调试可以看到main()函数的栈基址存储在高于格式化字符串32字节的位置,同时通过多个%p打印栈中内容,得到格式化字符串存储在printf()第六个可变参数的位置,即main()栈基址位于printf()第10个可变参数的位置。因此在echo2()函数中输入%10$p即可泄露main()函数的rbp。
而通过IDA进行栈分析可以得到,v6在rbp-0x20的位置,由此可得到v6的地址。
③将目标地址填充到o内存块 在程序的 menu
中选择“退出”选项时,cleanup() 会释放指针 o 所指向的 40
字节堆块,但程序并未将 o 置为 NULL,从而形成典型的 UAF 场景。由于该
chunk 的大小相同,当我们在 echo3() 中再次执行 malloc(40) 时,tcache
会优先返回这块刚刚被释放的 chunk,使得新指针 s 与原指针 o
实际上指向同一段内存区域。
接下来 echo3() 会将用户输入写入 s 指向的内存,而程序随后又通过 o 中的函数指针字段执行 greetings() 或 byebye()。因此,这一步就允许我们在这块被重复利用的堆内存中布置恶意数据:前半部分保持为 shellcode 或填充数据,最后 8 字节则被我们覆盖为 shellcode 的实际地址,从而替换原本的函数指针。当 echo3() 调用函数指针时,程序的执行流便会跳转至我们布置的位置,实现控制流劫持。
⑤构造payload执行攻击
完成地址泄露、函数指针覆盖与 shellcode
布置等步骤后,我们将整个攻击流程整合进一个自动化的 exploit
脚本,并将其存储在
/tmp/exp.py。脚本在运行时会自动完成用户输入、格式化字符串地址泄露、地址计算、UAF
触发、覆盖函数指针等全部操作,从而在程序调用 echo3()
时使控制流成功跳转到我们的 shellcode。
运行该脚本,可以看到我们成功获得了远程
Shell,能够正常执行系统命令。此时输入 cat flag 即可读取到题目要求的 flag
内容,标志着整条利用链完全奏效,攻击顺利完成。

五、防御工作总结
在本次实验中,漏洞利用链依赖 格式化字符串漏洞(FSB) 与 Use-After-Free(UAF) 两类常见内存安全问题。为了在真实系统中避免类似安全风险,需要从代码层面、编译器层面与运行时机制等角度进行综合防御。以下总结常见且有效的防御策略。 1、 UAF 防御策略: 针对 UAF,关键是避免程序在释放内存后继续使用原指针。通常可通过以下方式实现: ①释放后将指针置 NULL:在执行 free(ptr) 之后立即将指针设为 NULL,防止其变成悬空指针,从而避免对已释放内存的后续读写。 ②使用更安全的内存管理机制:在开发中可通过 C++ 智能指针或使用具备自动内存管理和所有权模型的语言(如 Rust、Go)来减少悬空引用与重复释放的风险。此外,在测试阶段启用 ASan 或 Valgrind 等内存检测工具,可以在漏洞进入生产环境前及时暴露潜在的 UAF 行为,对提高程序内存安全性非常有效。
2、FSB 防御策略 针对格式化字符串漏洞,核心原则是不要让用户输入直接成为格式化字符串。程序必须固定格式模板,例如始终使用 printf(“%s”, input) 的方式输出用户输入,从根源上阻断 %p、%n 等危险格式符的利用。同时,应启用编译器提供的格式化字符串检查选项(如 -Wformat 和 -Wformat-security),以及开启 glibc 的 FORTIFY_SOURCE 强化机制,对 printf 系列函数进行额外的参数与边界校验。这些手段可以有效发现或阻止潜在的格式化字符串滥用问题。