-
-
[原创] 第十题:卯时·曦光初现 WP
-
发表于: 1天前 369
-
题目程序是一个用 C++ 写的简易表达式解释器,支持 256 个变量寄存器,语法包括:
保护全开:
程序把对象序列化到一块固定大小的 arena 中,arena 大小是 0x10000。变量里保存的值分两类:
列表在 arena 中的格式是连续的 qword 流:
这里的 cookie 在程序启动时从 /dev/urandom 读入。
还有一个很关键的细节:数字字面量在写入 arena 时只保留低 48 位,但是变量寄存器里的运算结果是完整的 64 位。因此可以先构造出 2^48,再通过乘法和加法在寄存器里拼出完整的高位常量。
列表读写最终都是在 arena 的序列化数据上做遍历。程序判断“当前元素是不是子对象”时,只看解码后的高 16 位是不是 0xbeef,并且完全信任 header 里的长度。
这意味着只要把一个普通整数槽位改写成:
解释器就会把这个普通整数当成一个超长子对象继续向后遍历。
原本的对象边界是:
伪造以后会变成:
这样就得到了 arena 内任意偏移的读写能力,而且可以继续越过 0x10000 的 arena 边界访问后面的 glibc 堆。
先初始化一个只有一个元素的列表:
然后在寄存器里构造 0xbeef000000004000。构造方法和最终 exp 完全一致:
这几步之后,$10 的值就是:
把它写进 $1[0]:
此时 $1[0] 已经不是普通数字,而是一个伪造出来的超长对象头。后续访问:
实际对应的是 arena 里的:
也就是:
接着读取:
这里会读到 arena_base + 0x10 这一格。这个位置尚未使用,原始内容是 0,于是解释器返回的值就是:
于是 $20 中拿到了完整的 cookie。
真实利用链里,最终被劫持的对象是一个 SetField AST,它在稳定布局下位于:
对应的 chunk 头在:
exp 里把这个 chunk 的大小伪造成 0x501,并在后面补一组能通过 glibc 检查的假 chunk 元数据:
这样一来,这个对象在析构时会按大 chunk 进入 unsorted bin。
在当前题目的稳定堆布局下,读取:
可以拿到 unsorted bin 写入的 fd 指针。这个值不是原始指针,而是:
所以要先和 $20 异或,才能还原出真实地址:
还原后的值是:
因此:
arena 后面的稳定布局里还有一个现成的堆指针,位于:
读取后同样先和 cookie 异或:
还原出的真实值是:
所以:
拿到 libc_base 和 heap_base 之后,后面的地址都可以直接算出来。
最终控制流劫持点是 SetField AST 的虚表指针。主循环在执行完 AST 以后会调用虚表里的析构函数:
因此做法很直接:
exp 中使用的固定布局是:
setcontext+0x35 的作用是从指定内存中恢复寄存器,然后跳到新的 rip。需要布置的关键字段如下:
其中:
命令行参数组织成:
fake stack 的第一个 qword 放 exit,这样 execve 如果失败返回,也不会立刻跑飞。
最后一步只需要把:
覆写成 fake vtable 地址。对象析构时会跳到 setcontext+0x35,随后直接进入:
完整流程可以概括成:
Arch: amd64
RELRO: Full RELRO
Stack: Canary
NX: Enabled
PIE: Enabled
0x1337000000000000 | addr
header = (0xbeef000000000000 | length) ^ cookie
elem = real_value ^ cookie
0xbeef000000000000 | fake_length
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。