NTFS文件系统解析

1. 介绍

NTFS(New Technology File System)是一种现代的高性能文件系统,最早由微软公司引入并用于其操作系统中。NTFS的基本构成如下:
NTFS文件系统解析_第1张图片

● GPT区(老式的为MBR区),也被称为隐藏扇区,此区域有描述当前区域占用多少个扇区。
● 分区信息,由DBR记录,从DBR一直到备份DBR属于一个分区。
● DBR中有记录当前分区所占大小,扇区大小,簇大小,MFT起始簇号。
● 扇区记录的是硬盘硬件的最小操作单位,一般是512B。
● 簇记录的是文件系统分区的最小操作单位,一般是8个扇区4KB。
● 逻辑簇号(Logic Cluster Number, LCN),从0开始一直递增到当前分区最后位置。
● 虚拟簇号(Virtual Cluster Number,VCN),VCN用来描述一个文件的,从0开始
● MFT(Master File Table,主文件表或主索引记录),MFT区记录有所有文件的索引信息,目录、文件、元文件( M F T / MFT/ MFT/MFTMirr/$LogFile$Volume$AttrDef$Root$Bitmap$Boot$BadClus$Secure$UpCase\扩展等文件系统保留文件)。MFT文件记录项默认1K对应一个文件索引。小于1KB的文件直接在MFT文件记录项中描述,大于1K的文件通过一个RunList属性来描述,RunList是一个链表,每个项描述一个LCN和大小,这样一个RunList就可以描述一整个文件的内容。MFT区大小是动态的,根据文件目录的数量来动态变化。为了更好地支持MFT动态扩展,所以MFT区放在中间位置更方便动态扩展。当分区都是写的大文件时,MFT占用很小,MFT后面也被用户数据占用。这时,删除大文件,再写小文件,就会出现MFT区不够用,必须得跳到空闲簇来写MFT。所以MFT区不一定是连续的。
NTFS文件系统解析_第2张图片

B i t m a p 是特殊的系统区,其 1 B i t 表示 1 个簇, 1 标识对应的簇已经使用, 0 表示对应的簇空闲。 Bitmap是特殊的系统区,其1Bit表示1个簇,1标识对应的簇已经使用,0表示对应的簇空闲。 Bitmap是特殊的系统区,其1Bit表示1个簇,1标识对应的簇已经使用,0表示对应的簇空闲。Bitmap区有可能是实时更新的,也有些系统为了提高性能会缓存一定数据的修改再来更新 B i t m a p 区。 Bitmap区。 Bitmap区。Bitmap区的大小与分区总簇数大小相关,且以1M对齐。
L o g F i l e 是记录操作日志的。因为 N T F S 是日志型文件系统,通过日志记录操作的开始到结束来保证操作的完整性。 LogFile是记录操作日志的。因为NTFS是日志型文件系统,通过日志记录操作的开始到结束来保证操作的完整性。 LogFile是记录操作日志的。因为NTFS是日志型文件系统,通过日志记录操作的开始到结束来保证操作的完整性。LogFile大小也是动态变化的,一般2T盘不超过128MB。
● 其他元文件都比较小,且都在分区最前面。2T盘默认不超过16MB。

2. 关键信息计算

2.1. GPT

GPT(GUID Partition Table)是一种磁盘分区表,替换传统的MBR,支持更大的分区容量,更多的分区数。
● GPT表0x420偏移处记录着分区1的启动扇区(DBR)的扇区偏移。
NTFS文件系统解析_第3张图片

2.2. MBR

现在主流的起始分区描述表是GPT,但是在之前主流的分区描述表是MBR(Master Boot Record),其只支持最大2TB的磁盘(因为MBR中的描述磁盘大小只预留了4BYTE大小),并且其只支持4个分区。
● GPT表0x1C6偏移处记录着分区1的启动扇区(DBR)的扇区偏移。
NTFS文件系统解析_第4张图片

2.3. DBR

DBR就是启动扇区(Boot Sector),记录着此分区的一些基本信息。此扇区的偏移是以GPT中描述的Starting LBA为基准。
● 0x0B描述扇区大小。
● 0x0D描述簇所占扇区数。
● 0x30描述 M F T 区起始偏移。● 0 x 38 描述 MFT区起始偏移。 ● 0x38描述 MFT区起始偏移。●0x38描述MFTMirr起始偏移。
NTFS文件系统解析_第5张图片

