NTFS文件系统详解 之 文件定位

这篇博客详细介绍了如何从MBR扇区开始,逐步定位到NTFS分区的MFT,进而解析文件系统。通过理解MBR、DBR、EBR、MFT的概念和结构,作者逐步解析了文件记录和目录树,展示了如何遍历NTFS分区,最终达到读取特定文件内容的目的。内容包括MBR分区定位、NTFS分区的MFT分析、文件记录解析以及目录树的构建过程。
摘要由CSDN通过智能技术生成

一如既往的叨叨

    首先要对硬盘分区(MBR、GPT)和文件系统(NTFS、FAT32等)有一定的认识,要知道MBR扇区以及DBR扇区的基本结构,如果后面遇到不清楚的地方可以参考上一篇文章https://blog.csdn.net/hilavergil/article/details/79270379,如果觉得这个文章不行的话,Emmm...还有Google呢。

    接下来的代码目前只适用于MBR格式的分区,如果后面有时间的话再加上GPT的分区定位,实际上GPT的分区表更简洁明了一些。

    我尽量少用Windows的Api,从硬盘的MBR扇区(0扇区)开始逐步定位,确定每一个主分区和扩展分区的位置,如果是NTFS分区则定位到MFT,从根目录开始使用DFS进行遍历。

我们的目标是

    由于硬盘的分区定位在上面那篇文章已经写过了,因此这篇文章的重点会放在NTFS文件系统的解析上。我们从一个硬盘的MBR扇区开始,定位到一个NTFS分区,然后从NTFS分区的第一个扇区(Boot扇区)开始,逐步分析并定位到该分区中的每个目录和文件。

好了开始写正文

    前面那篇文章曾提到过,MBR扇区中的分区表只有四项,当分区数小于等于3的时候,所有的分区都属于主分区,安装操作系统的主分区也叫活动分区,这些在Windows的磁盘管理中都可以看到,MBR扇区中分区表的每一项直接指向该主分区的起始位置,也就是该主分区的DBR扇区。当分区数大于等于4的时候呢怎么办?一般来说这种情况下MBR扇区中分区表的第四项将会指向扩展分区,一个扩展分区可以包含多个逻辑分区,每个逻辑分区都有一个EBR扇区与之对应。而EBR扇区和MBR扇区的结构是一致的,但是在EBR扇区中仅用到了其分区表的前两项,其中第一项指向本逻辑分区的起始偏移(注意逻辑分区是存在于扩展分区内的,见下图),第二项指向下一个逻辑分区的EBR扇区。如此循环往复,你会发现实际上EBR扇区构成了一个逻辑分区的链表,链表的每一个节点都包含两项,第一项指向该EBR对应的逻辑分区,第二项指向下一个逻辑分区的EBR扇区。最后一个节点的第二项为零,表示此后不再有逻辑分区。

                   

                                                                   图1 名字可能不太对但结构应该没差.png

 

    当找到一个NTFS文件系统的分区之后,接下来就可以开始着手遍历目录树和文件了。你可能要问为什么不是FAT32或者其他的分区,因为我只看了NTFS……

 

第一步:读取NTFS的DBR扇区

    DBR扇区的结构在之前那片文章写过了,下面给个DBR的截图。

                            

                                                                         图2 DBR扇区

    图中红色方框里面的数据是MFT的偏移(小端字节序),即0X 00 00 00 00 00 0C 00 00,单位是簇,即MFT偏移786432个簇(相对于该分区的起始位置而言),蓝色方框里是每簇的扇区数0X08,黑色方框里是每扇区字节数0X 02 00,即512个字节。有了这些就可以算出MFT偏移的扇区数:786432 乘 8 等于 6291456个扇区,我们转到该分区的第 6291456扇区,就是MFT文件的第一个扇区了。接下来该分析MFT了。

 

第二步:解析MFT

    MFT是什么鬼?百度一下。

                         

                                                                                   图3 ???

    不好意思,进错片场了。

                       

                                                                            图4 MFT主文件表

    实际上MFT也是一个文件,在winhex中可以看到它:

                   

                                                                           图5 WinHex中的MFT文件

    也能看到MFT的偏移和我们之前计算的结果时一致的。MFT由一个个表项构成,每一个表项是一个文件记录,大小一般为1KB(两个扇区),记录着卷中每一个目录和文件的信息,每个文件记录的结构都是固定的,由文件记录头和若干属性构成。下面给一个截图:

                        

                                                                               图6 一个文件记录

    其中,文件记录头的结构定义如下:

