论坛首页 逆向工程技术区 阅读主题

[原创]2025 强网杯S9 pwn - bph 复盘详解:一个任意地址写00到RCE的新技巧

76 浏览 0 回复
#1 楼主 2026-06-01 21:09:19
文章首发自 先知社区:572K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5P5W2)9J5k6h3q4D9K9i4W2#2L8W2)9J5k6h3y4G2L8g2)9J5c8X3&6W2N6%4y4Q4x3V1j5I4z5e0t1#2z5l9`.`.pwn的技巧都跟魔法一样,一瞬间,发生了很多事情,如果不了解原理,那真的很难理解了,这里和大家分享一下 强网杯S9 pwn-bph题目的魔法般的技巧(内含glibc-2.39源码&反汇编进行辅助讲解)xx一把梭搞得很多并不简单的题目分都烂了条件:当得到任意地址写00,且程序存在利用IO_FILE获取输入的场景时,例如:fgets,fread等流程:可以向stdin->_IO_buf_base末位写入00,扩大输入缓冲区以至于能够覆盖stdin的输入缓冲区指针控制输入缓冲区指针指向stdout(利用下次输出时候的虚指针调用),或者stderr(利用exit流程的虚指针调用)再次输入可以覆盖目标IO_FILE结构,为后续利用做准备利用 puts:_IO_sputn(vtable+0x38)的调用,配合house of emma + house of cat组合完成控制流劫持‍此题目存在沙箱,所以后续还需要进行栈迁移打ROP,具体思路就是伪造IO结构控制rdx,通过setcontext+61栈迁移进行ROP,ROP ORW绕过沙箱读取flag具体流程和分析过程看下面今年强网的题目分析吧glibc 2.39 版本(目前最新的大版本),程序保护全开:存在沙箱:只能使用openat,read,write这些系统调用open函数内部调用的系统调用就是openat:(来自 glibc-2.39 源码)最终需要通过open-read-write完成flag的读取main 函数:这里init函数初始化了2个全局变量:size和ptrsub_1640函数提供了泄露地址的机会:可以利用残留数据泄露地址功能函数中只有create和delete实现了,其他两个没实现功能create:读取size,申请内存,读取数据,写入末尾的00,设置ptr和dword_4040=1**假如输入的size过大,malloc会失败返回0,read也会失败,但是 ptr+size-1=0****依然会执行,就会对 size-1=0**进行赋值,此处存在任意地址写入00字节delete:检查dword_4040和ptr,如果存在ptr且dword_4040=0,才能执行free,这个条件不可能存在,所以可以认为这个函数不存在,不用再看了程序逻辑很简单,就一次任意大小分配+末尾写入00字节的功能分析一下现状:glibc-2.39 版本,保护全开存在地址泄露,可以泄露libc地址存在任意地址写入00字节通过fgets读取输入思路:精准满足3个条件,这里可以用从任意地址写入 00 到 RCE的技巧:向stdin的IO_FILE结构体中_IO_buf_base末位写入00,就可以在下一次输入的时候,将输入的数据读取到输入缓冲区base到end的位置上,通过向末位写入00,可以让base指针指向更提前的地方,以至于下一次写入能够完整覆盖_IO_buf_base和_IO_buf_end下一次写入修改stdin的_IO_buf_base和_IO_buf_end为能够覆盖 stdout 结构体的范围,再下一次读取数据便可以完整覆盖stdout利用程序中会调用puts函数,puts函数会调用 _IO_sputn函数指针,通过house of emma偏移vtable指针的思想,即可通过调整偏移伪造FILE结构完成利用对于沙箱,则通过setcontext来绕过即可利用残留数据泄露libc地址:通过构造size = stdin->_IO_buf_base + 1,进入create函数,触发任意地址写00写入00后的stdin:这里+0x38处就是_IO_buf_base,+0x40处就是_IO_buf_end,可以看到,范围从0x7ffff7f90964(原本缓冲区就1字节)变到0x7ffff7f90900~0x7ffff7f90964,范围刚好覆盖到这个_IO_buf_base和_IO_buf_end下一次输入的时候,输入缓冲区会从0x7ffff7f90900开始,需要伪造0x7ffff7f90900开始的数据:这里只用管_IO_buf_base和_IO_buf_end的值,前面的值会自动更新的,让他刚好覆盖到stdout结构体即可,覆盖完之后:下一次输入的时候,输入缓冲区就会覆盖到stdout结构体,现在可以伪造stdout结构了,利用puts的 _IO_sputn调用进行利用摘出来:r13是stdout指针需要第一个跳转不成立,第二个成立最小化payload,可以让+0xa0和当前IO_FILE结构重叠,指向stdout本身需要+0x8 和 +0x10不同,这个不用管,+0x10会自动赋值,必然和+0x8的不一样+0x18 < +0x20rdx 可控, 接下来的赋值:需要用到+0x20的值,+0x20也设置为fp此时的+0x18需要满足上面的条件且为一个libc中代码段地址(gadget):此时内存布局,stdout的位置如下,可见gadget地址一定小于fp地址接下来的流程:需要[rax+18] < [rax+0x20] 来规避跳转,这个问题刚刚已经解决了[rax + 0xe0] 也重叠 fp,最后call到 +0x18处payload:对于寄存器的控制:rdx = [fp+0x20]setcontext+61:需要可控rdx就能控制寄存器的值,因为需要rop,所以需要控制rsp的值,然后最后通过ret进入rop链中,此时已经不再需要IO结构体了,可以损坏IO结构,在这片内存上任意挥霍,最简单的思路就是跳转到read上,写入数据到rsp里开始rop+0xa0已经设置为fp了,会被赋值给rsp这里最后经过push操作,在ret的之后,rsp指向 _IO_2_1_stderr_+216,也就是push的值:[rdx+0xa8],这里是ret跳转的地址直接跳转到read,控制rdi=0,rsi=fp,rdx=size,rdx=[fp+0x88],数字过大,read调用会失败,需要再次进入setcontext重新设置三个寄存器的值即可,原本fp+0x88指向fp+8,只需要将需要赋值的偏移+8,最终的结构体:完成ORW需要设置3个参数,这里的libc-2.39直接用ropper或者ROPGadget无法搜到pop rdx的片段,但是这不重要,第三个参数只要是个数字就行,是多少无所谓,直接搜mov dl的片段:利用这个完成第三个参数的赋值即可,dl是rdx的低8位,rop的时候rdx=0,可以这么用最终rop chain:一个新的技巧1:任意地址写00到rce,利用IO读取(fgets)会使用IO_FILE读取缓冲区的特点,完成IO_FILE结构伪造,通过puts触发虚函数调用劫持执行流一个新的技巧2:orw rop的时候,对于没有pop rdx的场景,找mov dl, 0x??也

...(已截断)

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

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

请登录后参与讨论

立即登录 注册账号