2.4. MFT

● $MFT偏移:DBR的0x30处。
● $MFT大小:动态大小,和文件及目录数量有关。10’000个文件及目录实际对应的MFT项为10‘0000*1KB=10MB,另外大概会预留一倍的空格,所以此时实际的MFT大概为20MB。

2.5. Bitmap

● Bitmap偏移:其记录在元文件 B i t m a p 的 D A T A 属性中,其位置可能在分区前面,也可能在分区后面,不是固定的。● B i t m a p 大小:其大小与文件大小相关,每个 B i t 表示 1 个簇, t o t a l s i z e / 8 / c l u s t e r s i z e 。其当前大小记录在 Bitmap的DATA属性中,其位置可能在分区前面,也可能在分区后面,不是固定的。 ● Bitmap大小:其大小与文件大小相关,每个Bit表示1个簇,total_size/8/cluster_size。其当前大小记录在 BitmapDATA属性中,其位置可能在分区前面,也可能在分区后面,不是固定的。Bitmap大小:其大小与文件大小相关,每个Bit表示1个簇,totalsize/8/clustersize。其当前大小记录在Bitmap元文件DATA属性中。

2.6. LogFile

● LogFile偏移:其记录在元文件 L o g F i l e 属性中,其位置可能在分区前面,也可能在分区后面,不是固定的。● L o g F i l e 大小:其当前大小记录在 LogFile属性中,其位置可能在分区前面,也可能在分区后面,不是固定的。 ● LogFile大小:其当前大小记录在 LogFile属性中,其位置可能在分区前面,也可能在分区后面,不是固定的。LogFile大小:其当前大小记录在Bitmap元文件DATA属性中。在文件系统刚创建时,其比较小。其大小与操作次数有关,是动态增加的,2T盘一般不超过120MB。

3. 计算

详细计算Bitmap和LogFile区域的大小和偏移,需要分析MFT表项的属性详细内容。

3.1. 属性介绍

MFT表项记录着文件的相关属性,有常驻属性(比较小),有非常驻属性(数据较大,此属性只记录真实数据的索引)。Bitmap和LogFile一般都超过1K,都是记录在MFT的非常驻DATA属性中。
MFT表项由MFT_HEADER+多个属性项构成。
NTFS文件系统解析_第6张图片

3.1.1. MFT_HEADER

MFT_HEADER位于MFT表项最前面,其大小固定为56Byte。
typedef struct MFT_HEADER{
    UCHAR    mark[4];        // "FILE" 标志
    UCHAR    UsnOffset[2];   // 更新序列号偏移     30 00
    UCHAR    usnSize[2];     // 更新序列数组大小+1   03 00
    UCHAR    LSN[8];         // 日志文件序列号(每次记录修改后改变) 58 8E 0F 34 00 00 00 00
    UCHAR    SN[2];          // 序列号 随主文件表记录重用次数而增加
    UCHAR    linkNum[2];     // 硬连接数 (多少目录指向该文件) 01 00
    UCHAR    firstAttr[2];   // 第一个属性的偏移  38 00
    UCHAR    flags[2];       // 0已删除 1正常文件 2已删除目录 3目录正使用
    UCHAR    MftUseLen[4];   // 记录有效长度    A8 01 00 00
    UCHAR    maxLen[4];      // 记录占用长度   00 04 00 00
    UCHAR    baseRecordNum[8];  // 索引基本记录, 如果是基本记录则为0
    UCHAR    nextAttrId[2];     // 下一属性Id  07 00
    UCHAR    border[2];         //
    UCHAR    xpRecordNum[4];    // 用于xp, 记录号
    UCHAR    USN[8];         // 更新序列号(2B) 和 更新序列数组
}Mft_Header, *pMft_Header;

3.1.2. 常驻属性

