Qt/C++ 了解NTFS文件系统,解析盘符引导扇区数据获取MFT(Master File Table)主文件表偏移地址

系列文章目录

整个专栏系列是根据GitHub开源项目NTFS-File-Search获取分区所有文件/目录列表的思路。
具体的如下:

  1. Qt/C++ 了解NTFS文件系统,了解MFT(Master File Table)主文件表(一)
    介绍NTFS文件系统,对比通过MFT(Master File Table)主文件表获取数据的优劣,简单介绍开源项目NTFS-File-Search,以及了解借鉴NTFS系统中所参考的所有文章。
  2. Qt/C++ 了解NTFS文件系统,解析盘符引导扇区数据获取MFT(Master File Table)主文件表偏移地址
    读取$Boot引导分区扇区数据,获取单个簇大小(4096字节),$MFT元数据大小(1024字节)和$MFT元数据的起始簇号,计算出$MFT元数据在磁盘的偏移地址。
  3. Qt/C++ 了解NTFS文件系统,获取首张MFT表数据,解析文件记录头内容找到第一个属性偏移地址
    解析$MFT元数据结构,根据$MFT元数据文件记录头获取属性的偏移地址,用于后面解析0x80 $Data属性,获取Run List数据列表
  4. Qt/C++ 了解NTFS文件系统,解析MFT主文件表中的常驻属性与非常驻属性
    简单介绍$MFT元数据中的常驻属性与非常驻属性结构.
  5. Qt/C++ 了解NTFS文件系统,解析0x80 $Data属性,获取Run List数据列表
    根据0x80 $Data属性,找到存放所有$MFT元数据的区间列表(Run List数据列表)
  6. Qt/C++ 了解NTFS文件系统,遍历Run Lists数据列表,读取0x30 $FILE_NAME属性,获取所有文件/目录数据
    根据前面获取的 获取Run List数据列表,
    遍历所有Run List数据列表读取所有$MFT元数据,并解析 $FILE_NAME属性和$STANDARD_INFORMATION属性获取文件或目录的实际大小,名称,磁盘分配大小,记录号 ,文件属性等信息


前言

根据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浏览器翻译]

字节偏移量段长度平均数字段名目的
0x003字节0xEB5290x86JMP和nototherwiseprovided(for)除非另有规定说明 导致在该引导扇区中的数据结构之后继续执行。
0x038字节"NTFS    "单词“NTFS”后跟四个尾随空格(0x20)OEM ID这是一个神奇的数字,表明这是一个NTFS文件系统。
0x0B2字节0x0200BPB每扇区字节数磁盘扇区中的字节数。
0x0D1字节0x08BPB每簇扇区簇中的扇区数量。如果该值大于0x80,则扇区数是2的绝对值的幂,认为该字段为负。
0x0E2字节0x0000BPB未使用的保留扇区
0x103字节0x000000BPB不用的该字段始终为0
0x132字节0x0000BPBNTFS未使用该字段始终为0
0x151字节0xF8BPB媒体描述符驱动器的类型。0xF8用于表示硬盘驱动器(与几种大小的软盘不同)。
0x162字节0x0000BPB不用的该字段始终为0
0x182字节0x003FBPB每个磁道的扇区驱动器磁道中的磁盘扇区数量。
0x1A2字节0x00FFBPB头数驱动器上的磁头数。
0x1C4字节0x000D6800BPB隐藏扇区分区前的扇区数量。
0x204字节0x00000000BPB不用的NTFS不使用
0x244字节0x00800080EBPB不用的NTFS不使用
0x288字节0x000000000C8007FFEBPB总部门扇区中的分区大小。
0x308字节0x00000000000C0000EBPB$MFT集群号包含主文件表的群
0x388字节0x0000000000000002EBPB$MFTMirr群集号包含主文件表备份的群集
0x401字节0xF6EBPB每个文件记录段的字节或簇正值表示文件记录段中簇的数量。负值表示文件记录段中的字节数,在这种情况下,大小是绝对值的2次方。(0xF6 = -10 → 210 = 1024).
0x413字节0x000000EBPB不用的NTFS不使用此字段
0x441字节0x01EBPB每个索引缓冲区的字节或簇正值表示索引缓冲区中的簇数量。负值表示字节数,它对负数使用与“每个文件记录段的字节数或簇数”相同的算法
0x453字节0x000000EBPB不用的NTFS不使用此字段
0x488字节0x86EEA7DEEEA7C4B1EBPB卷序列号分配给该分区的唯一随机数,以保持有序。
0x504字节0x00000000校验和,未使用应该是校验和。
0x54426字节引导代码加载操作系统其余部分的代码。这由该扇区的前3个字节指向。
0x01FE2字节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表数据,解析文件记录头内容找到第一个属性偏移地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

得鹿梦鱼、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值