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

[原创] 第十题:卯时·曦光初现 wp by lzq2000

1天前
374

题目给了个 ELF pwn,再配一份 libc-2.27.so,远程是 nc 123.57.66.184 10086。保护拉满:Full RELRO、Canary、NX、PIE,libc 还是 2.27。跑起来就一个 > ,整行读进去解析,语法不对打 invalid syntax!,执行出错打 runtime error!

一开始以为是普通菜单堆题,IDA 打开才发现是个自己写的小解释器。后面整道题其实就一件事:用解释器自己的对象标签和算术溢出,把任意相对读写做出来,再在 2.27 上把 __free_hook 写成 system

main0x2d09。开头把 stdin/stdout/stderr 都 setvbuf(..., _IONBF),然后死循环:

有两个很烦、后面又很有用的细节:

语法是整行正则对上才认,变量下标只能是 0~255。能用的语句大概就这几类:

数字字面量走 stoul,变成 Number 对象时 只留低 48 位& 0xFFFFFFFFFFFF)。BinOp 却 不截断。这俩不一致就是洞。

变量表在 BSS unk_2062A0,256 个 qword,存的是带标签的对象指针:

真正的对象数据不在这个表里,而在一块 new[](0x10000) 的 bump arena 上:

分配函数是 sub_31DC:bump 往前走 8 * nfields,超过 arena+0x10000 就打印 OOM 然后 exit(1)。没有 GC,对象也不会回收。

对象头和字段都跟 key 异或:

sub_1EDA 做 GetField / 按下标 skip 的时候,先把指针指向的 qword 跟 key 异或,高 16 位必须是 0xBEEF,低 48 位当 size。如果字段本身高 16 位也是 0xBEEF,就当成嵌套对象再往里 skip。GetField 取到一个 BEEF 头会直接 runtime error——后面漏 bit 就靠这个。

所以从解释器的视角看,只要你能把某个字段写成 key XOR (0xBEEF << 48 | size),再对它做 $obj[0][K],就能相对这块假对象做任意远的读写。 字面量造不出 0xBEEF,因为高 16 位被抹掉了;必须用 BinOp 把高位加回来。

造法很干净:

现在 $0 是一个「看起来有 65536 个字段的对象头」。再分配一个真正的小对象,把这个头种进去:

之后 $4[0][K] 就会把 $4[0] 当成一个巨大对象,从它后面开始按 qword 往外爬。K 没有 256 的限制,字面量直接写 79978066 都行。

key 怎么拿?对象头后面第 0 个字段附近,往回/往旁边读到 arena 里别的 BEEF 头再异或回去就行。实际用的是:

读堆上任意 qword 的流程是:

写的时候反过来:先把要写的值跟 $5 异或,再 SetField

这步踩了挺久。本地和远程的 libc/IO 初始化不完全一样,必须先分配 4 个 48 元数组当 bit oracle,再分配 $4,后面那些 K 才对得上。

顺序固定成:

种完 BEEF|65536 之后,相对 $4[0][K] 看过去,远程是这样:

字段地址可以这么换算:

libc-2.27.so 里用到的偏移:

Number AST 自己是 operator new(0x10),落在 0x20 的 chunk 里:+0 是 vtable,+8stoul 出来的值。这个布局后面毒 __free_hook-8 正好用得上。

解释器没有打印数字的语句,堆上的值只能间接看。GetField 碰到 BEEF 头会 runtime error,所以可以当 1-bit oracle:

四个 48 元数组刚好够漏一个 48-bit 的 libc/heap 指针。高 16 位在用户态地址里本来就是 0,丢了也无所谓。

实际只漏了 K=8023 的 vtable:

K=7996 那里躺着一块 0x230 的 FILE,vtable 还在,看起来特别像标准 FSOP 素材。顺着 _IO_wfile_jumps 去改 wide_data_mode,再想办法触发 exit_IO_flush_all

不行。这块 FILE 已经被 fclose 掉了,不在 _IO_list_all 上,flush 根本走不到它。2.27 还有 vtable 校验,乱指 _IO_wfile_jumps 也不一定过。OOM 的 exit(1) 倒是能触发,但没有 shell。

前面说过了,getline 超长只置 failbit,循环看的是 eofbit。屏幕上会刷一堆 > ,进程还活着。

这两条废掉之后,回头看那些 0x20 tcache,路就清楚了。

2.27 的 tcache 没有 key 保护,fd 就是裸指针。远程在 $4 后面能看到一串 0x20 chunk,其中 K=8066 是某个 0x20 的 fd。

利用步骤:

有两个坑,不注意会以为没打上:

shell 起来之后发 cat flag;echo 就出 flag。

远程一次连上就能跑完。bit oracle 要漏 48 次,会慢一点,但比本地调试稳。

跑完能在输出里看到:

这题的解释器其实写得很规矩:变量范围卡死、字面量截断、对象头带标签、GetField 还检查 BEEF。真正漏的是两处不一致——BinOp 不截断,以及 GetField/SetField 的下标是裸字面量、不走变量范围检查。两处叠在一起,标签系统就变成了相对任意读写。

利用上 2.27 还是老味道,tcache 没有 key,__free_hook 最好打。FSOP 看起来香,但这块 FILE 已经不在 _IO_list_all 里,白折腾一场。最后卡壳的反而都是 C++ 的小细节:SSO 15 字节、getline 只看 eofbit、free(buf) 必须先让 std::string 走上堆。

最终flag:

BSS 含义
0x206AA0 arena 基址
0x206AA8 bump 指针
0x206AB0 /dev/urandom 读出来的 8 字节 key
K 含义
7994 下一 chunk 的 prev_size,0
7995 chunk size,0x231
7996 一块已经 fclose 过的 FILE,flags / tcache fd 是 0
7997 _IO_read_ptr,值是 heap = arena + 0x10010
8009 _IO_2_1_stderr_,也就是 libc+0x3ec680
8023 这块 FILE 的 vtable,_IO_file_jumps = libc+0x3e82a0
8065 / 8069 / 8073 连续几个 0x20 chunk 的 size(0x21
8066 / 8070 / 8074 这些 0x20 chunk 的 tcache fd
cin.getline(buf, 256)
把 buf 构造成 std::string
丢给 sub_2B14 做整行匹配
成功就拿 AST 去跑,失败就 invalid syntax!

冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

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