好友
阅读权限30
听众
最后登录1970-1-1
|
沙发
囚徒灬
发表于 2026-5-27 13:46
本帖最后由 囚徒灬 于 2026-5-27 17:02 编辑
这是一道 UAF + 双重释放(double free) 的堆题。
程序结构
菜单式 Note Manager,笔记指针数组在 .bss 的 0x4040C0,共 20 个槽。每个笔记是一个 malloc(0x10) 的元数据结构:
struct note { void *content_ptr; // +0
size_t size; // +8 };
┌───────────────────┬────────────────────────────────────────────────────────────┐
│ 功能 │ 行为 │
├───────────────────┼────────────────────────────────────────────────────────────┤
│ Create (0x401334) │ 槽必须为空;size 限 1..0x80;分配元数据+内容,读入数据 │
├───────────────────┼────────────────────────────────────────────────────────────┤
│ Edit (0x4015AF) │ 按 note->size 读入到 note->content_ptr(任意写的雏形) │
├───────────────────┼────────────────────────────────────────────────────────────┤
│ Delete (0x4016C2) │ free(content_ptr),然后只把 size=0 │
├───────────────────┼────────────────────────────────────────────────────────────┤
│ Show (0x4017C0) │ 按 note->size 把 content_ptr 内容 write 出来(任意读的雏形) │
└───────────────────┴────────────────────────────────────────────────────────────┘
核心漏洞(delete 0x4016C2)
释放时三件事都没做:
1. 没把 content_ptr 置 NULL → 悬空指针
2. 没释放元数据结构、没清空数组槽 → 同一 index 可被反复 delete → double free
3. 只把 size 清零(让 edit/show 对已删笔记暂时失效,但双重释放仍可用)
保护机制
- No PIE(固定地址 0x40xxxx)
- Partial RELRO:.got.plt(0x404000–0x404068)可写 → GOT 可改写
- 有 Canary(__stack_chk_fail),但栈不是攻击面
- GLIBC 2.34+(tcache 带 safe-linking + key 检测)
- free@Got = 0x404018
利用思路
利用「元数据 0x20、内容尺寸可调」让 double-free 的 chunk 同时是某个活跃笔记的内容(可 edit),实现 tcache 投毒:
1. 构造重叠:create 一个内容在 0x50 bin 的笔记(如 size=0x48)→ delete → 再 create 同尺寸,使新笔记的内容复用刚释放的
chunk B(元数据 0x20 走另一个 bin,先被取走);此时旧笔记悬空指针仍指向 B。
2. double free:再次 delete 旧 index,把仍在使用中的 B 放回 tcache[0x50]。现在 B 既在 freelist
又是活跃笔记的可编辑内容。
3. 泄露堆地址:show 该活跃笔记,读出 B 的 fd = B>>12(safe-linking 还原堆基址)。
4. 投毒:edit 该笔记,把 fd 改成 (B>>12) ^ target,两次 malloc 后拿到 target 处的 chunk。
5. 伪造元数据 → 任意读:把 chunk 指到 notes 数组/伪造 metadata,令 content_ptr=puts@got, size=8,show 泄露 libc → 求
system。
6. 任意写 → getshell:edit 把 free@got(0x404018) 改成 system;再建一个内容为 "/bin/sh\0" 的笔记,delete 它 →
free("/bin/sh") 即 system("/bin/sh")。
opus 跑了一下
运行说明
- 这是 Linux ELF + pwntools,需在 Linux 上跑(本机是 Windows,我没法直接执行验证)。
- 本地:python3 exp.py(用本机 libc,脚本走 exe.libc 自动解析偏移)。
- 远程:目录里没附带 libc,需把题目给的 libc 放成 ./libc.so.6,再 python3 exp.py REMOTE HOST=ip PORT=port。如果远程 libc
版本不同,system/偏移会错,需用泄露的 puts 去 libc-database 匹配版本。
exp.rar.txt
(2.06 KB, 下载次数: 1)
heap - Simple Note Manager 分析报告
目标文件:E:\ai\cm\pwn\heap(x86-64 ELF,动态链接)
利用脚本:exp.py
日期:2026-05-27
一、结论
漏洞类型:Use-After-Free + Double Free(堆)。
delete 函数在释放笔记内容后:
- 不清空
content_ptr → 悬空指针;
- 不释放 metadata 结构、不清空 数组槽 → 同一 index 可被重复
delete → double free;
- 仅把
size = 0(让已删笔记的 edit/show 暂时失效,但 double free 仍可用)。
可由此构造 tcache 投毒,改写 .bss 中的笔记指针数组,获得任意读写,最终改写 free@got 为 system,以 free("/bin/sh") 拿到 shell。
二、程序结构(证据)
笔记指针数组位于 .bss 的 0x4040C0,共 20 槽(0x4040C0 + 8*i,i<0x14)。
每条笔记是 malloc(0x10) 的 metadata:
struct note { void *content_ptr; // +0
size_t size; // +8 };
| 功能 |
地址 |
行为 |
| Create |
0x401334 |
槽必须为空且 idx<0x14;1<=size<=0x80;分配 metadata(0x10)+内容(size),read 读入 |
| Edit |
0x4015AF |
按 note->size 读入到 note->content_ptr(任意写雏形) |
| Delete |
0x4016C2 |
free(content_ptr),然后 只 size=0 |
| Show |
0x4017C0 |
按 note->size write 出 note->content_ptr(任意读雏形) |
| Menu |
0x4012BB |
打印菜单,末尾 printf("> ") |
| Init |
0x401256 |
setvbuf 关缓冲 |
Delete 关键反编译(漏洞点)
// sub_4016C2
if ( (unsigned int)v1 < 0x14 && *((_QWORD *)&unk_4040C0 + v1) ) {
free(**((void ***)&unk_4040C0 + v1)); // free(content_ptr)
*(_QWORD *)(*((_QWORD *)&unk_4040C0 + v1) + 8LL) = 0; // size = 0
// 缺失: content_ptr 未清零、metadata 未释放、数组槽未清空
}
三、保护机制(证据)
来自 IDA 段信息与 GOT 检查:
- No PIE:固定基址
0x400000,代码/数据地址 0x40xxxx。
- Partial RELRO:
.got.plt(0x404000–0x404068)段权限 RW → GOT 可写。
- Canary:存在
__stack_chk_fail,但栈不是攻击面。
- NX:数据段不可执行(常规)。
- GLIBC 2.34+:
.rodata 含 GLIBC_2.34 版本符号 → tcache 启用 safe-linking + double-free key 检测,且无 __free_hook/__malloc_hook。
关键地址:
| 符号 |
地址 |
| notes 数组 |
0x4040C0 |
free@got |
0x404018 |
puts@got |
0x404020 |
(已通过 .free PLT stub bnd jmp cs:off_404018 验证 free@got=0x404018。)
四、利用链
整体:UAF/double free → tcache 投毒 → 改写 notes 数组 → 任意读写 → 改 free@got=system → getshell。
- 构造可控悬空 chunk:用
size=0x48(内容落 0x50 bin,独立于 metadata 的 0x20 bin)。让 double-free 的 chunk CA 同时是某活跃笔记的内容(可 edit/show)。
- 堆地址泄露:CA 单独入空 tcache 时
fd = CA>>12(safe-linking key)。show 读出 → 得 heap_key。
- count=2 投毒:把
tcache[0x50] 凑成 [CA, C4](CA 在表头,count=2),edit 写 CA.fd = heap_key ^ 0x4040C0。
> 注:tcache 取块需 counts>0,故必须先把计数凑到 2,否则第二次分配不会走 tcache。
- 改写 notes 数组:两次分配,第二次返回
0x4040C0,把整段写成伪造 slot + metadata:
slot0 -> 0x4040E0 = {content_ptr=puts@got, size=8}(任意读)
slot1 -> 0x4040F0 = {content_ptr=free@got, size=8}(任意写)
- leak libc:
show(0) 打印 puts@got 处 8 字节 = libc puts 地址 → 求 libc 基址与 system。
- 改 GOT:
edit(1) 写 8 字节到 free@got → system。
- getshell:
create 内容 "/bin/sh\0",delete 它 → free(ptr) 实为 system("/bin/sh")。
详见 exp.py,各步骤已对应注释。
五、待确认 / 研究方向
- 远程 libc 版本:目录内未附带 libc。脚本中 tcache 桶分流与
system 偏移依赖具体 glibc。
- 远程若给 libc → 放为
./libc.so.6,python3 exp.py REMOTE HOST=.. PORT=..。
- 未给 → 先只跑到第 5 步泄露
puts,用 libc-database 按低 3 字节匹配版本。
- glibc 版本差异:
- 2.34/2.35 行为如脚本所设;若为更高版本(2.39+),tcache key/计数检查、
_int_free 校验略有差异,double-free 序列可能需微调。
- 若
global_max_fast/tcache bins 被改,0x50 桶假设需复核。
- 备选利用路径(若 GOT 改写在目标环境受限):
- FSOP:伪造
_IO_FILE(stdout/stderr)+ _IO_2_1_stdout_ vtable(2.34+ 用 _IO_wfile_jumps / house of apple 系列)。
- 任意写打
__exit_funcs / tls_dtor_list,或 _dl_fini 相关结构。
- 由于 No PIE,也可考虑直接劫持其他频繁调用的 GOT(
printf/puts)配合可控参数。
- 稳定性:脚本对
read 不追加换行、内容按 size 对齐填充已处理;实测时关注菜单回显是否完全匹配 > / Index: / Size: / Content: / New content:。
附:复现命令
# 本地(需 Linux + pwntools)
python3 exp.py
# 远程(放入题目 libc)
python3 exp.py REMOTE HOST=<ip> PORT=<port>
|
|