// 文件记录头
typedef struct _FILE_RECORD_HEADER
{
	/*+0x00*/  BYTE Type[4];            // 固定值'FILE'
	/*+0x04*/  UINT16 USNOffset;        // 更新序列号偏移, 与操作系统有关
	/*+0x06*/  UINT16 USNCount;         // 固定列表大小Size in words of Update Sequence Number & Array (S)
	/*+0x08*/  UINT64 Lsn;               // 日志文件序列号(LSN)
	/*+0x10*/  UINT16  SequenceNumber;   // 序列号(用于记录文件被反复使用的次数)
	/*+0x12*/  UINT16  LinkCount;        // 硬连接数
	/*+0x14*/  UINT16  AttributeOffset;  // 第一个属性偏移
	/*+0x16*/  UINT16  Flags;            // flags, 00表示删除文件,01表示正常文件,02表示删除目录,03表示正常目录
	/*+0x18*/  UINT32  BytesInUse;       // 文件记录实时大小(字节) 当前MFT表项长度,到FFFFFF的长度+4
	/*+0x1C*/  UINT32  BytesAllocated;   // 文件记录分配大小(字节)
	/*+0x20*/  UINT64  BaseFileRecord;   // = 0 基础文件记录 File reference to the base FILE record
	/*+0x28*/  UINT16  NextAttributeNumber; // 下一个自由ID号
	/*+0x2A*/  UINT16  Pading;           // 边界
	/*+0x2C*/  UINT32  MFTRecordNumber;  // windows xp中使用,本MFT记录号
	/*+0x30*/  UINT16  USN;      // 更新序列号
	/*+0x32*/  BYTE  UpdateArray[0];      // 更新数组
} FILE_RECORD_HEADER, *pFILE_RECORD_HEADER;

    文件记录头后面就是属性了,属性由属性头和属性体构成,有如下几种类型:

                               

                                                                                    图7 属性类型

    属性有常驻属性非常驻属性之分,当一个属性的数据能够在1KB的文件记录中保存的时候,该属性为常驻属性;而当属性的数据无法在文件记录中存放,需要存放到MFT外的其他位置时,该属性为非常驻属性。常驻属性和非常驻属性的头部结构定义如下:

//常驻属性和非常驻属性的公用部分
typedef struct _CommonAttributeHeader {
	UINT32 ATTR_Type; //属性类型
	UINT32 ATTR_Size; //属性头和属性体的总长度
	BYTE ATTR_ResFlag; //是否是常驻属性(0常驻 1非常驻)
	BYTE ATTR_NamSz; //属性名的长度
	UINT16 ATTR_NamOff; //属性名的偏移 相对于属性头
	UINT16 ATTR_Flags; //标志(0x0001压缩 0x4000加密 0x8000稀疏)
	UINT16 ATTR_Id; //属性唯一ID
}CommonAttributeHeader,*pCommonAttributeHeader;

//常驻属性 属性头
typedef struct _ResidentAttributeHeader {
	CommonAttributeHeader ATTR_Common;
	UINT32 ATTR_DatSz; //属性数据的长度
	UINT16 ATTR_DatOff; //属性数据相对于属性头的偏移
	BYTE ATTR_Indx; //索引
	BYTE ATTR_Resvd; //保留
	BYTE ATTR_AttrNam[0];//属性名,Unicode,结尾无0
}ResidentAttributeHeader, *pResidentAttributeHeader;

