JNCTF 9th
poison
这里主要是泄露 heap 和 libc 的手法,off by null 的手法,打 tcache poison 的手法。
菜单题目, glibc 2.31 ,功能如下:
1 | 1. Add a game |
源码:
修了一个结构体,先放在这里,为什么要修结构体,因为避免阅读指针乱飞的代码
1 | struct game |
1.添加游戏:
在 add_game() 中有 off-by-null 漏洞存在。
1 | unsigned __int64 add_game() |
2.编辑游戏:
这里有点神秘?
1 | unsigned __int64 edit_game() |
3.删除游戏:
这里将指针置零了,不存在 UAF 漏洞。
1 | unsigned __int64 delete_game() |
4.查看游戏:
1 | unsigned __int64 view_game() |
vulnerable:
在 add_game() 中存在 off-by-null 漏洞,通过对 chunk 的 size 字段进行 修改 , 通过 Tcache poison 实现目标
利用:
阶段1:泄露 Heap 与 Libc 地址
当较大的 chunk 被释放后会进入 unsorted bin , unsorted bin 中不同的 chunk 会通过 fd 和 bk 指针链接,其中包含指向 main_arena 的地址和堆块自身的地址。比如:
1 | add(0x500,b'A'*8,b'B'*8) # 0 |
通过调试看到:
1 | pwndbg> bins |
再将它们申请回来可以获得 Libc 和 Heap 地址。
阶段2:构造 Fake Chunk (绕过 Safe Unlink)
泄露之后自然地想到继续申请几个 chunk ,使用 off-by-null 来覆盖下一个 chunk 的 size 字段制造堆块重叠,( 报错就好受了) , 先绕过 glibc 2.31 的安全检查机制,要在想修改 size 位的 chunk 前伪造一个 fake chunk 。
所以需要:
在 Chunk 4 中伪造一个 prev_size 和一套假指针(fd 和 bk 指向伪造位置)。利用 add_game 的 off-by-null 将 Chunk 5 的 size 位(原本是 0x501)最低字节改为 00,并清空其 PREV_INUSE 标志。同时在 Chunk 5 的 prev_size 处填入指向 Chunk 4 伪造起始位置的大小 。
1 | 0x5f790d14edb0 0x0000000000000000 0x00000000000000b1 |
阶段3:制造堆块重叠 Overlap
执行 delete(5)。系统检查到 PREV_INUSE 为 0,会根据 prev_size 寻找上一个 空闲 块。 系统找到 Chunk 4 所在位置并完成合并。此时,Unsorted Bin 中出现了一个覆盖了原有 Chunk 4 和 Chunk 5 范围的巨大空闲块。后果:chunk 4 的指针仍然存在,且指向了现在处于空闲状态的内存内部。实现了类似 UAF 的效果。
阶段4:Tcache Poisoning
从小切割刚才合并的大块(add(0x20)),拿到 chunk 5。此时 chunk 4 和 chunk 5 实际上指向同一块或极其接近的内存。释放 chunk 6 和 chunk 5 进入 Tcache。通过 edit(4) 修改 chunk 5 原本在 Tcache 中的 next 指针,将其指向 __free_hook。
编辑4实际上是改 chunk_5 的 next
1 | edit(4,p64(free_hook),b'aaaa') |
可以看到确实改成功了:
1 | pwndbg> bins |
阶段5:Getshell
连续申请两次 0x20 的 chunk。第二次申请将直接返回 __free_hook 的地址。在 __free_hook 处写入 system 函数地址。将某个 chunk 的开头写为 /bin/sh\x00 并对其执行 delete 操作。触发 free("/bin/sh") -> system("/bin/sh"),成功获取 Shell。
1 | from pwn import * |
OverLib
main里只调用了两个函数,还是在 libfunc.so 里的,于是直接逆向 libfunc.so
game 函数是程序的主体:
1 | unsigned __int64 game() |
发现是 game() 通过 scanf() 读取了 32 个 int 数据,当输入 + 时,由于不匹配而不会读取,也就不会覆盖掉栈上原来的数据;投硬币游戏可以泄露出这些原本在栈上的数据,比如libc
还有 get_gift() 和 gift
1 | int get_gift() |
1 | unsigned __int64 gift() |
gift() 中给了一个 参数完全由我们掌控的 系统调用 我们非常喜欢的方式是直接 execve("/bin/sh") , 但是这里的话第二个参数似乎有些问题,于是想到 syscall 0 读取一个 rop 链到 RBP 的位置,这样程序退出时就会触发这个 rop 链了,rbp 的位置可以通过泄露栈地址获得。
1 | from pwn import * |
- 标题: JNCTF 9th
- 作者: Snowcat
- 创建于 : 2026-04-19 16:00:00
- 更新于 : 2026-05-19 22:35:29
- 链接: https://sadsnowcat.github.io/2026/04/19/jnctf9th/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。