SUCTF2026 Minivfs
阻穷西征,岩何越焉?
题目分析
模拟了一套简单的虚拟文件系统 , 包含了几个常用接口
1 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
其中touch对应add , rm对应delet , cat对应show , write对应edit
stat和ls则可以查看单个文件的情况和整体文件的列表
TOUCH

经过了一定程度的混淆, 使用的规则为
1 | touch <filename> <size> <auth> |
其中auth有一套简单的哈希计算逻辑
1 | __int64 __fastcall sub_15F6(__int64 a1, int *a2) |
size的大小必须位于(0x417 , 0x500] , 也就是largebins的范围 ; 堆指针会被储存在全局变量中
我们可以用一个简单的脚本得出filename对应的hash
1 | def calc_auth(path: str) -> int: |
rm
使用规则为
1 | rm <filename> <auth> |

可以看到不存在uaf
write
使用规则为
1 | write <path> <nbytes> <auth> |

注意到, 这里memcpy后, 会把末尾+1的bytes进行置零操作
所以这里存在off-by-null操作
cat
使用规则为
1 | cat <path> <auth> |
比较常规没什么好说的
可以看到每种hash只有一个全局变量的槽位
也就是即使名字不同也可能存在哈希碰撞, 这一点要特别注意, 以免堆块申请失败
可以编写出如下的辅助函数
1 | from __future__ import annotations |
攻击链分析
信息泄露
我们可以发现, 当被rm的file地址再次被touch , touch原语并没有清除原本地址内的指针数据
于是我们可以轻松地利用unsortedbins泄露出堆地址和libc地址
1 | touch("1111",0x480) |
此时将堆内存恢复到最初没有申请的状态
off-by-null
本次libc的版本为2.41, 属于标准的高版本
2.27以上的off-by-null有着特殊的手法

首先我们要准备ABC三个堆块和一个隔离堆块
其中B必须复用C的pre_size位 (sizeof(B)以8结尾)
第一步, 在A中伪造一个fake_chunk , 使得sizeof(fake_chunk)=sizeof(A+B)-0x10 (剔除chunk_head_A)

第二步, 编辑B, 在C的pre_size中填上sizeof(fake_chunk), 此时因为off-by-null导致C的pre_inuse位被置零
此时在ptmalloc2的视角中 :
- C的前一个堆块的大小是fake_chunk , 且前一个堆块处于释放状态
- fake_chunk的前一个堆块处于使用中状态不可合并, 且大小为fake_chunk, 与C的pre_size相符

此时释放C, ptmalloc2会判断前个堆块可合并, 且合并后被放入unsortedbins中
此时B虽然处于使用中, 但被送入了unsortedbins中
对应以下操作
1 | touch("1111",0x440) |
此时拥有了以下的效果
1 | pwndbg> heap |
此时虽然形成了堆叠, 但手法仍未结束, 我们必须取出包裹中的B , 形成双指针指向B才能真正利用
1 | touch("3333",0x438) |
此时我们获得了完全重叠的2222和2222222222222
largebins-attack
因为elf要求我们申请的大小必须在unsorted-bins中
所以只能使用largebins-attack , 我们可以将堆地址写入_IO_list_all进行FSOP
下面给出how2pwn仓库中largebins-attack的最小poc
1 | int main(){ |
我们需要先形成一个chunk位于unsortedbins, 一个chunk位于largebins中的状态
这个很容易实现, 只要unsortedbins中的chunk不符合申请大小便可以从unsortedbins转移到largebins中
1 | rm("2222") |
因为unsortedbins是尾插, 当申请0x510堆块时会从2222开始扫, 导致2222被放入了largebins中, 最后将7777i7申请
最后再释放888i:88888就形成了预期结构
1 | tcachebins |
下一步就该 : 将largebins堆块的bk_nextsize指针修改为<target>-0x20
可以通过2222222222222办到这一点, 最后再申请一个同时超过bins中两个堆块大小的堆块
1 | _IO_list_all=base+libc.sym._IO_list_all |
以下为部分gdb信息
1 | pwndbg> telescope 0x7ffff7e114c0 |
可以看到0x55555555df50(888i:88888)被写入了目标
但是我们没有操作这个堆块的指针, 只需要最后一步, 将这个堆块申请出来, 就可以将2222222222的地址写入_IO_list_all中
1 | touch("2222",0x430) |
可以看到
1 | pwndbg> telescope 0x7ffff7e114c0 |
现在我们可以通过3333复用2222222222的pre_size位写入魔术头, 然后伪造其他字段形成完整的结构体
House of some
1 | zer00ne@zer00ne-VMware-Virtual-Platform:~/desktop/SUCTF/work$ seccomp-tools dump ./pwn |
这道题开启了沙箱 , 也就是说必须使用orw
这就涉及到了ROP
刚刚好house of some就可以完美解决这个问题 , 详细内容可以看我的另一篇文章 : house of some
这里就是公式地写出
读IO_FILE(读栈地址) ->写IO_FILE(写入下一个写IO_FILE)->写IO_FILE劫持read函数的返回地址进行稳定的ROP
1 | write("3333",b'a'*0x430+p64(0x800 | 0x1000)[:-1]) |
getdents64
这道题的远程环境有一个小trick
真flag被重命名为了flag_<hash>
我们没法直接读flag文件, 这就涉及到了一个系统调用getdents64 :
1 | getdents64 是 Linux 内核提供的一个底层系统调用,用于读取目录项(directory entries),也就是列出目录中的文件和子目录。它是 readdir() , system("ls")等高级接口在底层的实现基础之一。 |
也就是说我们可以先打印文件名再进行读文件
我选择ROP转shellcode, 这样可以精密地操作执行流
1 | ret=base+0x0000000000028882 |
关于getdents64直接给出AI生成的板子
1 | shellcode= f""" |
最后将ROP_shellcode发送
1 | payload+=shellcode |
此时可以获得当前目录文件名 (远程环境关了, 我就本地演示了)
1 | zer00ne@zer00ne-VMware-Virtual-Platform:~/desktop/SUCTF/work$ python3 exp.py |
此时获得了文件名后就可以将shellcode修改为
1 | shellcode=asm(shellcraft.cat("flag_<hash>")) |