刘收获

导航

NTFS文件定位和HIVE文件解析——1.NTFS文件定位

0x00 前言

  关于NTFS文件系统等知识点的介绍,网上诸多资料,这里不再做过多介绍了,本文仅作为笔者学习NTFS文件系统结构以及HIVE文件结构的总结实践(win7 x64)。

 

0x01 从MBR定位到HIVE文件

   首先通过winhex手动定位到“system32\config\system”文件,而要定位文件,就需要ntfs的MFT主文件表,首先要找到MFT主文件表的起始位置,也就是它的起始簇号。

   打开物理磁盘,就可以看到作为整个硬盘的第一个扇区的MBR,DPT分区表,先定位到0x1be处的DPT分区表的第一个表项(选中的那16个字节,每个表项16字节),右侧贴图为DPT分区表项各字段含义,可以看到第一个分表项就是活动分区(第一个字节值为0x80,代表活动分区),因此再定位到表项的第九个字节,取一个DWORD,这个值就是分区引导扇区DBR(系统启动时MBR要移交引导权给它的那个扇区),的起始扇区了:

    

 

   OK,那么直接起始扇区 * 扇区大小0x800 *0x200 = 0x100000,定位到分区引导扇区DBR康康,这里需要关注DBR中的三个字段,每个扇区的字节总数、簇大小、$MFT起始簇的簇号:

       

 

   那么根据分区引导扇区DBR,直接再定位到MFT起始簇康康,偏移地址为,MFT起始簇的簇号 * 簇大小 0xC0000*0x1000 = 0xC000 0000,

   转到0xC000 0000偏移地址处,看到了“FILE”标志,说明这个MFT主文件表位置没找错:

   这里再介绍几句MFT主文件表:MFT作为NTFS卷的核心,被实现为一个文件记录数组,每个文件记录的大小都是固定1kb,为了行文方便,此处不妨将每个文件记录称之为MFT Entry.

   

 

0x02  MFT基础概念

   这里再介绍一下MFT中的NTFS元数据文件:每个NTFS卷包含一组元数据文件——微软保留了前16个MFT 表项(MFT Entry)作为文件系统元数据文件,每一个文件系统元文件的名字都由“$”开始,并且第一个字母大写。其中,第一个MFT记录存储

 其他MFT记录的位置,他被称为基本文件记录(base file record),这一项的含义也就是也就是将整个MFT看做一个文件。

  那么这组元数据文件什么时候用到呢:当NTFS第一次访问一个卷时,它必须要挂载(mount)这个卷,这个时候文件系统就需要从磁盘上读取元数据并构建起内部的数据结构,以应对处理应用程序的的文件系统访问请求。

 

  定位到了MFT主文件表的起始地址,那下面再来看看MTF文件表的关键元文件列表,首先进一步定位到5号文件记录,根目录文件,每一个MFT表项的大小是1kb(0x400),那么5号文件记录,对应的位置是:

  0xC000 0000 + 5*0x400 = 0xC000 1400:

 

 

                      

 

 

   到这里就需要介绍一下MFT Entry中数据结构,以及几个关键属性了:

  一个MFT entry简单来讲的话,可以分为两部分,一个是固定大小的MFT Entry Header,一个是各类属性,而各类属性的话,又可以被细分为属性头和属性体;而实际上它的大部分空间都被用来存储属性,属性是用来存储特定类型数据的数据结构。

  MFT entry结构的简易示意图如下所示:

  

   MFT Entry Header(MFT记录头)结构各字段如下图所示,其中最关键的字段莫过于0x14偏移处,长度为2个字节的“第一个属性的偏移地址”,根据这个字段可以获取到文件记录中第一个属性的位置。

  

 

  关于属性头,它标识了属性的类型、大小和名称等。值得关注的一点是0x08偏移处的“属性是否常驻”标志;

  而属性体,则用于存储文件的内容(此处“文件”一词泛指文件,文件夹等,因为从NTFS的角度,一切皆“文件”,更无文件和文件夹之区分),因此它可以是任意大小,但鉴于MFT Entry数组的每一项只有1kb的大小,所以也就衍生出了“常驻属性”和“非常驻属性”,即文件数据能在MFT Entry里全部记录下的,称为“常驻属性”,放不下的,以“run”结构的方式将数据地址记录在属性头中——放在其他簇中,不再本MFT Entry中,称为”非常驻属性“。

  (这个“run”结构,《深入解析windows操作系统》的中文版将其翻译为“行串”一词,嗯~~,其实我觉得此翻译实在词不达意,可能让初学者看迷糊,我大胆猜想下,命名“run”结构的代码作者,是否是想表达:”这是一个非常驻属性吖!文件数据run了(跑到其他簇了)哟!“;鉴于run这个词很难找到一个合适的翻译,故下文将直接称呼run而不翻译)

  非常驻属性的结构体示意图如下所示:

  

 

  

 

   常驻属性的属性头结构字段如下所示,值得关注的关键字段是:属性类型、属性长度:

  

   非常驻属性的属性头结构字段如下所示,值得关注的关键字段是:属性类型、属性长度,0x20偏移地址出的run的偏移地址

  

   关于非常驻属性中的run的含义:每一个run记录(代表)了若干个连续的簇,例如,如果一个属性分配了48、49、50、51和52这几个簇,则它有一个run,这个run开始于簇48,长度为5.如果这个属性还分配了簇80和81,则它有第二个run,起始于80长度为2。第三个run可能起始于簇56长度为4,这里的多个run称之为run list;因此,简单来讲,一个非常驻属性有多少个run,取决于它的文件数据放置在多少段连续的簇中

  

 

 

   每个run描述的第一个字节表示两个数据的占用字节数:高4bit描述数据的起始簇号,低4Bit则指明了数据占用的簇的数目。因此每个run都是可变长度。其结构示意图如下所示:

  

 