//非常驻属性 属性头
typedef struct _NonResidentAttributeHeader {
	CommonAttributeHeader ATTR_Common;
	UINT64 ATTR_StartVCN; //本属性中数据流起始虚拟簇号 
	UINT64 ATTR_EndVCN; //本属性中数据流终止虚拟簇号
	UINT16 ATTR_DatOff; //簇流列表相对于属性头的偏移
	UINT16 ATTR_CmpSz; //压缩单位 2的N次方
	UINT32 ATTR_Resvd;
	UINT64 ATTR_AllocSz; //属性分配的大小
	UINT64 ATTR_ValidSz; //属性的实际大小
	UINT64 ATTR_InitedSz; //属性的初始大小
	BYTE ATTR_AttrNam[0];
}NonResidentAttributeHeader, *pNonResidentAttributeHeader;

    比较重要的几个属性如0X30文件名属性,其中记录着该目录或者文件的文件名;0X80数据属性记录着文件中的数据;0X90索引根属性,存放着该目录下的子目录和子文件的索引项;当某个目录下的内容比较多,从而导致0X90属性无法完全存放时,0XA0属性会指向一个索引区域,这个索引区域包含了该目录下所有剩余内容的索引项。

    比较重要的几个属性的结构定义将在后面给出。

    下面是一个90属性和A0属性的截图:

                         

                                                                        图8 90属性和A0属性

    从图上大致也能看出来,90属性中包含了2个目录:Program Files (x86) 和 Users,剩下的其他目录就在A0的属性体所指向的索引区了。索引的具体含义将在后面给出。

    前面说过,每一个文件记录都会对应一个目录或者文件,因此,如果我们按顺序读取每一个MFT表项,我们就能得到该卷中所有的文件和目录信息,其中还包括了部分被删除的文件和目录(被删除但未覆盖掉)。但我们的目的并不是目录和文件的简单罗列,我们要的是按照目录的树形结构去列出该卷的目录树,并且去定位某一个给定的文件,读出文件的数据。因此到这里还不够,继续向下。

    由于所有的目录和文件都在MFT中有对应的记录,因此,一个卷中目录和文件的数量越多,MFT的大小就越大,$MFT文件的大小是动态增长的,NTFS默认给MFT分配了该卷的12.5%的存储空间。MFT的前16项(元数据文件)是固定的(现在第九项$Quota基本上不存在了):

                              

                                                                                  图9 NTFS的元数据文件

 

    有了前面这些基础,接下来我们就能去解析目录树,定位文件位置啦!

    大致的思路如下:

    首先,MFT的第五项是卷的根目录的文件记录,见上图的$Root项,这是我们遍历的起点。其次,通过根目录的文件记录的90属性和A0属性,可以定位根目录下所有内容的索引项,而这些索引项又指向了它自己的文件记录项(MFT中的一项),该文件记录的90和A0属性又指向了它的子目录的索引项。所以呢。写个递归呀,我们的目录树就出来啦。这个树形结构大致上是这样的:

                             

                                                                                 图10 一棵树

    接下来以E盘中的E:\dir1_0\dir2_0\dir3_1\新建文本文档.txt为例,一步步详细的写出遍历的步骤,并读取出这个文本文档中的数据。

第三步:得到根目录的文件记录

    前面我们通过计算得到,MFT的偏移为6,291,456个扇区,而根目录的文件记录是MFT的第5项,一个MFT表项占2个扇区,所以根目录的文件记录的偏移为6,291,456 + 2 * 5 = 6,291,466个扇区。转到该扇区,可以看到根目录的文件记录如下:

                               

                                                                      图11 根目录的文件记录

    其中,30属性的属性体结构定义如下(不包含属性头):

//FILE_NAME 0X30属性体
typedef struct _FILE_NAME {
	UINT64 FN_ParentFR; /*父目录的MFT记录的记录索引。
							注意:该值的低6字节是MFT记录号,高2字节是该MFT记录的序列号*/
	FILETIME FN_CreatTime;
	FILETIME FN_AlterTime;
	FILETIME FN_MFTChg;
	FILETIME FN_ReadTime;
	UINT64 FN_AllocSz;
	UINT64 FN_ValidSz;//文件的真实尺寸
	UINT32 FN_DOSAttr;//DOS文件属性
	UINT32 FN_EA_Reparse;//扩展属性与链接
	BYTE FN_NameSz;//文件名的字符数
	BYTE FN_NamSpace;/*命名空间,该值可为以下值中的任意一个
						0:POSIX 可以使用除NULL和分隔符“/”之外的所有UNICODE字符,最大可以使用255个字符。注意:“:”是合法字符,但Windows不允许使用。
						1:Win32 Win32是POSIX的一个子集,不区分大小写,可以使用除““”、“*”、“?”、“:”、“/”、“<”、“>”、“/”、“|”之外的任意UNICODE字符,但名字不能以“.”或空格结尾。
						2:DOS DOS命名空间是Win32的子集,只支持ASCII码大于空格的8BIT大写字符并且不支持以下字符““”、“*”、“?”、“:”、“/”、“<”、“>”、“/”、“|”、“+”、“,”、“;”、“=”;同时名字必须按以下格式命名:1~8个字符,然后是“.”,然后再是1~3个字符。
						3:Win32&DOS 这个命名空间意味着Win32和DOS文件名都存放在同一个文件名属性中。*/
	BYTE FN_FileName[0];
}FILE_NAME,*pFILE_NAME;

    对照上图,可以得到根目录的文件名为 “ . ”。

