-
-
[原创] 第十题:卯时·曦光初现 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。
main 在 0x2d09。开头把 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 的限制,字面量直接写 7997、8066 都行。
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,+8 是 stoul 出来的值。这个布局后面毒 __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内核攻防全技术栈,打造具备自动化能力的内核开发高手。