typedef struct NTFSAttribute_RESIDENT //所有偏移量均为相对于属性类型 Type 的偏移量
{
    UCHAR Type[4];           // 属性类型 0x10, 0x20, 0x30, 0x40,...,0xF0,0x100
    UCHAR Length[4];         // 属性的长度
    UCHAR NonResidentFiag;   // 是否是非常驻属性,l 为非常驻属性,0 为常驻属性 00
    UCHAR NameLength;        // 属性名称长度,如果无属性名称,该值为 00
    UCHAR ContentOffset[2];  // 属性内容的偏移量  18 00
    UCHAR CompressedFiag[2]; // 该文件记录表示的文件数据是否被压缩过 00 00
    UCHAR Identify[2];       // 识别标志  00 00
    UCHAR StreamLength[4];   // 属性值的长度, 即属性具体内容的长度。"48 00 00 00"
    UCHAR StreamOffset[2];   // 属性值起始偏移量  "18 00"
    UCHAR IndexFiag[2];      // 属性是否被索引项所索引,索引项基本组成  00 00
    // Reserver
};

3.1.3. 非常驻属性

//------------------  非常驻属性结构 ----
typedef struct NTFSAttribute_NonResident //所有偏移量均为相对于属性类型 Type 的偏移量
{
    UCHAR Type[4];           // 属性类型 0x10, 0x20, 0x30, 0x40,...,0xF0,0x100
    UCHAR Length[4];         // 属性的长度
    UCHAR NonResidentFiag;   // 是否是非常驻属性,l 为非常驻属性,0 为常驻属性 00
    UCHAR NameLength;        // 属性名称长度,如果无属性名称,该值为 00
    UCHAR ContentOffset[2];  // 属性内容的偏移量  18 00
    UCHAR CompressedFiag[2]; // 该文件记录表示的文件数据是否被压缩过 00 00
    UCHAR Identify[2];       // 识别标志  00 00
    UCHAR StartVCN[8];            // 起始的 VCN 值(虚拟簇号:在一个文件中的内部簇编号,0起)
    UCHAR LastVCN[8];             // 最后的 VCN 值
    UCHAR RunListOffset[2];       // 运行列表的偏移量
    UCHAR CompressEngineIndex[2]; // 压缩引擎的索引值,指压缩时使用的具体引擎。
    UCHAR Reserved[4];
    UCHAR StreamAiiocSize[8];     // 为属性值分配的空间 ,单位为B,压缩文件分配值小于实际值
    UCHAR StreamRealSize[8];      // 属性值实际使用的空间,单位为B
    UCHAR StreamCompressedSize[8]; // 属性值经过压缩后的大小, 如未压缩, 其值为实际值
    //RunList
};

3.2. 分析计算

下面是元文件$Bitmap的MFT表项内容:
NTFS文件系统解析_第7张图片

计算代码:

int CheckGPT(char *pBuff)
{
    if ((pBuff[0x400] != 0) && (pBuff[0x410] != 0))
    {
    return 1;
    }

    return 0;
}

uint64_t GetDBROffset(char *pBuff)
{
    if (CheckGPT(pBuff))
    {
    return *(uint64_t*)(pBuff+0x420);
    }
    else
    {
    return *(int*)(pBuff+0x1C6);
    }
}

int GetSctSize(char *pBuff)
{
    return *(short*)(pBuff+0x0B);
}

int GetSctOfCluster(char *pBuff)
{
    return *(char*)(pBuff+0x0D);
}

uint64_t GetMFTOffset(char *pBuff)
{
    uint64_t mftClusterAddr = *(uint64_t*)(pBuff+0x30);

    return mftClusterAddr*GetSctOfCluster(pBuff);
}

