论坛首页 CTF竞赛交流区 阅读主题

[原创] 看雪 2025 KCTF 第八题 暗云涌动

328 浏览 0 回复
#1 楼主 2026-06-01 21:08:55
程序不大,将反编译的代码稍微修一修后,整体喂给AI,得到回答:这里AI直接给出了VM结构体的字段定义、虚拟机指令8字节的编码格式,甚至还包括每种虚拟指令的opcode以及相应的功能人工在补一下8字节虚拟指令的结构体定义,连同AI给出的VM结构体定义,可以得到一份非常易读的伪代码: (p.s.以往分析到这种程序需要耗费大量时间,而现今的AI已经可以瞬间完成了) 注意到虚拟指令提供了读写内存的功能,那么题目预埋的漏洞点应该就是设法获得范围外的任意内存读写。 虚拟机的访存指令校验且只校验虚拟寄存器保存的地址的最高位为1,然后直接将剩下的部分作为裸指针访问内存。
(不知道本题的设计灵感是否有参考ARM的MTE(Memory Tagging Extension)机制)那么,如果要达成任意内存读写原语,则需要做到让虚拟机寄存器的最高位为1的同时低63位自由可控。 常规的movimm/pushptr虚拟指令只能加载32bit的立即数,因此必须依靠算术指令才能影响高32位。 所有的算术指令都有对虚拟寄存器最高位指针标记的判断处理。假定最高位为1的为指针,为0的为数值,那么规则是:数值与数值运算结果仍为数值;指针与指针运算结果变为数值(确实存在这种现实用法,例如指针减法计算数组长度等);指针与数值运算结果仍为指针,但要经过check_and_mark_ptr(sub_128B)的范围检查,在保留标记的情况下强制让结果位于虚拟机的内存范围内。 算术指令的指针标记传播规则从逻辑上是不存在问题的,且check_and_mark_ptr的检查是严谨且无法绕过的,看起来只要想获得高位标记就比如要经过范围检查? 但是从题目的角度看,一定是需要获得任意读写原语的,而且过程必然绕开算术指令,另外绝对不能触碰到调用check_and_mark_ptr的代码路径。 以这个思路重新审视各个算术指令的实现,最终发现题目算术指令对指针标记检查的缺陷:指针标记检查只有在原始的两个运算数至少有一个含有标记的时候才会启用,而标记自身只是虚拟寄存器的最高位,会参与到运算中。对于加/减/乘指令,很容易利用进位使得两个63位的操作数在运算后的第64位变为1(例如最简单的0x7fffffffffffffff+0x1=0x8000000000000000),这样既不会触碰到检查,又可以在低位可控的情况下设置到高位,从而能够利用访存指令实现全地址空间的任意读写。 下一步,获得任意地址读写能力后仍需要找到一个能帮助获得shell的可写地址。程序保护全开(NX、PIE、FULL RELRO),且libc2.35的高版本已不存在malloc_hook/free_hook,剩下比较简单的可劫持控制流的内存写入地方是栈上的返回地址。 栈地址可以通过libc全局变量environ获得,而写栈构造ROP也需要来自libc的gadget和目标函数,因此必须获取到libc的基地址。 虚拟机指令本身无法间接造成堆的分配和释放,且程序初始分配堆块时也有意清零,因此堆上不存在任何来自libc的地址残留。 check_and_mark_ptr(sub_128B)在校验超出范围时会强制将虚拟寄存器的值设为vm->dmem,而初始化时dmem是mmap到0x200000的固定值,并没有任何帮助。 但是,check_and_mark_ptr(sub_128B)校验范围时,还同时校验了值是否位于vm->stack的范围内,而vm->stack是在堆上分配的内存。 通过判断算术指令经过check_and_mark_ptr后的虚拟寄存器的值是否等于0x8000000000200000可以获知是否虚拟寄存器的值是否位于vm->dmem或vm->stack的范围内,如果为位于vm->stack的范围内,那么就得到了一个堆地址。 VM结构自身也分配在堆上,且堆结构稳定各个堆块偏移固定,在得到堆地址后可以获取VM结构的各个字段的值。其中vm->imem是未指定地址方式mmap的内存,在同一个环境下,这种方式mmap的地址与libc的相对偏移通常是固定的。 (p.s.这部分漏洞发现和利用最终是靠人工分析出来的。可能是提示词写的不好,并没有成功的让AI分析出来,虽然体感上AI应该能做的到) 至此,通过逐步推理和审查代码,打通了本题的全部利用路径: 具体到写exp,为了方便调试,先针对题目的虚拟指令集写了一个小汇编器(支持跳转和注释)。 63位立即数的加载可以通过先加载高位到低位置,再通过乘法移位到高位置,最后加上低位到低位置。
最高位(第64位)为1且低63位可控的虚拟寄存器值的构建,可以在低63位设定好后加1再加0x7fffffffffffffff得到。
其他各种偏移可以静态以及动态调试获得。 堆区的随机范围大致在0x555550000000-0x565550000000左右,为了稳妥,放大一些范围,从0x550000000000开始以0x800为步长,平均30多秒能探测到,程序里alarm给了100秒的时间,本地完全够用(但最后打通远程的时候还是缩小了范围到0x555500000800开始且以0x1000为步长,详情见下) 然后,陷入到了pwn题经典:本地打通远程死活不通……
问题就出在vm->imem的mmap地址和libc基地址的偏移上(下方exp里的heap_imem_mmap_pointer_offset),这个偏移虽然在同一个环境下反复启动一般是不变的,但不同环境下值可能是不同的,且影响因素非常玄学。
先后尝试了本地ubuntu24.04裸机patchelf换libc、本地ubuntu24.04 libc2.39裸跑、开启or关闭aslr、ubuntu22.04 libc2.35裸跑、ubuntu22.04 docker内手动启动、ubuntu22.04 docker内xinetd直接启动,发现偏移量都不全一样(前三个环境不考虑,后两个环境下发现大致在0x260000附近变化)
(尝试过多开exp爆破,但总是不成功,后来本地测试发现也不太行,感觉可能是爆破并行度太高?亦或者是最初堆地址的探测范围太大导致高并发下因cpu限制无法在100秒内完成?总之投机行为没有成功)
最后参考了去年第八题的dockerfile,在ubuntu22.04 docker+题目给的libc2.35覆盖+xinetd在chroot下启动,得到偏移0x262000,试探性的打远程,在多次重试中终于偶然有一次打通了(然后,第一次打通时由于爆破的遗留没开log没开interactive也没打印flag只打印了ls……然后又跑了很多遍才打通第二次拿到了flag) (最后还是疑惑,又测试了几遍,发现自己测试时相同环境下偏移量是固定的且exp能稳定打通(包括部署到另一台vps上从本地打),但是打远程总是要尝试很多次才能成功一次,不知道具体原因是什么以及出题人验题时有没有遇到过) 最终exp: 一、结构体与内存布局(修正类型)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-288319.htm

暂无回复,快来抢沙发吧!

请登录后参与讨论

立即登录 注册账号