作者:selph,首发自先知社区:f43K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5P5W2)9J5k6h3q4D9K9i4W2#2L8W2)9J5k6h3y4G2L8g2)9J5c8X3&6W2N6%4y4Q4x3V1j5I4z5o6V1&6x3R3`.`.唯一一个困难pwn题,难是真难,为期2天的比赛总共只有11只队伍解出,该题是glibc-2.41下的题目,总共3个难点:如何绕过图像校验?如何在受限情况下进行堆利用?受限情况下如何劫持执行流?对于前半,本文将介绍完整的深入分析复杂流程的过程,对于后半,本文将结合glibc2.41源码来介绍fastbin+tcachebin的组合利用技巧,和exit中攻击向量。 题目来源:DefCamp CTF 2025(2025年9月14号),困难 pwn保护全开,glibc-2.41 版本main函数很长,分段来看(这里是重命名后的结果):读取用户输入的图像数据,手动输入大小和内容,需要自己构造加载图像并解析为 RGB 格式,这里通过 stbi_load_from_memory函数进行,该函数是静态编译进去的库函数对图像进行一系列复杂的浮点数运算(问了AI知道是像素级的处理与变换,大致如下在处理后生成一个“权限值”(privilege),后续进入一个交互式菜单系统,根据权限值(需要是0才行)决定用户可执行的操作(如分配、释放内存块等)。让 privilege的值为0的条件如下:需要 privilege_pointer的值是4919,此处我测试了一个只有1个像素的bmp图片,将图片输入,这里解析出图片RGB三个hex值,经过一系列运算后,向privilege指针进行异或操作:这个操作进行一系列 &运算,结果就是 privilege_pointer的值和一个4位的值进行异或然后紧接着的相关操作如下:这里对 privilege_pointer进行 |运算,最后3为恒定设置为1,然后进行 & 0x1fff,保留最后2字节,最后在 | rand() & 0x3f设置最低一字节一个值4919的十六进制:0x1337,最后的7是无论如何都会设置上的,问题在于中间的两个3,最后的那个3的值取决于 rand函数的结果,也就是说,通过不断运行,就有概率得到0x37的最低字节值问题在于如何在得到0x13的值?这一块已经分析不出什么来了,注意啊!这是pwn问题,就要通过pwn的方式来完成目标!!回过头来,看看这个 privilege_pointer怎么来的:他们的定义:可见,privilege_value紧挨着 modification_parameters_1数组,局部变量里就这么一个数组,碰巧有 privilege_value紧挨着,是不是有可能,这个数组会发生溢出来影响 privilege_value的值呢?继续深入分析 modification_parameters_1参数:然后是浮点运算用的了,正常使用逻辑下一般不会出现越界溢出,这里可疑的就是 stbi_load_from_memory函数,进去追踪该参数:继续进入 stbi__load_and_postprocess_8bit:这里还要继续深入 stbi__load_main:这里可以看到,大量函数都用了 modification_parameters参数,经过逐个探索,发现位于 stbi__pic_load函数中存在溢出问题:这里先调用了 stbi__pic_load_core函数,调用完之后,下面的for循环进行赋值,从 image_noises中赋值,这里会赋值到 image_modifications_ptr[10]中,刚好就是溢出覆盖到下一个成员的地方能否在第2字节处赋值到0x13字节,就需要这里 image_noises[10]的第二字节是0x13,继续深入,在经过一系列计算之后,在最后会出现如下运算,给 image_noises数组进行赋值操作:到这里已经分析明白了如何给 privilege_value赋值,接下来就是分析如何构造结构了,这些函数都是基于文件结构进行一系列计算和处理,我们需要给定正确的格式才能执行到这里完成溢出的目标回到 stbi__load_main:我们要进入 stbi__pic_load,需要先通过 stbi__pic_test的校验:很显然,这里是校验文件头的地方,需要最前面4字节是PICT,然后在0x53字节处也是PICT然后在 stbi__pic_load中解析结构的部分如下:跳过0x5b字节,然后以16位大端序读取图像宽和图像长的值这里需要在0x5c处构造width,0x5e处构造height,这两个数据不能超过4096,也就是0x1000,换成大端序就是0x0010然后跳过8字节,进入 stbi__pic_load_core,前半部分如下:这里header_data是局部变量,对结构的操作如下:读取1个字节,作为类型,是0的话,就会直接跳出循环,降低运算的复杂性读取3个字节,给header_data数组这个do-while循环以4字节一组进行读取数据作为header信息然后后半段:这里根据header_data第一个字节判断是什么类型,我们需要进入的片段在最后为0的分支里所以刚刚读取的第一个字节的值需要是0然后这里,是个循环,循环次数取决于width,就是之前我们设置的width值每一轮读取4个字节,首字节和上一轮的首字节相减求模11,第一轮的上一轮首字节是0,如果这个值是负数,就求反根据模结果来决定image_noises的索引,我们需要溢出,需要这个值是10然后对于noise_value,则是读取该索引处原本的值,然后对读取的4字节中的中间2字节进行异或,乘以一个浮点数,结果进行累加为了让这个数值更快的加到0x13??,最好让异或的结果大一些,选择0xf0和0x0f进行组合对于首字节,交替输入0和10即可,做差求模一定是10综上,每2次循环为一组,则如此构造:因为乘法的常量1.0e-16是个很小的数字,所以需要累加很多很多次,循环次数取决于width,所以这里可无脑填充大量的该8字节组合数据,通过不断测试width的值观察程序输出结果来判断width应该是多少(最终 privilege_value需要是0x1337,末尾的0x37是固定的,只需要找到一个合适的width让 privilege_value第二个字节的值为0x13即可)经过测试,width为0x55时,能够覆盖出目标值,最终需要构造的结构&payload发送因为随机数的存在,需要多尝试几次,直到成功:成功拿到privilege==0之后,下面的菜单选项变得可用:do_alloc:这里申请内存长度取决于输入长度,范围是0到0x5f,最大申请出来0x70的chunk,tcache和fastbin范围内会记录当前申请的size大小,可以无限申请该size,size只能变化1次,变化后就只能申请变化后的size管理内存的数组一共11个,索引0-10可自由安排do_delete:释放chunk后没有清空指针,潜在UAF和Double-Free问题do_show:可以打印
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-288618.htm
[原创]DefCamp CTF 2025 onigirl 复盘详解
419 浏览
4 回复
感谢分享~方便的话麻烦上传一下附件供其他师傅学习、复现,谢谢!
题目已上传至附件
上传的附件:
pwn-onigiri.7z
(3.85MB,11次下载)
上传的附件:
pwn-onigiri.7z
(3.85MB,11次下载)
可以私聊吗
"需要注意,unsortedbin chunk next chunk也需要伪造",想问一下这里的next chunk怎么伪造?因为进unsortde bin最少0x421size的才行,但11个块即使申请完,即使是0号块+0x420也刚好到top chunk,没有可以控制的块?