0x03 MFT文件记录属性

   话至此处,就需要再介绍一下MFT Entry中的常见的属性类型(以属性区域开头标志字节为名,H为十六进制省写):

  关键的几个属性分别是0x80文件数据属性,0x90索引根属性,0xA0索引分配属性,其中0x80文件数据属性可能是常驻可能是非常驻,取决于文件数据大小,0x90索引根属性必然是常驻属性,0xA0索引分配属性必然是非常驻属性

      ① 0x80 文件数据属性$DATA

  该属性容纳着文件的内容,文件大小一般指的就是未命名数据流的大小。该属性没有最大最小限制,最小情况是该属性为常驻属性,可以不占用除MFT以外的空间。

  当文件属性能够在文件记录中完全存储下来而不需要存储到其他的Data Run(数据流)中时,这种属性就是常驻属性。

  

         0x90 索引根属性$INDEX_ROOT

  该属性是实现NTFS的B+树索引的根节点,它总是常驻属性。该属性没有最大最小长度限制。属性结构如下:

  

 

  

 

   索引根结构:

字节偏移    字段长度(字节)    描述
~    ~    标准属性头(已分析过)
0x00    4    属性类型
0x04    4    校对规则
0x08    4    每个索引缓冲区的分配大小(字节数)
0x0C    1    每个索引缓冲区的簇数
0x0D    3    无意义(填充到属性长度能被8整除)

   索引头结构: 

字节偏移    字段长度(字节)    描述
0x00    4     第一个索引项的偏移
0x04    4     索引项的总大小
0x08    4     索引项的分配大小
0x0C    1     标志:当该字节为00时,表示其为小索引(适合于索引根);当该字节为01时,表示其为大索引(适合于索引分配)
0x0D    3     无意义(填充到属性长度能被8整除)

   索引项结构:

  索引头后面有着不同长度的索引项的序列,由一个带有最后一个索引项标志的特殊索引项来结束。当一个目录比较小,可以全部存储在索引根属性中时,该目录就只需要这一个属性来描述。而如果目录太大不能全部存储在索引根中时,就会有两个附加的属性出现:一个是索引分配属性,描述B+树目录的子节点;另一个是索引位图属性,描述索引块的索引分配属性使用的虚拟簇号。根目录$Root包含它自身的一个索引项。

0x00    8    该文件的MFT参考号
0x08    2    索引项的大小(相对索引项开始的偏移)
0x0A    2    文件名属性体大小
0x0C    2    索引标志:此处为1表示这个索引项包含子节点;为2表示这是最后一个项,3表示包含子节点且为结束项 
0x0E    2    用0填充,无意义
0x10    8    父目录的MFT文件参考号
0x18    8    文件创建时间
0x20    8    文件最后修改时间
0x28    8    文件记录最后修改时间
0x30    8    文件最后访问时间
0x38    8    文件的分配大小
0x40    8    文件的实际大小
0x48    8    文件标志
0x50    1    文件名的长度
0x51    1    文件名的命名空间
0x52    2F    文件名
2F+0x52    P    填充到能被8整除(无意义)
P+2F+0x52    8    子节点的索引所在的VCN(需要有子节点时才有)

