资讯动态

从d3ctf_2019_ezfile看FILE结构体与文件描述符的漏洞利用

发布时间:2026/10/14 18:05:23 来源:尧图企业网站定制
1. 从一道经典题目看文件流底层机制1.1 为什么这个题目值得反复研究做二进制安全方向的朋友大概都有这样的体会很多看似复杂的漏洞利用最终都归结到对某个底层机制的深刻理解上。d3ctf_2019_ezfile这道题就是典型代表——它表面上是文件读写逻辑的考察实际上考的是对 C 标准库中FILE结构体与文件描述符之间关系的理解深度。我第一次接触这道题的时候第一反应是去找栈溢出、格式化字符串这些常规入口结果绕了一大圈才发现真正的突破口在于fileno这个平时不太起眼的函数。这道题的核心价值不在于它有多难而在于它揭示了一个容易被忽视的事实标准 I/O 库的缓冲机制和底层文件描述符之间存在着可以被利用的间隙。这道题适合已经掌握基本栈溢出、堆利用技巧但对文件流内部机制还不够熟悉的进阶学习者。如果你正在从 CTF 新手向中阶过渡或者想深入理解FILE结构体在漏洞利用中的价值这道题会给你很多启发。接下来我会从设计思路、核心机制、实操复现、问题排查几个维度把这道题彻底拆开讲清楚。1.2 题目场景还原与核心需求这道题模拟的是一个简易的文件管理系统程序提供了创建文件、写入内容、读取内容、删除文件等基本操作。从用户视角看它就是一个功能简陋的记事本类应用。但程序内部使用了FILE *指针数组来管理多个文件流每个文件流通过fopen打开通过fclose关闭。题目的关键设计在于程序在删除文件时只调用了fclose关闭了FILE流但没有将指针数组中的对应位置置空。这就留下了一个典型的use-after-free隐患——被关闭的FILE结构体内存被释放后如果还能通过残留的指针访问它就可能触发各种异常行为。而fileno函数的巧妙之处在于它返回的是FILE结构体中保存的文件描述符。当FILE结构体被释放后这块内存可能被其他数据覆盖此时fileno返回的值就不再是原来的文件描述符而可能是攻击者可控的数据。如果程序后续用这个被污染的文件描述符去执行读写操作就可能实现任意地址读写。注意这里说的“任意地址读写”并不是直接读写内存地址而是通过文件描述符操作间接影响文件系统层面的行为进而配合其他机制实现更复杂的利用。2. FILE结构体与文件描述符的绑定关系2.1 FILE结构体里到底存了什么要理解这道题的利用原理必须先搞清楚FILE结构体的内部布局。在 glibc 中FILE结构体实际类型是struct _IO_FILE包含了一系列用于管理缓冲区和文件状态的字段。其中和本题最相关的是_fileno字段它保存了通过open系统调用获得的文件描述符。一个典型的FILE结构体在内存中的布局大致如下以 64 位系统为例字段名偏移量说明_flags0x00文件状态标志_IO_read_ptr0x08读缓冲区当前指针_IO_read_end0x10读缓冲区结束指针_IO_read_base0x18读缓冲区起始指针_IO_write_base0x20写缓冲区起始指针_IO_write_ptr0x28写缓冲区当前指针_IO_write_end0x30写缓冲区结束指针_IO_buf_base0x38缓冲区起始地址_IO_buf_end0x40缓冲区结束地址_chain0x68链表指针指向下一个FILE_fileno0x70文件描述符_lock0x88锁指针fileno函数做的事情非常简单就是返回_fileno字段的值。正常情况下这个值是一个小的非负整数对应进程打开的文件描述符表中的一个条目。但当FILE结构体被释放后这块内存如果被重新分配并写入攻击者可控的数据_fileno字段就可能变成一个很大的值或者特定的值。2.2 文件描述符的本质与分配规律文件描述符本质上是进程文件描述符表的下标。每个进程启动时默认打开三个描述符0标准输入、1标准输出、2标准错误。后续每次调用open或fopen系统会分配当前最小的可用描述符。这个分配规律非常关键。假设程序先打开了文件 A、B、C它们分别获得描述符 3、4、5。然后关闭了 B描述符 4 被释放。此时如果再打开一个新文件 DD 会获得描述符 4而不是 6。这种“最小可用”的分配策略是理解后续利用链的基础。在ezfile这道题中程序维护了一个FILE *数组。假设数组大小为 10用户可以通过索引操作这些文件。当用户删除某个文件时程序调用fclose关闭流但数组中的指针仍然指向那块已经被释放的内存。此时如果用户再次通过该索引进行读写操作程序会使用一个悬垂指针。2.3 fileno返回值被污染后的连锁反应当FILE结构体内存被释放后它会进入堆管理器的空闲链表。如果此时程序还有其他堆分配操作这块内存可能被重新分配给其他数据结构。攻击者可以通过精心构造的输入让这块内存中的_fileno字段变成特定的值。假设攻击者能够控制_fileno的值为一个较大的数字比如 0x100。当程序调用fileno获取这个值后如果后续用这个值去执行read或write系统调用系统会检查该描述符是否有效。如果无效操作会失败并返回错误。但如果攻击者能够通过某种方式让这个描述符对应到进程中的其他打开文件就可能实现非预期的文件访问。更进一步的利用思路是通过控制_fileno的值配合dup2等描述符操作可能实现描述符的重新映射。比如将某个高编号描述符映射到标准输入或标准输出从而劫持程序的输入输出流。实操心得在分析这类题目时不要只盯着_fileno一个字段。FILE结构体中的缓冲区指针_IO_buf_base、_IO_buf_end等同样重要。如果能控制这些指针配合fread/fwrite操作可能实现更直接的任意地址读写。3. 利用链的完整构建过程3.1 第一步制造悬垂指针利用的起点是制造一个悬垂的FILE *指针。在ezfile中删除操作只调用了fclose没有将数组对应位置置为NULL。这就意味着删除后数组中仍然保存着指向已释放FILE结构体的指针。// 伪代码示意 void delete_file(int index) { if (files[index] ! NULL) { fclose(files[index]); // 缺少 files[index] NULL; } }这个遗漏是整条利用链的基石。在实际的漏洞挖掘中这种“释放后未置空”的模式非常常见尤其是在快速开发的项目中。很多开发者认为fclose之后指针就无效了但实际上指针的值仍然存在只是指向的内存已经归还给堆管理器。3.2 第二步堆风水与内存布局控制制造出悬垂指针后下一步是控制这块被释放内存的内容。这需要用到堆风水技术——通过一系列精心安排的分配和释放操作让目标内存块被特定的数据覆盖。在ezfile的场景下程序本身提供了写入文件内容的功能。如果被释放的FILE结构体内存恰好被重新分配为某个文件的内容缓冲区那么攻击者就可以通过写入文件内容来覆盖_fileno字段。具体操作步骤大致如下创建文件 A获得描述符 3对应FILE结构体在堆地址 X。创建文件 B获得描述符 4对应FILE结构体在堆地址 Y。删除文件 AFILE结构体 X 被释放但数组指针仍指向 X。创建一个新文件 C其内容缓冲区可能被分配到地址 X。向文件 C 写入精心构造的数据覆盖地址 X 处的_fileno字段。通过数组中的悬垂指针访问“文件 A”此时fileno返回被污染的值。这个过程中堆分配器的具体行为如 tcache、fastbin 的分配策略会影响最终的布局效果。在 glibc 2.27 及之后的版本中tcache 的引入让堆风水变得更加可控但也增加了一些限制。3.3 第三步利用被污染的fileno当_fileno被污染后程序后续对该文件流的操作会使用这个伪造的描述符。在ezfile中读取文件内容的操作大致是void read_file(int index) { char buf[0x100]; fread(buf, 1, 0x100, files[index]); // 输出buf内容 }fread内部会使用_fileno对应的描述符执行read系统调用。如果这个描述符恰好对应到进程中的另一个文件比如某个包含敏感信息的文件就可能读取到非预期的内容。更巧妙的利用方式是通过控制_fileno的值让它指向一个由攻击者控制的文件描述符。比如攻击者可以先打开一个文件获得描述符 N然后通过堆风水让悬垂FILE的_fileno变成 N。这样对“已删除文件”的读写操作实际上作用在了攻击者控制的文件上。3.4 第四步从文件操作到代码执行单纯的任意文件读写还不足以完成利用。在 CTF 题目中通常需要进一步获取 shell 或读取 flag。从文件操作到代码执行的常见路径包括覆盖关键配置文件如果程序后续会读取某个配置文件并执行其中的命令可以通过文件写操作篡改配置。利用 /proc/self/mem在某些环境下可以通过文件描述符操作访问进程自身的内存空间。配合其他漏洞文件读写可能只是利用链的一环需要结合栈溢出、格式化字符串等其他漏洞完成最终利用。在ezfile这道题中常见的解法是通过控制_fileno实现文件描述符的混淆进而读取到 flag 文件的内容。具体的 flag 路径和读取方式需要根据题目环境调整。注意事项在实际操作中堆风水的成功率受很多因素影响包括 glibc 版本、程序之前的分配历史、环境变量等。建议在本地调试时使用与远程一致的环境必要时可以通过多次尝试或调整分配顺序来提高成功率。4. 调试技巧与常见问题排查4.1 如何确认FILE结构体的内存布局在调试这类题目时第一步永远是确认FILE结构体在内存中的实际布局。不同 glibc 版本的_IO_FILE结构体可能有细微差异_fileno的偏移量也可能不同。确认方法很简单在fopen之后用调试器查看返回的FILE *指针指向的内存找到_fileno字段的位置。可以通过以下方式快速定位# 在gdb中 p/x *(struct _IO_FILE *)files[0] # 或者直接查看内存 x/20gx files[0]通常_fileno的值会是一个小的整数如 3、4、5在内存中很容易识别。记录下这个偏移量后续构造 payload 时需要精确覆盖这个位置。4.2 堆风水失败的常见原因堆风水是这类利用中最容易出问题的环节。以下是我在实际操作中遇到过的几种典型失败情况问题现象可能原因解决思路覆盖后fileno值不对内存块被其他数据占用调整分配顺序增加 padding程序崩溃在fread_IO_buf_base等指针被破坏只覆盖_fileno保持其他字段不变读取内容为空描述符无效或文件为空确认描述符对应的文件有内容远程成功本地失败环境差异导致堆布局不同使用 Docker 复现远程环境其中最常见的问题是覆盖范围过大破坏了FILE结构体中的其他关键字段。fread和fwrite不仅依赖_fileno还依赖缓冲区指针和标志位。如果这些字段被破坏程序会在执行系统调用前就崩溃。4.3 精确覆盖fileno的实操方法为了避免破坏其他字段推荐使用“精确覆盖”策略。具体做法是先通过调试确定_fileno相对于FILE结构体起始地址的偏移通常是 0x70。在构造写入数据时只在对应偏移处写入目标值其他位置保持原值或写入无害数据。如果写入操作是从缓冲区起始位置开始的需要在 payload 前面填充足够的字节。假设_fileno偏移为 0x70目标描述符为 5payload 构造如下payload bA * 0x70 p32(5) b\x00 * (buffer_size - 0x74)这里用p32(5)写入 4 字节的描述符值后面用空字节填充。注意_fileno是int类型占 4 字节但结构体对齐可能导致后面有 4 字节的填充。实操心得在写入 payload 时尽量保持FILE结构体中_flags字段不变。这个字段控制文件流的读写模式如果被破坏后续的fread/fwrite可能直接失败。可以在覆盖前先读取原始值在 payload 中保留。4.4 验证利用是否成功的检查点在每一步操作后都需要验证当前状态是否符合预期。以下是我常用的检查清单悬垂指针确认删除文件后用调试器查看数组中的指针是否仍然非空。内存复用确认创建新文件后查看新分配的缓冲区地址是否与之前释放的FILE结构体地址重叠。fileno污染确认通过悬垂指针调用fileno查看返回值是否变为预期值。读写效果确认执行读取操作后检查输出内容是否来自目标文件。如果某一步不符合预期需要回退到上一步重新调整。这个过程可能需要多次迭代尤其是在堆布局比较复杂的情况下。5. 从这道题延伸出的通用利用思路5.1 文件流相关漏洞的常见模式ezfile展示的只是文件流漏洞的一种模式。在实际的漏洞挖掘中与FILE结构体相关的利用方式还有很多FSOPFile Stream Oriented Programming通过控制FILE结构体中的虚表指针和函数指针在fclose、fflush等操作时劫持控制流。缓冲区指针劫持控制_IO_buf_base和_IO_buf_end让fread/fwrite读写任意地址。链式结构利用FILE结构体通过_chain字段形成链表控制这个字段可能影响程序的退出流程。这些利用方式的共同点是都需要对FILE结构体的内部布局有清晰的认识并且能够通过某种方式控制这块内存的内容。5.2 堆风水在文件流利用中的核心地位无论是ezfile还是更复杂的 FSOP堆风水都是绕不开的环节。文件流利用的本质是“用可控数据覆盖敏感结构体”而堆风水就是实现这种覆盖的手段。做好堆风水的关键在于理解目标程序的分配模式。在ezfile中分配主要来自两个方面fopen创建的FILE结构体和文件内容缓冲区。这两种分配的大小不同生命周期也不同为堆风水提供了操作空间。一个实用的技巧是在分析阶段用调试器记录每次分配和释放的地址、大小画出堆的内存变化图。这样在构造利用时就能预判某次分配会落在哪个地址从而提高成功率。5.3 这类题目的学习路径建议如果你刚接触文件流相关的漏洞建议按以下路径循序渐进基础阶段理解FILE结构体的基本布局掌握fopen、fclose、fread、fwrite的底层实现。进阶阶段学习堆风水的基本原理能够通过调试观察堆的分配和释放过程。实战阶段从ezfile这类简单题目入手逐步尝试 FSOP 等更复杂的利用技术。拓展阶段研究不同 glibc 版本中FILE结构体的变化以及相应的利用技巧调整。每道题做完后建议写一份详细的 writeup记录利用思路、调试过程和踩过的坑。这些记录在遇到类似题目时会非常有价值。我在实际做这类题目时最大的体会是不要急于写 exploit先花时间把程序的逻辑和内存布局搞清楚。很多时候利用失败不是因为技术不够而是因为对目标的理解不够深入。把FILE结构体的每个字段都搞清楚把堆的分配规律摸透利用链自然就清晰了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