首页
社区
课程
招聘
[原创] 第十题:卯时·曦光初现 WP
发表于: 1天前 369

[原创] 第十题:卯时·曦光初现 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_baseheap_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内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回