/*命名空间,该值可为以下值中的任意一个
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文件名都存放在同一个文件名属性中。*/

      

       ③  0xA0索引分配属性 $INDEX_ALLOCATION 

  大目录不能把它的所有索引放在常驻$INDEX_ROOT属性里面,所以它们需要一个非常驻$INDEX_ALLOCATION属性。

  它记录了外部索引分配的空间,其所在目录称作大目录。其结构与0X80属性完全一致。它是MFT记录之外的一组索引块运行,用于描述B+树目录的子节点。每个4KB大小的索引缓冲区(索引块)可以容纳20到30个文件项。

  也是一个索引(如目录)的基本结构,存储着组成索引的B+树目录所有子节点的定位信息。它总是非常驻属性,没有最大最小值限制。

  它的属性结构很简单,就是一个非常驻属性的属性头加Data Run就完事。A0H属性的Run List所描述的数据流,也就是NTFS的B+树结构的索引缓冲区。

  

 

 0x04 NTFS的索引结构分析

  行文至此,不得不再描述下基于0x90,0xA0索引根和索引分配属性的目录文件查找逻辑了。

 

   

   

  上图是一个包含3个节点,每个节点3个项,共9项的目录的MFT条目,其中索引根(Index Root)属性指向B+树的根。这个MFT条目并不能容纳所有9个目录项,NTFS必须把其中一部分项存放在别处,因此,NTFS分配了两个索引缓冲区来存放另外的项(索引根以及索引缓冲区一般能够存储三个以上文件项,这依赖于文件名的长度)。一个MFT条目占1KB的空间,而一个索引缓冲区占4KB的空间。

  从上图箭头可以看出NTFS的存储项是按字母顺序进行的。假设运行一个程序试图打开目录中的e.doc文件,NTFS会首先去读索引根属性,其中包含了d.pic、h.ppt及i.ddt三个文件项,并将名称“e.doc”与第一个项的名称“d.pic”进行比较,NTFS得出结论:e.doc在字母序上排在d.pic之后。因此转而处理下一个项“h.ppt”,经过比较以后,由于e.doc的字母序在h.ppt之前,NTFS就会检索h.ppt项中索引缓冲区的虚拟簇号(VCN)。这个索引缓冲区中存储着字母序比h.ppt小但比d.pic大的项。VCN代表一个簇在一个文件或目录中的顺序,NTFS能够将虚拟簇号映射成逻辑簇号(LCN),而逻辑簇号表示着一个簇相对于一个卷的起始点的相对偏移量。而如果h.ppt项中并不存在这个索引缓冲区的虚拟簇号,则NTFS可以立即断定这个目录中不存在e.doc文件,并报告打开文件失败。

  当获得索引缓冲区的虚拟簇号以后,NTFS就会继续查找。从图4-466中可以看出,索引缓冲区的第一个项正是要找的e.doc,于是NTFS就从e.doc项中读出它对应的文件记录的位置。索引项中还存储着如时间戳(创建时间、最后修改时间等)、文件大小、属性等其他信息。虽然NTFS在文件的文件记录中也存储了这些信息,但将这些信息复制到目录项中可以提高列目录或做一些简单的文件查询时的效率。

  目录项是按照字母排序的,这也解释了NTFS系统在列目录的时候为什么总是按照字母顺序的问题。相反,FAT文件系统并不对目录项排序,所以在FAT文件系统中列目录时并没有顺序。另外,由于NTFS将目录项按照B+树的结构保存,使得在包含很多文件的目录中查找文件时效率非常高。一般来说,NTFS只需要扫描所有目录项中的一小部分即可。在这一点上,FAT文件系统只能使用线性查找,也就是说,为了查找一个文件,很大程度上需要查询这个目录中的几乎每一个文件项。

  索引缓冲区是NTFS的B+目录管理中非常重要的一个结构,每个索引缓冲区在NTFS中一般是4KB的固定大小,其位置和大小由目录的文件记录中A0H属性的Run List定义,

   

                       根目录的A0H属性

 

  从上图中A0H属性的Run List可以看到,索引缓冲区的起始簇号是4014H,换算为十进制就是16 404号簇,大小是一个簇。只要跳转到16 404号簇就是索引缓冲区的开始了,如下图所示。

    

  索引块(索引缓冲区)

  目录的外部索引,以“INDEX”开头。里面存放有目录下面的所有文件或者子目录的索引项,文件名都是以unicode进行编码的。一个索引块为一个单位 (与簇的概念类似),其大小在引导记录$Boot中定义,一般总是4KB(簇大小一般也是4K)。

  索引块的结构图(以49 4E 44 58开头,ASCII码为“INDX”,一共8个扇区,参见上图):

  

  索引块由索引块头和一个个的索引项组成(如上图)。

  索引块头(Index Header):在每4KB的一个索引块中,有一个文件索引的标准索引头。

  索引项(Index Entry):节点内的Key,按照文件名首字母顺序排列。目录下每一个文件或子目录对应一个索引项(与90属性中的索引项完全一样)。

  当文件被删除,索引项也会随之消失。

   

  1.索引(块)头的结构(类似MFT头,天蓝色标示的部分同前面索引根属性头的最后部分):

  标准索引(块)头结构

  

 

   

  索引块头部之后,连接的是索引目录项。其结构与前面的索引根属性中介绍的“标准索引项结构”是一致的(此处省略)。

  这说明90属性中的索引项和索引块的索引项是完全一样的。

   

 

 

 0x05 从根目录文件定位到HIVE文件

  前文已经定位到了根目录文件的MFT文件记录在0xC0001400偏移处了,此处也就是“C:\”的MFT文件记录了,接下来需要进一步定位到他的子目录"windows"目录的MFT文件记录;

 

  

  目录的MFT基本文件记录,一般包括头属性、标准属性(0X10)、文件名属性(0X30)、索引根属性(0X90),大目录的MFT基本文件记录下还会包含索引分配属性(0XA0)和BO属性。

      

 

   

  首先从MFT记录头偏移0x14的地址处,读取出第一个属性的相对偏移地址(0x38偏移),可以看到下图中的第一个属性是0x10标准信息属性($STANDARD_INFORMATION)(下图橙色区域是0x10属性),同时可以看到根目录文件记录的MFT中,

