[ 2021长城杯 ]K1ng_in_h3Ap_I
整个程序分析:
main
1 | void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
add
1 | _DWORD *add() |
delete
1 | void delete() |
edit
1 | __int64 edit() |
printf_w
1 | int printf_w() |
EXP 思路:
封装函数:
1 | def add(idx, size): |
彩蛋泄露 printf 地址
1 | egg() |
程序中存在一个隐藏后门或已知功能 printf_w,它直接泄漏了 printf 函数在内存中的真实地址。
- 计算 stdout 地址: 在同一个版本的 libc 中,任意两个符号之间的偏移量是固定的。攻击者通过本地调试得出 printf 和 IO_2_1_stdout 之间的偏移为 0x36fe10,从而精准算出 stdout 结构体在内存中的起始位置。这是为了后续篡改输出流做准备。
- ###存疑###
布置堆风水&overlap
1 | add(0, 0x78) |
1 | add(1, 0x68) |
dele(2) # index 2 进入 fastbin
dele(1) # index 1 进入 fastbin,此时 head -> 1 -> 2
1 | add(3, 0x78) # 从UnSortedbin里拿 |
1 | pwndbg> heap |
1 | edit(1, p8(0x00)) |
edit(1, p8(0x00)) 将 chunk 1 的 fd 指针的最低位字节改成了 \x00
进行局部字节覆盖。它将 chunk 1 -> fd 本来指向 chunk 2 的地址,强行扭转到了堆内存中 0x0000559ca3207100,放到 UnSortedbin 当中去:
1 | pwndbg> x/20gx 0x5622474c4100 |
###存疑###
局部覆盖绕过 ASLR:edit(0, …) 利用了 Chunk 0 的 堆溢出漏洞,覆盖了下一个释放态 chunkFastbin的头部。
- p64(0x71) 伪造了 chunk 的 size。
- p16(…) 修改了 Fastbin chunk 的 fd 指针。这里只覆盖了低 2 字节(16位)。因为 ASLR 只会随机化内存的高位,低 12 位是固定的。 Fastbin 在分配时会严格检查目标地址的 size 字段是否合法(目标 size 必须是 0x7x,在 stdout - 0x43(即代码中的 - 3 - 0x40)的位置,内存中往往包含类似 0x7f 的数据。利用这个错位地址,可以完美检查 。由于低 16 位中有 4 位是随机的,这里存在 1/16 的爆破概率。
攻击 IO_FILE 结构体并泄露 Libc 基址
1 | add(0, 0x68) |
- b’a’ * (3 + 6 * 8) 51 字节填充: Fake Chunk 从 stdout - 0x43 开始,减去 0x10 的 chunk header,用户数据从 stdout - 0x33 ( 0x33 = 51 ) 开始。这 51 字节刚好填充到 stdout 的起始字段 _flags。
- **p64(0xfbad1800) 覆写 _flags:**0xfbad0000 是 glibc 识别 FILE 的 Magic Number;0x1800 设置了
_IO_IS_APPENDING和_IO_CURRENTLY_PUTTING标志位。这能让 glibc 跳过常规的缓冲检查流程,避免程序崩溃,直接执行底层写操作。 - p64(0) * 3 覆写 read 指针: 将
_IO_read_ptr、_IO_read_end、_IO_read_base清零,防止干扰后续写流程。 b'\x00'** 覆写_IO_write_base**** 最低字节:** 这是泄露数据的触发开关。通过将_IO_write_base最低字节改小(设为 0),导致_IO_write_base<_IO_write_ptr。- 当程序继续运行并执行任意输出操作时,底层会调用
sys_write,强行将_IO_write_base到_IO_write_ptr之间的所有内存打印出来。这其中包含了stdout内部的 libc 指针。接收数据后减去固定偏移 0x3c5600,就得到了精确的 libc 基地址。
Fastbin Double Free 劫持 __malloc_hook
1 | add(0, 0x68) |
- Double Free: 连续释放
0 -> 1 -> 0构造了经典的 Fastbin 环形链表。 - 伪造 fd: 编辑处于 free 状态的 chunk0,将其
fd指针指向__malloc_hook - 0x23。在 glibc 2.23 中,__malloc_hook 前方刚好有一个可以被当作 0x7f size 的错位地址,完美满足 Fastbin 的分配检查。
利用realloc 调整栈帧触发 one_gadget
1 | add(0, 0x68) |
- 前两次
add会把伪造的__malloc_hook区域申请出来。 - 为什么不直接将
__malloc_hook覆盖为one_gadget**?**one_gadget 的成功执行极其依赖当前的寄存器状态或栈环境(例如要求 rsp+0x30 == NULL)。直接从 malloc 调用过去往往不满足条件,会导致段错误 - Realloc 调栈技术: 在 libc 内存中,
__malloc_hook和__realloc_hook是紧挨着的。 - 攻击者将
__realloc_hook覆盖为one_gadget。 - 将
__malloc_hook覆盖为realloc + 8(即realloc汇编指令向下偏移 8 字节的位置)。 - 精妙的执行流: 1. 当下次调用
malloc时,程序实际上跳转到了realloc + 8。 2.realloc函数开头有许多push寄存器的汇编指令。从+8开始执行,意味着**跳过了前面的几个 **push。这会导致栈指针(RSP)发生偏移,从而改变了当前的栈布局。 3. 栈布局被调整后,正好满足了one_gadget的触发条件。 4. 随后realloc内部正常执行,去调用它自己的钩子__realloc_hook,此时钩子已经是one_gadget,最终成功且稳定地获取 Shell。
触发 Payload,获取 Shell
1 | add(0, 0x18) |
随意调用一次 malloc(通过 add),触发已经被劫持的 __malloc_hook,走完上述的执行流,最终进入交互模式。
总 EXP:
2.23-0ubuntu11.3_amd64
1 | #┌──(pwn_env)─(root㉿kali)-[/home/kali/Desktop] |
更新: 2026-04-27 18:38:12
原文: https://www.yuque.com/idcm/wnemg9/tqphv65tgu53gb8g