第四步:计算根目录下索引项的偏移

    90属性的属性体由3部分构成:索引根索引头索引项。但是有些情况下90属性中是不存在索引项的(上图的90属性不包含索引项,图8中的90属性包含2个索引项),这个时候该目录的索引项由A0属性中的data runs指出。90属性体的结构如下(不包含属性头):

typedef struct _INDEX_HEADER {
	UINT32 IH_EntryOff;//第一个目录项的偏移
	UINT32 IH_TalSzOfEntries;//目录项的总尺寸(包括索引头和下面的索引项)
	UINT32 IH_AllocSize;//目录项分配的尺寸
	BYTE IH_Flags;/*标志位,此值可能是以下和值之一:
				  0x00       小目录(数据存放在根节点的数据区中)
				  0x01       大目录(需要目录项存储区和索引项位图)*/
	BYTE IH_Resvd[3];
}INDEX_HEADER,*pINDEX_HEADER;

//INDEX_ROOT 0X90属性体
typedef struct _INDEX_ROOT {
	//索引根
	UINT32 IR_AttrType;//属性的类型
	UINT32 IR_ColRule;//整理规则
	UINT32 IR_EntrySz;//目录项分配尺寸
	BYTE IR_ClusPerRec;//每个目录项占用的簇数
	BYTE IR_Resvd[3];
	//索引头
	INDEX_HEADER IH;
	//索引项  可能不存在
	BYTE IR_IndexEntry[0];
}INDEX_ROOT,*pINDEX_ROOT;

    由于这里90属性没有索引项,我们直接看A0属性。

    A0属性的属性体即为Data Runs,指向若干个索引区域的簇流(若干个物理上连续的簇称为一个簇流)。以上图的Data Runs为例:【11 01 2C 00 00 00 00 00】第一个字节【11】中的第二个1,表示接下来占用“1”个字节,用来存储该簇流占用簇的个数,即紧随其后的一个字节【01】,表示这个簇流占了0X01个簇的空间。第一个字节【11】中的第一个1,表示接下再占用“1”个字节,用来存储该簇流的偏移,即字节【2C】,表示偏移量为0X2C个簇。再往后一个字节为【00】,是结束标志。也就是说,该Data Runs只有一个簇流,而这个簇流包含一个簇,簇流的起始偏移为0X2C个簇,一个簇为8个扇区,即根目录所指向的索引区的偏移为44 * 8 = 352个扇区。

    下面再给一个多簇流的data runs:

                            

                                                                             图12 Data Runs

    十六进制的Data Runs 如下:31 05 F9 FF 0B 21 01 4E FF 11 01 12 31 01 12 CA F4 21 02 31 12 21 02 00 48 21 04 C9 0F 21 04 C9 59 31 04 87 11 01 21 08 57 10 00 02 A0 F8 FF FF。下面是对该Data Runs的解析:

                               

                                                                             图13 Data Runs解析

    从上图可以看出,该Data Runs一共包含了10个簇流,其中第一个簇流占用了5个簇,起始偏移为0X0BFFF9,即786425个簇;第二个簇流占用1个簇,相对于第一个簇流偏移【-178】个簇,即第二个簇流起始偏移为786425 – 178 = 786247个簇。接下来的簇流的计算方法是一样的。

