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
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;
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)
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
$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頭如下定義:
ATTRIBUTE_TYPE AttributeType;
ULONG Length;
BOOLEAN Nonresident;
UCHAR NameLength;
USHORT NameOffset;
USHORT Flags; // 0x0001 = Compressed
USHORT AttributeNumber;
} ATTRIBUTE, *PATTRIBUTE;
ATTRIBUTE Attribute;
ULONG ValueLength;
USHORT ValueOffset;
USHORT Flags; // 0x0001 = Indexed
} RESIDENT_ATTRIBUTE, *PRESIDENT_ATTRIBUTE;
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 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)