uint64_t GetInfoByMFTNo(const char *pBuff, int nSctSize, int nSctOfCluster, int mftNo, int* pBitmapSize)
{
    // mft size = 1K.
    uint64_t bitmapMFTOffset = mftNo * 1024;

    // Find bitmap run list, sizeof(MFT_HEADER)=0x38
    uint64_t attrOffset = bitmapMFTOffset + 0x38;
    while (*(unsigned char*)(pBuff+attrOffset) != 0x80)
    {
    int attrSize = *(int*)(pBuff+attrOffset+4);
    attrOffset += attrSize;
    }

    int runListOffset = *(short*)(pBuff + attrOffset + 0x20);
    int buffSizeBytes =  (*(unsigned char*)(pBuff + attrOffset + runListOffset))&0x0F;
    int buffCluserAddrBytes =(*(unsigned char*)(pBuff + attrOffset + runListOffset))>>4;

    int dataBuffSize = 0;
    for (int i = 0; i < buffSizeBytes; i++)
    {
    int val = *(unsigned char*)(pBuff + attrOffset + runListOffset + 1 + i);
    dataBuffSize += (val<<(i*8));
    }
    *pBitmapSize = dataBuffSize*nSctOfCluster;

    uint64_t dataClusterOffset = 0;
    for (int i = 0; i < buffCluserAddrBytes; i++)
    {
    int val = *(unsigned char*)(pBuff + attrOffset + runListOffset + 1 + buffSizeBytes + i);
    dataClusterOffset += (val<<(i*8));
    }

    return dataClusterOffset*nSctOfCluster;
}

int main(int argc, char* argv[])
{
    FileIO file;
#ifdef _WIN32
    const char* filePath = "\\\\.\\PhysicalDrive1";
#else
    const char* filePath = "/dev/sda";
#endif

    if (!file.Open(filePath)) {
    return 1;
    }

    const int bufferSize = 40960;
    char* pBuff = new char[bufferSize]();

    // Read GPT/MBR
    if (!file.Read(pBuff, bufferSize, 0)) 
    {
    printf("Failed to read disk");
    return 1;
    }     
    
    std::cout<<(CheckGPT(pBuff) ? "It is GPT" : "It is MBR" )<<std::endl;
    std::cout<<"DBROffset:"<<GetDBROffset(pBuff)<<std::endl;

    // Read DBR
    int dbrOffset = GetDBROffset(pBuff);

    if (!file.Read(pBuff, bufferSize, dbrOffset*512)) 
    {
    printf("Failed to read disk");
    return 2;
    } 

    uint64_t mftOffset = GetMFTOffset(pBuff);
    int nSctSize = GetSctSize(pBuff);
    int nSctOfCluster = GetSctOfCluster(pBuff);

    std::cout<<"Sector Size:"<<nSctSize<<std::endl;
    std::cout<<"Sector of Cluster:"<<nSctOfCluster<<std::endl;
    std::cout<<"MFT Sector offset:"<<mftOffset<<std::endl;

    // Read MFT
    if (!file.Read(pBuff, bufferSize, (mftOffset + dbrOffset)*nSctSize)) 
    {
    printf("Failed to read disk");
    return 2;
    } 

    int nLogFileSize = 0;
    int mftNoOfLogFile = 2;
    uint64_t logFileOffset = GetInfoByMFTNo(pBuff, nSctSize, nSctOfCluster, mftNoOfLogFile, &nLogFileSize);

    std::cout<<"LogFile sector offset:"<<logFileOffset<<std::endl;
    std::cout<<"LogFile size:"<<nLogFileSize*nSctSize<<std::endl;

    int nBitmapSize = 0;
    int mftNoOfBitmap = 6;
    uint64_t bitmapOffset = GetInfoByMFTNo(pBuff, nSctSize, nSctOfCluster, mftNoOfBitmap, &nBitmapSize);

    std::cout<<"Bitmap sector offset:"<<bitmapOffset<<std::endl;
    std::cout<<"Bitmap size:"<<nBitmapSize*nSctSize<<std::endl;

    return 0;
}

验证工程,支持Windows/Linux下脚本编译运行测试:
代码
Windows:
NTFS文件系统解析_第8张图片

Ubuntu:
NTFS文件系统解析_第9张图片

4. 总结

NTFS文件写操作过程中,最常修改的文件系统内容分别为:
MFT、Bitmap、LogFile,其次是DBR区的一些其他元文件如 M F T M i r r o r 和 MFTMirror和 MFTMirrorAttrDef等。
MFT和Logfile都是动态增加的
● 10‘000个文件及目录对应10MBMFT保留区。
● 2TB磁盘对应120MBLogFile区。
● DBR一般保留16MB足够。
● DBR、MFT、Bitmap、LogFile偏移见测试代码。

你可能感兴趣的:(存储,NTFS)