[ISCC 2024] chaos_plus(堆栈结合)
网上有师傅对 [ISCC 2024] chaos 进行了修改,抬高了 libc 版本至 2.35 并修改了部分函数,思路可以供自己去学习高版本 libc 的堆攻击,也认识了 Safe-Linking 基址机制。总而言之,是一道非常好的题目。
参考资料:https://blog.csdn.net/j284886202/article/details/139119047
chaos_plus(UAF libc-2.35)
程序分析:
main
1 | __int64 __fastcall main(const char *p_%d, char **num, char **a3) |
menu
1 | int __fastcall menu(__int64 p_%d, __int64 p_n4) |
add
1 | __int64 add() |
delete
1 | __int64 del() |
edit
1 | __int64 edit() |
printf
1 | __int64 printf() |
EXP 思路:
这是一个非常经典的针对 glibc 2.35 环境的堆溢出/UAF(Use-After-Free)漏洞利用脚本。
在 glibc 2.35 中,漏洞利用主要面临两个巨大的挑战:
- 移除了所有的 Hook 函数:去掉了
__malloc_hook、__free_hook等,这意味着我们无法再通过简单地劫持堆相关的函数指针来拿 shell,必须转而寻找其他目标(如栈上的返回地址、_IO_FILE结构等)。 - 引入了 Safe-Linking(安全链接):Tcache 和 Fastbin 的
fd指针被异或加密了, - 公式为 mangled_ptr = ( L ≫ 12 ) ^ P (L 为当前 chunk 的地址,P 为原本指向的地址)。
这份脚本的总体思路非常清晰:泄露 libc 基址 → 泄露 Heap 基址(绕过 Safe-Linking) → 劫持 tcache 读出 environ 从而泄露栈地址 → 劫持 tcache 修改栈上的返回地址(ROP)。
1. 泄露 Libc 基址 (Unsorted Bin 泄露)
1 | add(0x410, b'A') # 申请一个超出 tcache 范围的 chunk (idx 0) |
- 我们可以通过 unsortedbin 连的 main_arena 寻找 environ 的地址:
查找 main_arena + 96 和 environ 的偏移
1 | distance 0x7fd636c1ace0 &environ |
- 释放大小超过 0x408 的 chunk 时,它会被放入 Unsorted Bin。此时它的 fd 和 bk 指针会指向 libc 内部的 main_arena。重新申请回来并通过打印,可以带出这个 libc 地址.
还有一种方法是直接去计算偏移:
1 | readelf -s libc.so.6 | grep environ |
2. 泄露堆基址 (绕过 Safe-Linking)
1 | delete(2) |
- 由于脚本中利用了 UAF 漏洞,它打印了刚刚被释放到 tcache 中的 chunk 2 的 fd 指针。
- 根据 Safe-Linking 机制,链表末端的 chunk 其原始 fd 应该是 0。所以被加密后的指针值为
( HeapBase ≫ 12 ) ⊕ 0 = HeapBase ≫ 12。
- 脚本拿到这个值后,直接乘以 0x1000(等价于左移 12 位),就完美还原出了当前堆块所在的页面基址。有了这个基址,我们就可以随时伪造 Safe-Linking 的加密指针了。
3. 利用堆溢出/UAF 构造 Chunk Overlap
1 | edit(1, p64(0) * 3 + p64(0x41)) |
- 利用了 edit 函数存在的堆溢出漏洞,修改了紧随其后的 chunk 2 的
size字段,将其从 0x21 改成了 0x41。然后释放再申请回来,这使得原本属于 chunk 3 的内存区域现在被包含在新的 chunk 2 内,形成了 Chunk Overlap,为后续的 Tcache Poisoning 打下基础。
4. 泄露栈地址 (利用 environ)
1 | delete(4) |
- 因为 glibc 2.35 删除了 __free_hook,我们无法直接劫持 free,最简单的办法就是修改栈上的函数返回地址。而要知道栈的地址,必须去读取 libc 中 environ 变量存储的栈指针。
- Tcache Poisoning:上面的
add写入了伪造的fd:p64((heap >> 12) ^ (environ - 0x10))。注意这里严格遵循了 Safe-Linking 的加密规则,将目标地址environ - 0x10进行了异或加密。 - 这里把 fd 申请到了 environ - 0x10,是因为 因为当你通过 malloc 拿到一块内存时,指针是跳过 Chunk 头部的 16 字节的。我们将伪造的 Chunk 头部定在 environ - 0x10,这样后续 malloc 返回的用户区指针,就精准无误地落在了
environ变量上。 - 随后两次申请,将 tcache 链表引导到了
environ变量所在的内存附近,通过show(4)成功读取了其中的栈指针。
5. 计算栈指针
1 | stack1 = u64(io.recvuntil('\x0a', drop = True)[-6:].ljust(8, b'\0')) |
泄露出的 stack1 就是通过 environ 返回到栈上的值。
当 main 函数结束后,就会弹出返回地址 0x7ffd3bb7a148 —▸ 0x7fea09629d90 ◂— mov edi, eax,此时程序就会跳回到 libc 当中的 __libc_start_call_main 函数。在 libc 接管之后,就会执行 mov edi,eax,把刚才 main 函数返回的 0 转移到 edi 当中,随后作为 exit(0)的参数。
6. 在栈上布置 ROP 链
1 | # 第一次 Tcache 投毒,目标地址是:栈的返回地址 stack - 8 |
- 原理:两次重复了 Tcache 投毒的操作,将堆块直接申请到了栈上(
stack - 8和stack + 8)。 - 在栈上写入了一段经典的 ROP 链:pop rdi; ret→&”/bin/sh”→ret (用于栈对齐) →system()。
7. 触发 ROP
1 | io.sendline('5') |
- 发送
5应该是触发菜单中的Exit(退出)功能。 - 此时程序执行
return,但原本的返回地址已经被上述的 ROP 链覆盖。程序不会正常返回,而是会去执行system("/bin/sh"),从而成功 get shell。
总结:
这是一个非常标准且熟练的高版本 glibc 堆利用模板。作者对 Safe-Linking 的加解密、environ 的妙用以及通过 Tcache 劫持栈执行流的手法运用得非常纯熟。
如果您正在学习高版本堆利用,你希望我再详细解释一下 Safe-Linking 的底层机制,或者探讨一下除了修改返回地址外,glibc 2.35 还有哪些常见的攻击面(比如 _IO_FILE 的劫持)吗?
总 EXP:
1 | from pwn import * |
C 源代码:
1 | // 题目源代码 |
更新: 2026-04-27 20:59:52
原文: https://www.yuque.com/idcm/wnemg9/trs5s1cskesxttbk