第五步:读取索引项

    回到图11,我们计算出data runs指向的索引区的偏移为352个扇区,转到第352扇区,即索引区第一个扇区的位置,我们将看到如下数据(只截取了部分索引项):

                       

                                                                          图14 索引区域

    索引区域占用了若干个簇,每一个簇都包含了两部分:一个标准索引头和若干个标准索引项。这两部分的结构定义如下:

//标准索引头的结构
typedef struct _STD_INDEX_HEADER {
	BYTE SIH_Flag[4];  //固定值 "INDX"
	UINT16 SIH_USNOffset;//更新序列号偏移
	UINT16 SIH_USNSize;//更新序列号和更新数组大小
	UINT64 SIH_Lsn;               // 日志文件序列号(LSN)
	UINT64 SIH_IndexCacheVCN;//本索引缓冲区在索引分配中的VCN
	UINT32 SIH_IndexEntryOffset;//索引项的偏移 相对于当前位置
	UINT32 SIH_IndexEntrySize;//索引项的大小
	UINT32 SIH_IndexEntryAllocSize;//索引项分配的大小
	UINT8 SIH_HasLeafNode;//置一 表示有子节点
	BYTE SIH_Fill[3];//填充
	UINT16 SIH_USN;//更新序列号
	BYTE SIH_USNArray[0];//更新序列数组
}STD_INDEX_HEADER,*pSTD_INDEX_HEADER;

//标准索引项的结构
typedef struct _STD_INDEX_ENTRY {
	UINT64 SIE_MFTReferNumber;//文件的MFT参考号
	UINT16 SIE_IndexEntrySize;//索引项的大小
	UINT16 SIE_FileNameAttriBodySize;//文件名属性体的大小
	UINT16 SIE_IndexFlag;//索引标志
	BYTE SIE_Fill[2];//填充
	UINT64 SIE_FatherDirMFTReferNumber;//父目录MFT文件参考号
	FILETIME SIE_CreatTime;//文件创建时间
	FILETIME SIE_AlterTime;//文件最后修改时间
	FILETIME SIE_MFTChgTime;//文件记录最后修改时间
	FILETIME SIE_ReadTime;//文件最后访问时间
	UINT64 SIE_FileAllocSize;//文件分配大小
	UINT64 SIE_FileRealSize;//文件实际大小
	UINT64 SIE_FileFlag;//文件标志
	UINT8 SIE_FileNameSize;//文件名长度
	UINT8 SIE_FileNamespace;//文件命名空间
	BYTE SIE_FileNameAndFill[0];//文件名和填充
}STD_INDEX_ENTRY,*pSTD_INDEX_ENTRY;

    在标准索引头中,几个比较重要的数据如下:

        (1)索引项的偏移:即第一个标准索引项的偏移。在上图中为0X 00 00 00 40,这个偏移是相对于当前位置的偏移,即相对于字节【40】而言,偏移0X40个字节。

        (2)索引项的大小:表示这个簇中,有效的索引项和索引头的总字节数。在上图中为0X 00 00 0A 80,即2688个字节,超出该范围的索引项为无效的索引项。

    我们顺序读取每一个索引项,找到路径E:\dir1_0\dir2_0\dir3_1\新建文本文档.txt中目录dir1_0的索引项:

                         

                                                               图15 目录dir1_0的索引项

     标准索引项的结构已给出,其中几个重要的数据:

        (1)文件的MFT参考号:低6字节是目录或者文件对应的文件记录的编号,由于MFT是顺序存储的,根据该编号可以定位到该文件记录在MFT中的位置。在上图中,目录dir1_0的MFT参考号为0X 00 00 00 00 00 2A,即dir1_0的文件记录是MFT的第0X2A,即第42项。之前已计算出MFT的偏移为6,291,456扇区,每一项占用2个扇区,因此dir1_0的文件记录的偏移为6,291,456 + 2 * 42 = 6291540扇区。

        (2)索引项的长度:即本索引项占用的字节数,定义该索引项的边界。

第六步:现在可以递归啦

    我们转到第6291540扇区,即dir1_0的文件记录,按照先前的步骤,去解析90属性和A0属性,就能进入下一层目录,再去定位索引项,越来越深,直到列出该目录的树形结构。OK,还是慢慢来,dir1_0的文件记录如下:

 

                                                   图16

评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值