如何编程读取磁盘空闲空间中留存的已删除文件原始字节数据
核心实现逻辑
你要的空闲空间无结构字节流,核心实现两步即可:
- 获取目标磁盘/分区所有已分配块的偏移区间,排除这些区间后剩下的就是未分配(空闲)空间的区间列表
- 按偏移顺序读取原始块设备中所有未分配区间的字节,拼接后就是你需要的纯字节流
macOS + APFS 实现方案
操作需要root权限,且需要先关闭SIP(系统完整性保护)才能读取原始块设备。
- 先通过
diskutil apfs list获取目标APFS卷的块设备路径(形如/dev/disk1s2)和卷块大小(APFS默认块大小为4096字节) - 分配状态获取有两种可选方式:
- 手动解析APFS结构:读取卷起始位置的超级块,找到分配位图的存储位置,遍历位图每一位,标记为
0的就是未分配块,整理出所有未分配块的偏移区间 - 借助成熟工具输出:直接调用
sleuthkit套件的blkstat命令,解析输出即可直接拿到所有未分配块的偏移和长度,不需要手动解析文件系统结构
- 手动解析APFS结构:读取卷起始位置的超级块,找到分配位图的存储位置,遍历位图每一位,标记为
- 读取未分配空间的C语言简化示例:
#include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { // 替换为你的目标块设备路径 int disk_fd = open("/dev/disk1s2", O_RDONLY); if (disk_fd < 0) { perror("打开磁盘失败,请确认有root权限且SIP已关闭"); return 1; } // 这里替换为你拿到的未分配区间:起始偏移、长度 off_t free_offset = 1048576; size_t free_len = 4096 * 200; char *buf = malloc(free_len); if (!buf) { perror("内存分配失败"); close(disk_fd); return 1; } lseek(disk_fd, free_offset, SEEK_SET); ssize_t read_cnt = read(disk_fd, buf, free_len); if (read_cnt != free_len) { perror("读取磁盘失败"); } else { // 这里将buf写入输出文件,即可得到对应区间的原始字节 FILE *out = fopen("./free_space_dump.bin", "wb"); fwrite(buf, 1, free_len, out); fclose(out); } free(buf); close(disk_fd); return 0; }
注意:APFS的快照占用的块会被标记为已保留,如果你需要读取快照关联的残留数据,需要额外解析快照的块分配表。
Linux 平台实现(适配ext4/xfs/btrfs等)
操作需要root权限,块设备路径形如/dev/sda1。
- ext4文件系统可以直接用
ext2fs开源库,调用ext2fs_block_iterate2遍历所有已分配块,剩余区间就是未分配空间,不需要手动解析块组、块位图结构 - btrfs、zfs这类写时复制文件系统,建议直接用
sleuthkit的接口获取分配状态,避免手动处理快照、子卷的复杂分配逻辑
Windows 平台实现(适配NTFS/exFAT/FAT32)
操作需要管理员权限。
- NTFS直接调用系统API即可:用
CreateFile打开卷路径(形如\\.\C:),调用DeviceIoControl传入控制码FSCTL_GET_VOLUME_BITMAP,直接获取整个卷的簇分配位图,每一位对应一个簇的分配状态,0就是未分配簇 - 拿到未分配簇的偏移和长度后,直接调用
ReadFile读取对应位置的字节即可 - exFAT/FAT32可以直接读取FAT表,遍历所有簇的分配标记,未标记为已用的就是未分配簇
通用注意事项
- 尽量操作已卸载或者只读挂载的卷,避免读写状态下操作系统动态分配块,导致你读取的空闲空间数据和实际状态不一致
- 不要在运行中的系统盘上做写入类操作,避免误操作损坏现有文件
- 如果不需要兼容老旧文件系统,直接复用
sleuthkit的成熟接口,可以大幅降低跨平台移植的工作量
内容的提问来源于stack exchange,提问作者mistertim
相关产品推荐
相关产品推荐

