发信人: zelor(无良小叔张作乐) 
整理人: wenbobo(2003-03-04 13:08:15), 站内信件
 | 
 
 
 探索NTFS
                      WebCrazy([email protected])
 
     NTFS是Windows NT引入的新型文件系统,它具有许多新特性。本文旨在探索NTFS的底层结构,所叙述的也仅是文件在NTFS卷上的分布。NTFS中,卷中所有存放的数据均在一个叫$MFT的文件中,叫主文件表(Master File Table)。而$MFT则由文件记录(File Record)数组构成。File Record的大小一般是固定的,通常情况下均为1KB,这个概念相当于Linux中的inode。File Record在$MFT文件中物理上是连续的,且从0开始编号。$MFT仅供File System本身组织、架构文件系统使用,这在NTFS中称为元数据(Metadata)。以下列出Windows 2000 Release出的NTFS的元数据文件(我将要给出的示例代码的部分输出结果)。
     File Record(inode) FileName
     ------------------ --------
           0             $MFT
           1             $MFTMirr
           2             $LogFile
           3             $Volume
           4             $AttrDef
           5             .
           6             $Bitmap
           7             $Boot
           8             $BadClus
           9             $Secure
          10             $UpCase
          11             $Extend
 
     Windows 2000中不能使用dir命令(甚至加上/ah参数)像普通文件一样列出这些元数据文件。实际上File System Driver(ntfs.sys)维护了一个系统变量NtfsProtectSystemFiles用于隐藏这些元数据。默认情况下,这个变量被设为TRUE,所以使用dir /ah将得不到任何文件。知道这个行为后使用i386kd修改NtfsProtectSystemFiles后即可以列出元数据文件:
 
     kd> x ntfs!NtfsProtect*
     fe213498  Ntfs!NtfsProtectSystemFiles
     fe21349c  Ntfs!NtfsProtectSystemAttributes
     kd> dd ntfs!NtfsProtectSystemFiles l 2
     fe213498  00000001 00000001
     kd> ed ntfs!NtfsProtectSystemFiles 0
     kd> dd ntfs!NtfsProtectSystemFiles l 2
     fe213498  00000000 00000001
     kd>
 
     D:\>ver
 
     Microsoft Windows 2000 [Version 5.00.2195]
 
     D:\>dir /ah $*
      驱动器 D 中的卷是 W2KNTFS
      卷的序列号是 E831-9D04
 
      D:\ 的目录
 
     2000-04-27  19:31               36,000 $AttrDef
     2000-04-27  19:31                    0 $BadClus
     2000-04-27  19:31               67,336 $Bitmap
     2000-04-27  19:31                8,192 $Boot
     2000-04-27  19:31       <DIR>          $Extend
     2000-04-27  19:31           13,139,968 $LogFile
     2000-04-27  19:31           27,575,296 $MFT
     2000-04-27  19:31                4,096 $MFTMirr
     2000-04-27  19:31              131,072 $UpCase
     2000-04-27  19:31                    0 $Volume
                    9 个文件     40,961,960 字节
                    1 个目录     51,863,552 可用字节
 
     需要指出的是ntfs.sys将元数据文件以一种特殊的方式打开,所以在打开NtfsProtectSystemFiles后,如果使用ReadFile等产生IRP_MJ_READ等IRP包时将会导致Page Fault(详见Gary Nebbett的《Windows NT/2000 Native API Reference》)。
 
     以上的讨论均是基于$MFT文件而讨论的,即基于$MFT中的File Record(inode)讨论的。为更好的继续以下的讨论,这儿我列出File Record Header的结构:
 
     typedef struct {
         ULONG Type;
         USHORT UsaOffset;
         USHORT UsaCount;
         USN Usn;
     } NTFS_RECORD_HEADER, *PNTFS_RECORD_HEADER;
 
     typedef struct {
         NTFS_RECORD_HEADER Ntfs;
         USHORT SequenceNumber;
         USHORT LinkCount;
         USHORT AttributesOffset;
         USHORT Flags;               // 0x0001 = InUse, 0x0002 = Directory
         ULONG BytesInUse;
         ULONG BytesAllocated;
         ULONGLONG BaseFileRecord;
         USHORT NextAttributeNumber;
     } FILE_RECORD_HEADER, *PFILE_RECORD_HEADER;
 
     下面我将讨论如何定位$MFT。稍微有点操作系统知识的人都会知道引导扇区(Boot Sector),其物理位置为卷中的第一个扇区。以下由dskprobe.exe(Windows 2000 Resource Kit中的一个小工具)分析的第一个扇区(当然也可以使用WinHex等其他应用程序):
 
     File: d:\Sector00.bin
     Size: 0x00000200 (512)
 
     Address  | 00 01 02 03-04 05 06 07 : 08 09 0A 0B-0C 0D 0E 0F | 0123456789ABCDEF
     ---------|-------------------------:-------------------------|-----------------
     00000000 | EB 52 90 4E-54 46 53 20 : 20 20 20 00-02 08 00 00 | ?R?NTFS    .....
     00000010 | 00 00 00 00-00 F8 00 00 : 3F 00 F0 00-3F 00 00 00 | .....?..?.e.?...
     00000020 | 00 00 00 00-80 00 80 00 : 90 C0 41 00-00 00 00 00 | ......惱A.....
     00000030 | 04 00 00 00-00 00 00 00 : 09 1C 04 00-00 00 00 00 | ................
     00000040 | F6 00 00 00-01 00 00 00 : 04 9D 31 E8-BB 31 E8 94 | ?.......?杌1钄
                         .                         .
                         .                         .
                         .                         .
     000001F0 | 00 00 00 00-00 00 00 00 : 83 A0 B3 C9-00 00 55 AA | ........儬成..U?
 
 
 
     这512字节为如下的格式:(摘自Gary Nebbett书中,本文许多代码均来自或参考此书。)
 
     #pragma pack(push, 1)
 
     typedef struct {
         UCHAR Jump[3];
         UCHAR Format[8];
         USHORT BytesPerSector;
         UCHAR SectorsPerCluster;
         USHORT BootSectors;
         UCHAR Mbz1;
         USHORT Mbz2;
         USHORT Reserved1;
         UCHAR MediaType;
         USHORT Mbz3;
         USHORT SectorsPerTrack;
         USHORT NumberOfHeads;
         ULONG PartitionOffset;
         ULONG Reserved2[2];
         ULONGLONG TotalSectors;
         ULONGLONG MftStartLcn;
         ULONGLONG Mft2StartLcn;
         ULONG ClustersPerFileRecord;
         ULONG ClustersPerIndexBlock;
         ULONGLONG VolumeSerialNumber;
         UCHAR Code[0x1AE];
         USHORT BootSignature;
     } BOOT_BLOCK, *PBOOT_BLOCK;
 
     #pragma pack(pop)
 
     各个字段的详细意义从字段名中即可大致清楚。在linux-ntfs的GNU工程(http://sf.net/projects/linux-ntfs)中也有详细的文档,限于篇幅我不将其列出。可以使用如下代码读出卷中的第一个扇区:
 
 
     hVolume = CreateFile(drive, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0,
                          OPEN_EXISTING, 0, 0);
 
     ReadFile(hVolume, &bootb, sizeof(bootb), &n, 0);
 
     bootb是一个BOOT_BLOCK结构,在我的卷中如下格式(请对应Sector00.bin分析):
 
     Dump BootBlock at below:
         BytesPerSector:200
         SectorsPerCluster:8
         BootSectors:0
         SectorsPerTrack:3F
         NumberOfHeads:F0
         PartitionOffset:3F
         TotalSectors:41C090
         MftStartLcn:4
         Mft2StartLcn:41C09
         ClustersPerFileRecord:F6
         ClustersPerIndexBlock:1
         VolumeSerialNumber:E8319D04
         BootSignature:AA55
 
 
     以上的MftStartLcn其实是$MFT在卷中的簇(Cluster)号。簇是NTFS的基本单位,最小单位。一个只有1Byte的文件也要占用一簇的空间。NTFS使用LCN(Logical Cluster Number)来代表NTFS卷中的物理位置,其简单的从0到卷中的总簇数减一进行编号。对于一个特定的文件NTFS则使用VCN(Virtual Cluster Number)来映射LCN实现文件的组织。从MftStartLcn的值4可以知道$MFT的LCN为4与SectorsPerCluster、BytesPerSector的大小即可定位$MFT的位置。得到$MFT的位置后,如果遍历$MFT中所有的File Record即可以得到卷中所有的文件列表(前面已经提到File Record只是简单的从0开始编号)。也就是说到目前为止已经可以对文件组织有最简单的认识,但如何得到文件的信息呢,如文件名等等。NTFS中所有文件包括普通的用户文件、元数据文件均用同样的方式组织数据、属性等。我将nfi.exe(来自Windows NT/2000 OEM Support Tools)的输出结果列出,作为我叙述的开始:
 
     D:\>copy con file
     testforntfs^Z
     已复制         1 个文件。
 
     D:\>nfi d:\file
     NTFS File Sector Information Utility.
     Copyright (C) Microsoft Corporation 1999. All rights reserved.
 
     \file
         $STANDARD_INFORMATION (resident)
         $FILE_NAME (resident)
         $DATA (resident)
 
     D:\>echo testforattr>file:ATTR
 
     D:\>nfi d:\file
     NTFS File Sector Information Utility.
     Copyright (C) Microsoft Corporation 1999. All rights reserved.
 
     \file
         $STANDARD_INFORMATION (resident)
         $FILE_NAME (resident)
         $DATA (resident)
         $DATA ATTR (resident)
 
     nfi的输出结果$STANDARD_INFORMATION、$FILE_NAME、$DATA等在NTFS中称为属性(Attribute)。属性分为常驻属性(Resident Attribute)与非常驻属性(Nonresident Attribute)。文件的数据也包含在属性中,似乎与属性这个名称有点混谣。不过这又让NTFS有了更加统一的组织文件的形式。这也同时让NTFS有MultiStreams的特性(上面也演示了这个特性)。通过指定的File Record定位给定的Attribute的实现代码如下:
 
     template <class T1, class T2> inline 
     T1* Padd(T1* p, T2 n) { return (T1*)((char *)p + n); }
 
     PATTRIBUTE FindAttribute(PFILE_RECORD_HEADER file,
                              ATTRIBUTE_TYPE type, PWSTR name)
     {
         for (PATTRIBUTE attr = PATTRIBUTE(Padd(file, file->AttributesOffset));
              attr->AttributeType != -1;
              attr = Padd(attr, attr->Length)) {
 
             if (attr->AttributeType == type) {
                 if (name == 0 && attr->NameLength == 0) return attr;
 
                 if (name != 0 && wcslen(name) == attr->NameLength
                     && _wcsicmp(name, PWSTR(Padd(attr, attr->NameOffset))) == 0) return attr;
             }
         }
 
         return 0;
     }  
 
     Gary Nebbett提供的这个FindAttribute函数在Attribute name(即第三个参数)不为空串时可能会出现bug,主要原因是_wcsicmp对UNICODE字符串比较时应该是以\0结束的标准的C字符串。我在提供的代码中已经纠正了这个错误。 
 
     下面我将通过使用SoftICE来分析这段代码得到$MFT的$FILE_NAME属性来得到$MFT的file name。这个示例同样适用于得到其它文件的$FILE_NAME(如上面的file)、还有其它的属性如$DATA等等。
 
     :bpx FindAttribute
 
     Break due to BPX FindAttribute  (ET=6.89 seconds)
 
     :locals
         [EBP-4] +struct ATTRIBUTE * attr = 0x00344D68 <{...}>
         [EBP+8] +struct FILE_RECORD_HEADER * file = 0x00344D38 <{...}> 
         [EBP+C]  enum ATTRIBUTE_TYPE type = AttributeFileName (30)
         [EBP+10] +unsigned short * name = 0x004041BC <"$MFT">
 
     :?file
     struct FILE_RECORD_HEADER * = 0x00344D38 <{...}>
      struct NTFS_RECORD_HEADER Ntfs = {...}
      unsigned short SequenceNumber = 0x1, "\0\x01"
      unsigned short LinkCount = 0x1, "\0\x01"
      unsigned short AttributesOffset = 0x30, "\00"
      unsigned short Flags = 0x1, "\0\x01"
      unsigned long BytesInUse = 0x2D8, "\0\0\x02\xD8"
      unsigned long BytesAllocated = 0x400, "\0\0\x04\0"
      unsigned quad BaseFileRecord = 0x0, "\0\0\0\0\0\0\0\0"
      unsigned short NextAttributeNumber = 0x6, "\0\x06"
 
     file参数我传入的是$MFT,从$MFT的LCN=4可以得到其在卷中的物理地址,这在上面已说明。你也可以使用dskprobe(我机子中为第LCN*SectorsPerCluster=4*8扇区)得到底下SoftICE的输出结果:
 
     :dd @file //以下的注释可对照文中开头列出的FILE_RECORD_HEADER定义。
     0023:00344D38 454C4946  0003002A  6D4AC04D  00000000      FILE*...M.Jm....
     0023:00344D48 00010001  00010030  000002D8  00000400      ....0...........
                                 ----
                                  |__AttributeOffset
     0023:00344D58 00000000  00000000  04340006  0000FA0D      ..........4.....
     0023:00344D68 00000010  00000060  00180000  00000000      ....`...........
                   --------  --------
                      |         |_指出这个Attribute的长度。定义如下。
                      |_根据AttributeOffset得到的Attribute头,定义如下。00000010指出这个Attribute为StandardInformation
 
     0023:00344D78 00000048  00000018  2C1761D0  01BFB03C      H........a.,<...
 
     Attribute头如下定义:
 
     typedef struct {
         ATTRIBUTE_TYPE AttributeType;
         ULONG Length;
         BOOLEAN Nonresident;
         UCHAR NameLength;
         USHORT NameOffset;
         USHORT Flags;               // 0x0001 = Compressed
         USHORT AttributeNumber;
     } ATTRIBUTE, *PATTRIBUTE;
 
     typedef struct {
         ATTRIBUTE Attribute;
         ULONG ValueLength;
         USHORT ValueOffset;
         USHORT Flags;               // 0x0001 = Indexed
     } RESIDENT_ATTRIBUTE, *PRESIDENT_ATTRIBUTE;
 
     typedef struct {
         ULONGLONG DirectoryFileReferenceNumber;
         ULONGLONG CreationTime;   // Saved when filename last changed
         ULONGLONG ChangeTime;     // ditto
         ULONGLONG LastWriteTime;  // ditto
         ULONGLONG LastAccessTime; // ditto
         ULONGLONG AllocatedSize;  // ditto
         ULONGLONG DataSize;       // ditto
         ULONG FileAttributes;     // ditto
         ULONG AlignmentOrReserved;
         UCHAR NameLength;
         UCHAR NameType;           // 0x01 = Long, 0x02 = Short
         WCHAR Name[1];
     } FILENAME_ATTRIBUTE, *PFILENAME_ATTRIBUTE;
 
 
     ATTRIBUTE_TYPE是一个Enum型定义。其中00000010为StandardInformation。30为FileName。因为FileNameAttribute总是一个常驻Attribute,所以我将RESIDENT_ATTRIBUTE定义也给出。OK,现在可以继续Dump下一个Attribute:
 
     // dd @file+file->AttributeOffset+length(StandardInformationAttribute)
 
     :dd @file+30+60
     0023:00344DC8 00000030  00000068  00180000  00030000      0...h...........
                   --------            ------
                      |                   |___这里的NameLength与NameOffset指FileNameAttribute名。不要与$MFT FileName混谣。
                      |_指出这是一个FileNameAttribute。
     0023:00344DD8 0000004A  00010018  00000005  00050000      J...............
                   --------      ----  --------
                      |            |       |_根据ValueOffset的值,得到FILENAME_ATTRIBUTE的具体位置。
                      |            |_ValueOffset值
                      |_ValueLength值
     0023:00344DE8 2C1761D0  01BFB03C  2C1761D0  01BFB03C      .a.,<....a.,<...
     0023:00344DF8 2C1761D0  01BFB03C  2C1761D0  01BFB03C      .a.,<....a.,<...
     0023:00344E08 00004000  00000000  00004000  00000000      .@.......@......
     0023:00344E18 00000006  00000000  00240304  0046004D      ..........$.M.F.
                                             --  --------
                                             |      |___找到$MFT的FileName了吧。
                                             |_NameLength
     0023:00344E28 00000054  00000000  00000080  00000190      T...............
     0023:00344E38 00400001  00010000  00000000  00000000      ..@.............
 
     这儿给出了Dump Attribute的一个具体方法。最后我将给出遍历File Record的代码,在给出代码前应该说明一下$MFT中$BITMAP属性。NTFS的这个Attribute相当于LINUX EXT2的s_inode_bitmap数组(Linux 2.0版本)。所以很容易明白$BITMAP的作用,即每bit指出相应File Record的在用情况。以下是DumpAllFileRecord的代码:
 
 
     BOOL bitset(PUCHAR bitmap, ULONG i)
     {
         return (bitmap[i >> 3] & (1 << (i & 7))) != 0;
     }
 
     VOID DumpAllFileRecord()
     {
         PATTRIBUTE attr = FindAttribute(MFT, AttributeBitmap, 0);
         PUCHAR bitmap = new UCHAR[AttributeLengthAllocated(attr)];
 
         ReadAttribute(attr, bitmap);
 
         ULONG n = AttributeLength(FindAttribute(MFT, AttributeData, 0)) / BytesPerFileRecord;
 
         PFILE_RECORD_HEADER file = PFILE_RECORD_HEADER(new UCHAR[BytesPerFileRecord]);
 
         for (ULONG i = 0; i < n; i++) {
             if (!bitset(bitmap, i)) continue;
 
             ReadFileRecord(i, file);
 
             if (file->Ntfs.Type == 'ELIF' && (file->Flags & 3 )) {
                 attr = FindAttribute(file, AttributeFileName, 0);
                 if (attr == 0) continue;
 
                 PFILENAME_ATTRIBUTE name
                     = PFILENAME_ATTRIBUTE(Padd(attr, PRESIDENT_ATTRIBUTE(attr)->ValueOffset));
 
                 printf("%8lu %.*ws\n", i, int(name->NameLength),name->Name)
              }
         }
     }
 
     本文引用Gary Nebbett的些定义可能对Windows 2000版本有些很小的出入,不过Internet有其神奇的地方,虽然Microsoft不提供这些信息,但诸如linux-ntfs GNU工程等均是学习NTFS的一个很好的资料,本文也参考了很多它提供的文档。另外Mark Russinovich的《Inside Win2K NTFS》、《Inside NTFS》、《Exploring NTFS On-disk Structures》等也是很好的NTFS资料。本文仍未涉及NTFS中目录的组织(B+树)等等,可能的话我会另行介绍。文中介绍的完整代码可到http://webcrazy.yeah.net下载。出现的错误也欢迎来信指教([email protected])!
 
     最后感谢Anton Altaparmakov,感谢我的同事在出差时抽空给我买到Gary Nebbett的书。感谢我看到的所有资料的原作者们。感谢他们!
 
 参考资料:
     1.Gary Nebbett《Windows NT/2000 Native API Reference》
     2.Linux-NTFS Project NTFS Documentation Version 0.4
     3.Mark Russinovich相关文档
     4.David Solomom《Inside Windows NT,2nd Edition》
 
 
  ---- 驾长车,踏破富士山缺。       | 
 
 
 |