Qt/C++ 了解NTFS文件系统,解析盘符引导扇区数据获取MFT(Master File Table)主文件表偏移地址
系列文章目录
一、Qt/C++ 了解NTFS文件系统,了解MFT(Master File Table)主文件表(一)
二、Qt/C++ 了解NTFS文件系统,解析盘符引导扇区数据获取MFT(Master File Table)主文件表偏移地址
三、Qt/C++ 了解NTFS文件系统,获取首张MFT表数据,解析文件记录头内容找到第一个属性偏移地址
目录导读
- 系列文章目录
- 前言
- BOOTICE工具介绍
- 读取分区引导扇区(PBS)
前言
根据NT File System (NTFS) 一文中对引导扇区数据结构的分析,结合前文介绍的GitHub开源项目 NTFS-File-Search中的NTFS_BOOT_SECTOR结构体设计,使用Bootice工具查看分区引导扇区实际数据,计算首张MFT(Master File Table)主文件表在磁盘的偏移地址。
BOOTICE工具介绍
BOOTICE 引导修复工具是款USM的启动相关维护的工具。用于编辑修改磁盘上的引导扇区的信息,也就是MBR,BOOTICE可在磁盘(硬盘、移动硬盘、U盘、SD卡等)上安装磁盘引导程序。BOOTICE引导修复工具此外还具有磁盘扇区编辑、磁盘填充、分区管理等等功能。BOOTICE有着强大的兼容性,在硬盘维护中起来了很大的作用。
BOOTICE支持的引导程序主要有 WEE, GRUB4DOS, SYSLINUX, Plop BootManager, Ms-Dos 及 Windows NT 5/6 等。BOOTICE 还具有分区管理、扇区查看以及对 USB 移动存储设备进行重新格式化的功能。
具体参考 BOOTICE 1.3.4.0最新版
这里注意用于查看磁盘扇区数据,进行数据字节对照,
其中显示的数值都是16进制数据,以C盘目录为例:
读取分区引导扇区(PBS)
分区引导扇区是从NTFS文件系统格式 分区 的第一个扇区,
只有盘符是NTFS文件系统格式才能通过MFT表获取数据,
其他FAT,FAT32,REFS文件格式有其他的读取方式
如C盘的第一个扇区,以上面显示的C盘数据为例,
引导扇区在第878592个扇区。
读取整段扇区数据,到55 AA结束,一般都是512字节大小,
解析第一个扇区(引导扇区)数据,获取NTFS文件系统中第一个MFT表对应起始地址。
- NTFS引导扇区内容结构:
表格出自Windows NT文件系统(NT File System)[使用QQ浏览器翻译]
字节偏移量 | 段长度 | 平均数 | 字段名 | 目的 | |
---|---|---|---|---|---|
0x00 | 3字节 | 0xEB5290 | x86JMP和nototherwiseprovided(for) | 除非另有规定说明 导致在该引导扇区中的数据结构之后继续执行。 | |
0x03 | 8字节 | "NTFS "单词“NTFS”后跟四个尾随空格(0x20) | OEM ID | 这是一个神奇的数字,表明这是一个NTFS文件系统。 | |
0x0B | 2字节 | 0x0200 | BPB | 每扇区字节数 | 磁盘扇区中的字节数。 |
0x0D | 1字节 | 0x08 | BPB | 每簇扇区 | 簇中的扇区数量。如果该值大于0x80,则扇区数是2的绝对值的幂,认为该字段为负。 |
0x0E | 2字节 | 0x0000 | BPB | 未使用的保留扇区 | |
0x10 | 3字节 | 0x000000 | BPB | 不用的 | 该字段始终为0 |
0x13 | 2字节 | 0x0000 | BPB | NTFS未使用 | 该字段始终为0 |
0x15 | 1字节 | 0xF8 | BPB | 媒体描述符 | 驱动器的类型。0xF8用于表示硬盘驱动器(与几种大小的软盘不同)。 |
0x16 | 2字节 | 0x0000 | BPB | 不用的 | 该字段始终为0 |
0x18 | 2字节 | 0x003F | BPB | 每个磁道的扇区 | 驱动器磁道中的磁盘扇区数量。 |
0x1A | 2字节 | 0x00FF | BPB | 头数 | 驱动器上的磁头数。 |
0x1C | 4字节 | 0x000D6800 | BPB | 隐藏扇区 | 分区前的扇区数量。 |
0x20 | 4字节 | 0x00000000 | BPB | 不用的 | NTFS不使用 |
0x24 | 4字节 | 0x00800080 | EBPB | 不用的 | NTFS不使用 |
0x28 | 8字节 | 0x000000000C8007FF | EBPB | 总部门 | 扇区中的分区大小。 |
0x30 | 8字节 | 0x00000000000C0000 | EBPB | $MFT集群号 | 包含主文件表的群 |
0x38 | 8字节 | 0x0000000000000002 | EBPB | $MFTMirr群集号 | 包含主文件表备份的群集 |
0x40 | 1字节 | 0xF6 | EBPB | 每个文件记录段的字节或簇 | 正值表示文件记录段中簇的数量。负值表示文件记录段中的字节数,在这种情况下,大小是绝对值的2次方。(0xF6 = -10 → 210 = 1024). |
0x41 | 3字节 | 0x000000 | EBPB | 不用的 | NTFS不使用此字段 |
0x44 | 1字节 | 0x01 | EBPB | 每个索引缓冲区的字节或簇 | 正值表示索引缓冲区中的簇数量。负值表示字节数,它对负数使用与“每个文件记录段的字节数或簇数”相同的算法 |
0x45 | 3字节 | 0x000000 | EBPB | 不用的 | NTFS不使用此字段 |
0x48 | 8字节 | 0x86EEA7DEEEA7C4B1 | EBPB | 卷序列号 | 分配给该分区的唯一随机数,以保持有序。 |
0x50 | 4字节 | 0x00000000 | 校验和,未使用 | 应该是校验和。 | |
0x54 | 426字节 | … | 引导代码 | 加载操作系统其余部分的代码。这由该扇区的前3个字节指向。 | |
0x01FE | 2字节 | 0xAA55 | 扇区结束标记 此标志表示这是一个有效的引导扇区。 |
需要注意的是:磁盘扇区数据中除字符串外的所有值都以小端序(little endian)存储,如
磁盘大小为0x000000000C8007FF字节
但是数据格式是按FF 07 80 0C 00 00 00 00顺序存储
大端模式(Big_endian):字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。
小端模式(Little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中。
参考:
Big-endian and Little-endian (大小端)
字节序:Big Endian 和 Little Endian
- 查看实际扇区数据:
使用BOOTICE工具查看磁盘扇区实际数据,并按上面表格占用字节划分标注;
结合引导分区结构表格与盘符数据进行对照,就可以设计一个512字节的C++结构体,来获取引导分区数据:
例如 GitHub开源项目 NTFS-File-Search 设计的NTFS_BOOT_SECTOR 结构
- NTFS_BOOT_SECTOR结构体
/*
* BIOS Parameter Block (BPB) - BIOS参数块(BPB)
* https://en.wikipedia.org/wiki/NTFS
* https://en.wikipedia.org/wiki/BIOS_parameter_block
* https://www.ntfs.com/ntfs-partition-boot-sector.htm
* NTFS文件系统详解(三)之NTFS元文件解析(https://blog.csdn.net/enjoy5512/article/details/50966009/)
*/
typedef struct NTFS_BOOT_SECTOR
{
/* Jump instruction */
/* 跳转指令 EB 52 90*/
BYTE Jmp[3];
/* Signature*/
/* 文件系统的ASSIIC码表示形式 NTFS*/
BYTE Signature[8];
//
// BPB and extended BPB
//
//!字节2 每个扇区的字节总数 一般是00 02H (一般后面描述加H表示十六进制)
WORD BytesPerSector;
//!字节1 簇大小 08 每个簇占8个扇区
BYTE SectorsPerCluster;
//!字节2 保留扇区
WORD ReservedSectors;
//!字节3 总为0
BYTE Zeros1[3];
//!字节2 不使用
WORD Unused1;
//!字节1 介质描述,硬盘为F8
BYTE MediaDescriptor;
//!字节2 总为0
WORD Zeros2;
//!字节2 每磁头扇区数
WORD SectorsPerTrack;
//!字节2 每柱面磁头数
WORD NumberOfHeads;
//!字节4 隐含扇区数 (从MBR到DBR的扇区总数)
DWORD HiddenSectors;
//!字节4 不使用
DWORD Unused2;
//!字节4 不使用 总为80 00 80 00
DWORD Unused3;
//!字节8 扇区总数,即分区大小
ULONGLONG TotalSectors;
//!字节8 $MFT的开始簇号
ULONGLONG MFT_LCN; /* $MFT Logical Cluster Number (LCN) - $MFT逻辑簇数(LCN) */
//!字节8 $MFTMirr的开始簇号
ULONGLONG MFTMirr_LCN; /* $MFTMirr Logical Cluster Number (LCN) - $MFTMirr 逻辑集群号(LCN) */
//!字节4 每个MFT记录的簇数
DWORD ClustersPerFileRecord;
//!字节4 每索引的簇数
DWORD ClustersPerIndexBlock;
//! 分区的逻辑序列号
BYTE VolumeSN[8];
/* Boot Code - 引导代码 */
BYTE BootCode[430];
//
// 0xAA55
//
BYTE _AA;
BYTE _55;
};
注意使用
#pragma pack( push, 1 )
#pragma pack( pop )
进行字节对齐。
学习C/C++源码案例的时候,pragma pack是经常会遇到
参考#pragma pack 详解
- 读取引导分区数据
读取引导分区数据转换成指定结构体,获取首个MFT表地址
qDebug()<<" Start ---->";
//!磁盘第一个512字节结构 BIOS参数
NTFS_BOOT_SECTOR m_BootRecord;
const WCHAR lpszVolumeName[7] = { L'\\', L'\\', L'.', L'\\', L'C', L':' };
HANDLE m_hVolume= CreateFile(lpszVolumeName,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
NULL
);
if (m_hVolume == INVALID_HANDLE_VALUE)
goto out ;
DWORD dwNumberOfBytesRead;
if (ReadFile(m_hVolume, &m_BootRecord, NTFS_BOOTSECTOR_SIZE, &dwNumberOfBytesRead, NULL))
{
if (dwNumberOfBytesRead == NTFS_BOOTSECTOR_SIZE)
{
qDebug()<<"[磁盘扇区中的字节数] : "<<QString::number(m_BootRecord.BytesPerSector,16).toUpper()<<" -> "<<QString::number(m_BootRecord.BytesPerSector,10);
qDebug()<<"[簇中的扇区数量] : "<<QString::number(m_BootRecord.SectorsPerCluster,16).toUpper()<<" -> "<<QString::number(m_BootRecord.SectorsPerCluster,10);
qDebug()<<"[每磁头扇区数] : "<<QString::number(m_BootRecord.SectorsPerTrack,16).toUpper()<<" -> "<<QString::number(m_BootRecord.SectorsPerTrack,10);
qDebug()<<"[每柱面磁头数] : "<<QString::number(m_BootRecord.NumberOfHeads,16).toUpper()<<" -> "<<QString::number(m_BootRecord.NumberOfHeads,10);
qDebug()<<"[分区大小] : "<<QString::number(m_BootRecord.TotalSectors,16).toUpper()<<" -> "<<QString::number(m_BootRecord.TotalSectors,10);
qDebug()<<"[$MFT的开始簇号] : "<<QString::number(m_BootRecord.MFT_LCN,16).toUpper()<<" -> "<<QString::number(m_BootRecord.MFT_LCN,10);
qDebug()<<"[$MFTMirr的开始簇号] : "<<QString::number(m_BootRecord.MFTMirr_LCN,16).toUpper()<<" -> "<<QString::number(m_BootRecord.MFTMirr_LCN,10);
}
}
CloseHandle(m_hVolume);
out:
qDebug()<<" End ---->";
/*
QCoreApplication Start ---->
[磁盘扇区中的字节数] : "200" -> "512"
[簇中的扇区数量] : "8" -> "8"
[每磁头扇区数] : "3F" -> "63"
[每柱面磁头数] : "FF" -> "255"
[分区大小] : "C8007FF" -> "209717247"
[$MFT的开始簇号] : "C0000" -> "786432"
[$MFTMirr的开始簇号] : "2" -> "2"
QCoreApplication End ---->
*/
由此得到首个MFT在簇号 786432 位置,
计算首张MFT表磁盘地址字节偏移量MFT_LCN:
UINT64 MFT_LCN= (UINT64)(m_BootRecord.MFT_LCN * m_BootRecord.BytesPerSector * m_BootRecord.SectorsPerCluster);
MFT偏移簇号786432
7864328512= “3221225472” =“0XC0000000”
计算出的地址通过ReadFile设置偏移量读取出来确实是MFT结构表数据,
但是这在其他文章中计算方式又不一样:
- 【1】.MFT偏移簇号786432
(786432∗4096(偏移)+63∗512(C盘偏移地址)=3221257728=0xC0007E00)
出自 NTFS文件系统详解
- 【2】.文件记录由两部分构成,一部分是文件记录头,另一部分是属性列表,最后结尾是四个“FF”。然后我们根据上面BPB中的偏移簇号偏移盘偏移地址找到系统MFT所在地址 (789632 * 8 +63 = 6291519),
出自 NTFS文件系统详解(三)之NTFS元文件解析
- 【3】.在NTFS文件系统中,每个文件记录都有一个唯一的标识符,称为文件记录号(File Record Number,简称FRN)。要访问某个文件记录,需要先找到该文件记录在$MFT元文件中的偏移地址。计算偏移地址的公式如下:
$MFTOffset = MFTStartCluster * ClusterSize + FRN * RecordSize
其中,MFTStartCluster是$MFT元文件的起始簇号,ClusterSize是簇的大小,FRN是文件记录号,RecordSize是文件记录的大小。
出自 「NTFS:让你的硬盘更安全、更高效!」NTFS文件系统详解
带入上面【1】,【2】的公式,我读取的数据反而是错误的,而C://盘符的文件记录号一般为5固定值,带入计算也错误,带入0计算正常,不知道是不是NTFS版本的问题,后面还是以NTFS-File-Search 示例中的计算方式为准。
下一篇:Qt/C++ 了解NTFS文件系统,获取首张MFT表数据,解析文件记录头内容找到第一个属性偏移地址