共包含有0x10标准信息属性,0x30文件名属性,0x40卷属性,0x90索引根属性,0xA0索引分配属性,0xB0位图属性。

   

   先来康康0x90索引根属性(上图中紫色区域),怎么定位到索引跟属性的呢?

  首先上文定位到了第一个属性0x10属性的位置是0xC0001438,该属性0x04偏移处的值是0x60(属性长度),故下一个属性的位置是:

  下一个属性的地址 = 本属性起始地址 + 本属性长度  ,即0xC0001438 + 0x60 = 0xC0001498;

 

   以此类推,可以得到0x0x90索引根属性的位置是0xC0001520(上图中紫色区域),还记得上文说的嘛,0x90索引根属性永远是常驻属性,所以这里根据常驻属性的属性头结构字段,可以得知0x14偏移处(2个字节大小)是属性体偏移位置0x20,

   故:

   0x90的属性体地址 = 0x90的属性起始地址 + 常驻属性的属性头0x14偏移处记录的属性体偏移  0xC0001520 + 0x20 = 0xC0001540;

 

   0x90的属性体是记录的索引,称为索引体,再回顾一下前面说的0x90属性的索引体结构构成:分为0x10字节的索引根,0x10自己的索引头,以及紧跟其后索引项,其中索引头的第一个DWORD字段代表的是第一个索引项的偏移值0x10,

        故:

   第一个索引项的地址  =  索引体起始地址 + 索引根大小 + 索引头中记录的第一个索引项的偏移值     0xC0001540+0x10+0x10 = 0xC0001560

 

  

 

  再结合索引项的结构体(如下图所示),可知索引项0x50偏移处为该索引项对应文件名长度(0x04),索引项0x52偏移处为文件名“Boot”,这并不是我想找的Windows文件名,所以继续找下一个索引项,找到索引项0x08偏移处记录的该索引项大小0x68,得到:

  下一个索引项的地址  =  该索引项的地址  +    该索引项的大小     0xC0001560 + 0x68 = 0xC00015C8

       

  同理,得知下一个索引项的文件名为“Program Files (x86)”(0xC00015C8 + 0x52 = 0xC000161A地址处),不是“Windows”文件名,继续查找下一个索引项地址(0xC00015C8 + 0x80 = 0xC0001648),进一步定位到其文件名长度0x05(0xC0001698)和文件名"Users"(0xC0001648+0x52 = 0xC000169a),所以继续找下一个索引项(0xC0001648 + 0x68 = 0xC00016B0), 查看这个索引项0x0C偏移处的索引标志是0x03(0xC00016BC),说明这是最后一个项且包含子节点,因此,接下来我们需要到0xA0索引分配属性中继续查找了。

 

  在上文中,我们可以计算出0xA0索引分配属性的地址为0xC00016C8,然会根据非常驻索引头的结构体,得到Data Run的地址为0xC0001710

  Data Run的地址 = 0xA0非常驻属性起始地址 +  Data Run的偏移地址(偏移0x20处记录)  0xC00016C8+ 0x68 = 0xC0001710

  故Data Run为:21 04 A0 02,表明0xA0索引分配属性记录的索引块,起始簇是0x2A0,大小占用4个簇,故:

  索引块的地址  =  起始簇 * 簇大小     0x2A0 * 0x1000 = 0x2A0000

  

  如下图,可以看到索引头的INDX标志,说明索引块没找错。

  

  继续分析索引块结构,最关键的字段莫过于0x18处记录的第一个索引项的偏移地址0x40了(0x2A0018),故:

  索引块的第一个索引项地址 = 索引块起始地址 + 0x18 + 0x18偏移处记录的索引项偏移    0x2A0000 + 0x18 + 0x40 = 0x2A0058,这里索引块的索引项和90属性中的索引项和是完全一样的,所以接下来继续查找文件名:

  $AttrDef(2A00AA = 0x2A0058 + 0x52),

 

       下一个索引项地址(0x2A0058 + 0x68(索引项0x08处记录) = 0x2A00C0),文件名$BadClus(0x2A0112= 0x2A00C0+ 0x52)......

  依此类推,最终可以在0x0x2A1ED8处找到“windows”目录对应的索引项,同时值得注意的是,这个索引块(4KB大小,0x1000)的最后一项(0x2A1FA8起始地址)的Flag标志(0x08偏移处)的值为2,这表示该索引项是这个索引块的最后一项如果还需要再往后找的话,应当找下一个索引块了   下一个索引块地址 = 当前索引块地址 + 索引块大小      0x2A1000 + 0x1000 = 0x2A2000,如果是代码实现查找的话,那就应该获取到索引项起始地址后,首先判断Flag标志(0x0C偏移处)的值是否为2,不为2才继续判断文件名,为2直接定位到下一个索引块开始查找(索引块类似数组结构,是连续的)。

  

   根据索引项的结构体字段,可以得知“Windows”目录文件对应的MFT参考号是0x28a(索引项第一个字段,占6个字节),因此,根据MFT表是数组结构,可知“Windows”目录文件的MFT记录的地址是:

  0xC000 0000 + 0x28a*0x400 = 0xC00A 2800;

  

 

 

       依此类推,可以再次从Windows目录MFT记录中,查找0x90索引根属性,0xA0索引分配属性,进而进一步定位到Windows目录的子目录,“System32”目录的MFT参考号0x9ec,计算出“System32”目录的MFT Entry文件记录地址,

  再进一步定位到子目录config的MFT Entry参考号0x9fe,再计算出“config”目录的MFT Entry文件记录地址,再进一步定位到“system”文件的MFT参考号0xef26,得到“system”文件的MFT Entry文件记录的地址0xC3BC9800(0xC000 0000 + 0xef26*0x400 = 0xC3BC 9800)

  

  继续看上图system文件的MFT文件记录的0x80文件数据属性(橙红色区域),可以看到这是一个非常驻属性(0x08偏移处),Data Run的偏移地址是0x40(0x20偏移处记录),即Data Run的值为:

  32 40 0D 88 AD 05

  即文件数据记录在5AD88个簇,占用D40个簇大小,即地址在0x5AD88 * 0x1000 = 0x5AD88000

  

  

  

  

 

 

 

  

   

 

  

  

 

 

 

 

 

 

  

  

 

 

  

 

 

   

 

 

 

 

 

 

  

 

posted on 2022-01-25 10:53  沉疴  阅读(1251)  评论(0编辑  收藏  举报