Pullarla的文章

  • 编程实现根据NTFS文件系统定位文件在磁盘上的偏移地址

    背景之前在上一篇博文中 “NTFS文件系统介绍及文件定位” 介绍过 NTFS 的基础概念和常用格式介绍,同时还详细给出了使用 NTFS 定位磁盘文件的例子。现在,这篇文章讲解的就是,编程实现 NTFS 文件定位。也就是把之前手动定位全部改成编程实现,输入一个文件路径,就可以得到文件的大小和数据在磁盘上的偏移地址。
    现在,就把实现的过程整理成文档,分享给大家。如果你之前没有了解过 NTFS 的相关概念,可以先阅读之前写的 “NTFS文件系统介绍及文件定位” 这篇文章。
    实现原理使用 NTFS 文件系统来定位文件,原理如下:

    首先打开磁盘,获取磁盘的分区引导扇区 DBR 中的数据。根据 DBR 的数据含义,从中获取扇区大小、簇大小以及\$MFT住文件记录的起始簇号
    根据根目录文件记录,一般处于 5 号文件,而且每个文件记录大小为 2 个扇区大小的知识前提。我们可以计算出,根目录文件记录的偏移地址。得到根目录文件记录偏移地址,就可以来到了磁盘文件根目录下了
    然后,我们根据文件记录头的数据格式,获取第一个属性的偏移位置,然后在获取属性的大小,以此计算出下一个属性的偏移位置。这样就可以对文件记录中的属性进行遍历了。我们按照 90H属性、A0H属性的处理顺序进行处理,获取文件或者目录的的$MFT参考号,然后继续跳转到下一个文件记录,重复处理 90H属性、A0H属性,获取文件或者目录的的$MFT参考号,直到获取到最终的文件
    这时,我们就可以根据 80H 属性获取文件数据的偏移位置

    这样文件定位就结束了。其中,我们需要继续介绍 90H 属性的处理过程、A0H属性的处理过程以及使用 80H 属性 定位文件数据偏移的过程。
    90H 属性的处理就是,直接扫描 90H 属性的数据,判断有没有我们要定位的文件或者目录的名称,若有,则获取该文件或者目录的\$MTF文件参考号;若没有,则不处理。
    A0H 属性的处理过程是:

    我们首先获取 Data Run 在属性中的偏移,然后从偏移中获取 Data Run 数据,并跳转到 Data Run 指向的偏移地址
    跳转到 Data Run 指向的偏移地址,便到了 INDX 索引。然后,我们就扫描 INDX 的数据,判断有没有我们要定位的文件或者目录的名称。若没有,则继续退出。若有,则获取该文件或者目录的\$MTF文件参考号
    如果有多个Data Run,则重复上面操作,若没有,执行完毕后,就退出

    根据 80H 属性定位文件数据:

    首先,我们先根据$MTF文件参考号来到文件记录处,并获取 80H 属性的偏移
    然后,判断 80H 属性是常驻属性还是非常驻属性。若是常驻属性,则直接在 80H 属性中获取文件数据。若为非常驻属性,则需要获取 80H 属性的 Data Run 数据,那么,Data Run 中执行的地址就是数据内容的存储地址

    编码实现从DBR中获取扇区大小、簇大小、\$MFT起始簇号// 从DBR中获取数据:每个扇区字节数、每个簇的扇区数、原文件$MFT的起始簇号BOOL GetDataFromDBR(HANDLE hFile, WORD &wSizeOfSector, BYTE &bSizeOfCluster, LARGE_INTEGER &liClusterNumberOfMFT){ // 获取扇区大小(2)、簇大小(1)、$MFT起始簇号(8) BYTE bBuffer[512] = { 0 }; DWORD dwRead = 0; // 注意:数据读取的大小最小单位是扇区!!! ::SetFilePointer(hFile, 0, NULL, FILE_BEGIN); ::ReadFile(hFile, bBuffer, 512, &dwRead, NULL); wSizeOfSector = MAKEWORD(bBuffer[0x0B], bBuffer[0x0C]); bSizeOfCluster = bBuffer[0x0D]; liClusterNumberOfMFT.LowPart = MAKELONG(MAKEWORD(bBuffer[0x30], bBuffer[0x31]), MAKEWORD(bBuffer[0x32], bBuffer[0x33])); liClusterNumberOfMFT.HighPart = MAKELONG(MAKEWORD(bBuffer[0x34], bBuffer[0x35]), MAKEWORD(bBuffer[0x36], bBuffer[0x37])); return TRUE;}
    文件定位// 定位文件BOOL LocationFile(HANDLE hFile, char *lpszFilePath, WORD wSizeOfSector, BYTE bSizeOfCluster, LARGE_INTEGER liMFTOffset, LARGE_INTEGER &liRootOffset){ BYTE bBuffer[1024] = { 0 }; DWORD dwRead = 0; // 分割文件路径 char szNewFile[MAX_PATH] = { 0 }; ::lstrcpy(szNewFile, (lpszFilePath + 3)); char szDelim[] = "\\"; char *lpResult = strtok(szNewFile, szDelim); BYTE bUnicode[MAX_PATH] = { 0 }; while (NULL != lpResult) { BOOL bFlag = FALSE; DWORD dwNameOffset = 0; // 将分割的目录转换成2字节表示的Unicode数据 DWORD dwLen = ::lstrlen(lpResult); ::RtlZeroMemory(bUnicode, MAX_PATH); for (DWORD i = 0, j = 0; i < dwLen; i++) { bUnicode[j++] = lpResult[i]; bUnicode[j++] = 0; } // 读取目录的数据,大小为1KB ::SetFilePointer(hFile, liRootOffset.LowPart, &liRootOffset.HighPart, FILE_BEGIN); ::ReadFile(hFile, bBuffer, 1024, &dwRead, NULL); // 获取第一个属性的偏移 WORD wAttributeOffset MAKEWORD(bBuffer[0x14], bBuffer[0x15]); // 遍历文件目录的属性 DWORD dwAttribute = 0; DWORD dwSizeOfAttribute = 0; while (TRUE) { if (bFlag) { break; } // 获取当前属性 dwAttribute = MAKELONG(MAKEWORD(bBuffer[wAttributeOffset], bBuffer[wAttributeOffset + 1]), MAKEWORD(bBuffer[wAttributeOffset + 2], bBuffer[wAttributeOffset + 3])); // 判断属性 if (0x90 == dwAttribute) { bFlag = HandleAttribute_90(bBuffer, wAttributeOffset, bUnicode, dwLen, liMFTOffset, liRootOffset); } else if (0xA0 == dwAttribute) { bFlag = HandleAttribute_A0(hFile, bBuffer, wSizeOfSector, bSizeOfCluster, wAttributeOffset, bUnicode, dwLen, liMFTOffset, liRootOffset); } else if (0xFFFFFFFF == dwAttribute) { bFlag = TRUE; break; } // 获取当前属性的大小 dwSizeOfAttribute = MAKELONG(MAKEWORD(bBuffer[wAttributeOffset + 4], bBuffer[wAttributeOffset + 5]), MAKEWORD(bBuffer[wAttributeOffset + 6], bBuffer[wAttributeOffset + 7])); // 计算下一属性的偏移 wAttributeOffset = wAttributeOffset + dwSizeOfAttribute; } // 继续分割目录 lpResult = strtok(NULL, szDelim); } return TRUE;}
    处理90H属性// 0x90属性的处理BOOL HandleAttribute_90(BYTE *lpBuffer, WORD wAttributeOffset, BYTE *lpUnicode, DWORD dwLen, LARGE_INTEGER liMFTOffset, LARGE_INTEGER &liRootOffset){ // 先遍历判断0x90属性里是否有此目录或文件(UNICODE) // 获取当前属性的大小 DWORD dwSizeOfAttribute = MAKELONG(MAKEWORD(lpBuffer[wAttributeOffset + 4], lpBuffer[wAttributeOffset + 5]), MAKEWORD(lpBuffer[wAttributeOffset + 6], lpBuffer[wAttributeOffset + 7])); for (DWORD i = 0; i < dwSizeOfAttribute; i++) { if (CompareMemory(lpUnicode, (lpBuffer + wAttributeOffset + i), 2 * dwLen)) { DWORD dwNameOffset = wAttributeOffset + i; // 计算文件号 dwNameOffset = dwNameOffset / 8; dwNameOffset = 8 * dwNameOffset; dwNameOffset = dwNameOffset - 0x50; // 获取文件号(6) LARGE_INTEGER liNumberOfFile; liNumberOfFile.LowPart = MAKELONG(MAKEWORD(lpBuffer[dwNameOffset], lpBuffer[dwNameOffset + 1]), MAKEWORD(lpBuffer[dwNameOffset + 2], lpBuffer[dwNameOffset + 3])); liNumberOfFile.HighPart = MAKELONG(MAKEWORD(lpBuffer[dwNameOffset + 4], lpBuffer[dwNameOffset + 5]), 0); // 计算文件号的偏移,文件号是相对$MFT为偏移说的 liRootOffset = liNumberOfFile; liRootOffset.QuadPart = liMFTOffset.QuadPart + liRootOffset.QuadPart * 0x400; return TRUE; } } // 读取Data Run List,去到索引处INDX遍历UNICODE,获取文件号 return FALSE;}
    处理A0H属性// 0xA0属性的处理BOOL HandleAttribute_A0(HANDLE hFile, BYTE *lpBuffer, WORD wSizeOfSector, BYTE bSizeOfCluster, WORD wAttributeOffset, BYTE *lpUnicode, DWORD dwLen, LARGE_INTEGER liMFTOffset, LARGE_INTEGER &liRootOffset){ // 读取Data Run List,去到索引处INDX遍历UNICODE,获取文件号 DWORD dwCount = 0; LONGLONG llClusterOffet = 0; // 获取索引号的偏移 WORD wIndxOffset = MAKEWORD(lpBuffer[wAttributeOffset + 0x20], lpBuffer[wAttributeOffset + 0x21]); // 读取Data Run List while (TRUE) { BYTE bTemp = lpBuffer[wAttributeOffset + wIndxOffset + dwCount]; // 读取Data Run List,分解并计算Data Run中的信息 BYTE bHi = bTemp >> 4; BYTE bLo = bTemp & 0x0F; if (0x0F == bHi || 0x0F == bLo || 0 == bHi || 0 == bLo) { break; } LARGE_INTEGER liDataRunSize, liDataRunOffset; liDataRunSize.QuadPart = 0; liDataRunOffset.QuadPart = 0; for (DWORD i = bLo; i > 0; i--) { liDataRunSize.QuadPart = liDataRunSize.QuadPart << 8; liDataRunSize.QuadPart = liDataRunSize.QuadPart | lpBuffer[wAttributeOffset + wIndxOffset + dwCount + i]; } if (0 == llClusterOffet) { // 第一个Data Run for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; } } else { // 第二个及多个Data Run // 判断正负 if (0 != (0x80 & lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + bHi])) { // 负整数 for (DWORD i = bHi; i > 0; i--) { // 补码的原码=反码+1 liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | (BYTE)(~lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]); } liDataRunOffset.QuadPart = liDataRunOffset.QuadPart + 1; liDataRunOffset.QuadPart = 0 - liDataRunOffset.QuadPart; } else { // 正整数 for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; } } } // 注意加上上一个Data Run的逻辑簇号(第二个Data Run可能是正整数、也可能是负整数(补码表示), 可以根据最高位是否为1来判断, 若为1, 则是负整数, 否则是正整数) liDataRunOffset.QuadPart = llClusterOffet + liDataRunOffset.QuadPart; llClusterOffet = liDataRunOffset.QuadPart; // 去到索引处INDX遍历UNICODE,获取文件号 LARGE_INTEGER liIndxOffset, liIndxSize; liIndxOffset.QuadPart = liDataRunOffset.QuadPart * bSizeOfCluster * wSizeOfSector; liIndxSize.QuadPart = liDataRunSize.QuadPart * bSizeOfCluster * wSizeOfSector; // 读取索引的数据,大小为1KB BYTE *lpBuf = new BYTE[liIndxSize.QuadPart]; DWORD dwRead = 0; ::SetFilePointer(hFile, liIndxOffset.LowPart, &liIndxOffset.HighPart, FILE_BEGIN); ::ReadFile(hFile, lpBuf, liIndxSize.LowPart, &dwRead, NULL); // 遍历Unicode数据 for (DWORD i = 0; i < liIndxSize.LowPart; i++) { if (CompareMemory(lpUnicode, (lpBuf + i), 2 * dwLen)) { DWORD dwNameOffset = i; // 计算文件号 dwNameOffset = dwNameOffset / 8; dwNameOffset = 8 * dwNameOffset; dwNameOffset = dwNameOffset - 0x50; // 获取文件号(6) LARGE_INTEGER liNumberOfFile; liNumberOfFile.LowPart = MAKELONG(MAKEWORD(lpBuf[dwNameOffset], lpBuf[dwNameOffset + 1]), MAKEWORD(lpBuf[dwNameOffset + 2], lpBuf[dwNameOffset + 3])); liNumberOfFile.HighPart = MAKELONG(MAKEWORD(lpBuf[dwNameOffset + 4], lpBuf[dwNameOffset + 5]), 0); // 计算文件号的偏移,文件号是相对$MFT为偏移说的 liRootOffset = liNumberOfFile; liRootOffset.QuadPart = liMFTOffset.QuadPart + liRootOffset.QuadPart * 0x400; return TRUE; } } delete[]lpBuf; lpBuf = NULL; // 计算下一个Data Run List偏移 dwCount = dwCount + bLo + bHi + 1; } return FALSE;}
    读取文件数据内容偏移BOOL FileContentOffset(HANDLE hFile, WORD wSizeOfSector, BYTE bSizeOfCluster, LARGE_INTEGER liMFTOffset, LARGE_INTEGER liRootOffset){ BYTE bBuffer[1024] = { 0 }; DWORD dwRead = 0; LARGE_INTEGER liContenOffset = liRootOffset; // 读取目录的数据,大小为1KB ::SetFilePointer(hFile, liRootOffset.LowPart, &liRootOffset.HighPart, FILE_BEGIN); ::ReadFile(hFile, bBuffer, 1024, &dwRead, NULL); // 获取第一个属性的偏移 WORD wAttributeOffset MAKEWORD(bBuffer[0x14], bBuffer[0x15]); // 遍历文件目录的属性 DWORD dwAttribute = 0; DWORD dwSizeOfAttribute = 0; while (TRUE) { // 获取当前属性 dwAttribute = MAKELONG(MAKEWORD(bBuffer[wAttributeOffset], bBuffer[wAttributeOffset + 1]), MAKEWORD(bBuffer[wAttributeOffset + 2], bBuffer[wAttributeOffset + 3])); // 判断属性 if (0x80 == dwAttribute) { // 读取偏移0x8出1字节,判断是否是常驻属性 BYTE bFlag = bBuffer[wAttributeOffset + 0x8]; if (0 == bFlag) // 常驻 { // 读取偏移0x14出2字节,即是内容的偏移 WORD wContenOffset = MAKEWORD(bBuffer[wAttributeOffset + 0x14], bBuffer[wAttributeOffset + 0x15]); liContenOffset.QuadPart = liContenOffset.QuadPart + wAttributeOffset + wContenOffset; printf("File Content Offset:0x%llx\n\n", liContenOffset.QuadPart); } else // 非常驻 { // 读取偏移0x20出2字节,即是数据运行列表偏移 DWORD dwCount = 0; LONGLONG llClusterOffet = 0; // 获取索引号的偏移 WORD wIndxOffset = MAKEWORD(bBuffer[wAttributeOffset + 0x20], bBuffer[wAttributeOffset + 0x21]); // 读取Data Run List while (TRUE) { BYTE bTemp = bBuffer[wAttributeOffset + wIndxOffset + dwCount]; // 读取Data Run List,分解并计算Data Run中的信息 BYTE bHi = bTemp >> 4; BYTE bLo = bTemp & 0x0F; if (0x0F == bHi || 0x0F == bLo || 0 == bHi || 0 == bLo) { break; } LARGE_INTEGER liDataRunSize, liDataRunOffset; liDataRunSize.QuadPart = 0; liDataRunOffset.QuadPart = 0; for (DWORD i = bLo; i > 0; i--) { liDataRunSize.QuadPart = liDataRunSize.QuadPart << 8; liDataRunSize.QuadPart = liDataRunSize.QuadPart | bBuffer[wAttributeOffset + wIndxOffset + dwCount + i]; } if (0 == llClusterOffet) { // 第一个Data Run for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | bBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; } } else { // 第二个及多个Data Run // 判断正负 if (0 != (0x80 & bBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + bHi])) { // 负整数 for (DWORD i = bHi; i > 0; i--) { // 补码的原码=反码+1 liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | (BYTE)(~bBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]); } liDataRunOffset.QuadPart = liDataRunOffset.QuadPart + 1; liDataRunOffset.QuadPart = 0 - liDataRunOffset.QuadPart; } else { // 正整数 for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | bBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; } } } // 注意加上上一个Data Run的逻辑簇号(第二个Data Run可能是正整数、也可能是负整数(补码表示), 可以根据最高位是否为1来判断, 若为1, 则是负整数, 否则是正整数) liDataRunOffset.QuadPart = llClusterOffet + liDataRunOffset.QuadPart; llClusterOffet = liDataRunOffset.QuadPart; // 显示逻辑簇号和大小 liContenOffset.QuadPart = liDataRunOffset.QuadPart*wSizeOfSector*bSizeOfCluster; printf("File Content Offset:0x%llx\nFile Content Size:0x%llx\n", liContenOffset.QuadPart, (liDataRunSize.QuadPart*wSizeOfSector*bSizeOfCluster)); // 计算下一个Data Run List偏移 dwCount = dwCount + bLo + bHi + 1; } } } else if (0xFFFFFFFF == dwAttribute) { break; } // 获取当前属性的大小 dwSizeOfAttribute = MAKELONG(MAKEWORD(bBuffer[wAttributeOffset + 4], bBuffer[wAttributeOffset + 5]), MAKEWORD(bBuffer[wAttributeOffset + 6], bBuffer[wAttributeOffset + 7])); // 计算下一属性的偏移 wAttributeOffset = wAttributeOffset + dwSizeOfAttribute; } return TRUE;}
    程序测试我们在 main 函数中调用上述封装好的函数,定位文件:H:\NtfsTest\520.exe,成功定位文件:

    总结这个程序需要管理员权限运行,因为我们使用 CreateFile 函数打开磁盘,这一步操作,需要权限才可操作成功。
    这个程序实现起来不是很难,关键是理解起来并不是那么容易。需要大家对 NTFS 要有足够的了解,熟练地使用掌握 80H属性、90H属性、A0H属性的数据含义,同时,还需要了解 Data Run 的分析。还是建议大家先阅读之前写的 “NTFS文件系统介绍及文件定位” 这篇文章,这里提到的知识点,都在这个程序中体现了。
    参考参考自《Windows黑客编程技术详解》一书
    2  留言 2018-12-20 12:06:16
  • NTFS文件系统介绍及文件定位

    背景在日常生活中,我们打开我们的电脑操作各种文件。我们都知道,文件数据都存储在硬盘上,但是,硬盘中存储的数据都是0、1的二进制数据,我们的电脑怎么从这一大堆0、1数据中知道哪个是哪个文件呢?
    这就是文件系统在起作用,它对硬盘的数据设置格式规则,存储数据的时候,就按这个存储规则进行存储,那么,在读取数据文件的时候,再按照相应规则读取还原数据,就形成了我们看到的文件了。
    现在,本文就介绍目前比较流行的 NTFS 文件系统及其格式定义,并给出一个使用 NTFS 文件系统进行文件定位的例子,模拟 NTFS 定位文件的过程原理。我把分析过程整理成文档,分享给大家。
    NTFS介绍NTFS 文件系统概念文件系统是操作系统用于明确磁盘或分区上的文件的方法和数据结构,即在磁盘上组织文件的方法。文件系统是对应硬盘的分区的,而不是整个硬盘,不管是硬盘只有一个分区,还是几个分区,不同的分区可以有着不同的文件系统。
    NTFS(New Technology File System)是运行在 Windows NT 操纵系统环境和Windows NT 高级服务器网络操作环境的文件系统,随着 Windows NT 操作系统的诞生而产生。NTFS 文件系统具有安全性高、稳定性好、不易产生文件碎片的优点,使得它成为主流的文件系统。
    NTFS 文件系统相关概念
    分区:分区是磁盘的基本组成部分,被划分的磁盘一部分
    卷:NTFS以卷为基础,卷建立在分区的基础上,当以NTFS来格式化磁盘分区时就创建了一个卷
    簇:NTFS使用簇作为磁盘空间的分配和回收的基本单位
    逻辑簇号(LCN):对卷中所有的簇从头至尾进行编号
    虚拟簇号(VCN):对于文件内的所有簇进行编号
    主文件表(\$MFT):$MFT是卷的核心,存放着卷中所有数据,包括:定位和恢复文件的数据结构、引导程序数据和记录整个卷的分配分配状态的位图等
    文件记录:NTFS不是将文件仅仅视为一个文本库或二进制数据,而是将文件作为许多属性和属性值的集合来处理;每个文件或文件夹在元文件\$MFT均有一个文件记录号
    常驻属性:文件属性值能直接存储在$MFT记录中
    非常驻属性:不能直接存储在\$MFT记录中,需要在\$MFT之外为其分配空间进行存储

    NTFS数据存放方式
    NTFS 文件系统以文件的形式来对数据进行管理,以簇为单位来存储数据的。在NTFS里边的分区的簇大小的规律:

    如果分区小于512M ,簇大小1扇区
    如果分区大于512M小于1G,簇大小为2个扇区
    如果分区大于1G小于2G,簇大小为4个扇区
    如果分区大于2GB,簇大小为8个扇区

    NTFS常见元文件列表
    分区引导扇区 DBR$Boot元文件由分区的第一个扇区(即 DBR)和后面的 15 个扇区(即 NTLDR 区域)组成,其中 DBR 由“跳转指令”、“OEM代号”、“BPB”、“引导程序”和“结束标志”组成:

    对于,DBR 部分的字段含义如下图所示。其中,我们需要重点关注每个扇区的字节总数、簇大小、\$MFT主文件记录表的开始簇号。

    文件记录在 NTFS 文件系统中,磁盘上的所有数据都是以文件的形式存储,其中包括元文件每个文件都有一个或多个文件记录,每个文件记录占用两个扇区,即 1024 字节。而 \$MFT 元文件就是专门记录每个文件的文件记录。
    由于 NTFS 文件系统是通过 \$MFT 来确定文件在磁盘上的位置以及文件的属性,所以\$MFT 是非常重要的,\$MFT 的起始位置在 DBR 中有描述。\$MFT 的文件记录在物理上是连续的,并且从 0 开始编号;\$MFT 的前 16 个文件记录总是元文件的,并且顺序是固定不变的。
    文件记录由两部分构成,一部分是文件记录头,另一部分是属性列表,最后结尾是四个“FF”:

    文件记录头解析对于文件记录头中每个数据的含义如下,其中,需要重点关注的是偏移为 0x14,长度为 2 字节的第一个属性的偏移地址,根据这个字段可以获取文件记录中第一个属性的位置。

    文件记录属性属性有两种,分为常驻属性和非常驻属性。在属性中,偏移 0x8,长度为 1 字节的字段,就是区分了常驻属性和非常驻属性。值为 0x00 表示常驻属性,0x01 表示非常驻属性。
    常驻属性头的每个字段含义如下所示。其中,要重点关注属性类型、属性长度、是否为常驻属性还是非常驻属性。

    非常驻属性头的每个字段含义如下所示。其中,要重点关注属性类型、属性长度、是否为常驻属性还是非常驻属性、Data Run 的偏移地址以及 Data Run 的数据信息。

    数据运行列表 Data Run List我们可以由上面知道,当属性为非常驻属性的时候,属性中就会有一个字段来表示 Data Run。当属性不能存放完数据,系统就会在NTFS数据区域开辟一个空间存放,这个区域是以簇为单位的。Data Run List 就是记录这个数据区域的起始簇号和大小。它的含义分析如下:

    Data Run的第一个字节分高4位和低4位。其中,高4位表示文件内容的起始簇号在Data Run List中占用的字节数。低4位表示文件内容簇数在Data Run List中占用的字节数。
    Data Run的第二个字节开始表示文件内容的簇数,接着表示文件内容的起始簇号。
    Data Run可以指示空间的大小以及偏移位置,例如上述中给的例子,起始簇号为:A2 59 00(10639616),数据大小为:C0 14(49172)。
    对于多个Data Run的情况,第一个Data Run的起始簇号是一个正整数,而第二个Data Run开始,起始簇号偏移是分正负的。可以根据起始簇号偏移的最高位是否来判断,若为1,则是负整数(补码表示);否则,是正整数。而且,从第二个Data Run开始,起始簇号偏移都是相对于上一个Data Run的起始簇号来说的。下面举个例子,方便大家理解。
    例如,有这么一个Data Run如下所示:
    31 01 FD 0A 28 21 01 AB FA 21 01 4A F5 21 01 91 C1 00我们可以看到上面一共有4个Data Run,分别如下:
    第1个Data Run

    31 01 FD 0A 28
    正整数:第一个Data Run的起始簇号都是正整数起始簇号:28 0A FD(2624253)

    第2个Data Run

    21 01 AB FA
    负整数:起始簇号偏移FA AB的最高位是1,所以是负整数(补码),所以FA AB(-1365)起始簇号:相对于上一个Data Run的偏移,所以为:2624253-1365=2622888

    第3个Data Run

    21 01 4A F5
    负整数:起始簇号偏移F5 4A的最高位是1,所以是负整数(补码),所以F5 4A(-2742)起始簇号:相对于上一个Data Run的偏移,所以为:2622888-2742=2620146

    第4个Data Run

    21 01 91 C1
    负整数:起始簇号偏移C1 91的最高位是1,所以是负整数(补码),所以C1 91(-15983)起始簇号:相对于上一个Data Run的偏移,所以为:2620146-15983=2604163

    几个重要的属性接下来,我们重点讲解下几个重要的属性:80H属性、90H属性以及 A0H属性。
    80H属性80H属性是文件数据属性,该属性容纳着文件的内容,文件的大小一般指的就是未命名数据流的大小。该属性没有最大最小限制,最小情况是该属性为常驻属性。当在数据在属性内没有办法展示完全的时候,就需要Data Run的帮助,那么这时属性就为常驻属性,文件数据就存储在 Data Run指向的簇当中。
    90H属性90H属性是索引根属性,该属性是实现NTFS的B+树索引的根节点,它总是常驻属性。该属性的结构如下图:

    其中,索引根的字段含义如下所示:

    索引头的字段含义如下所示:

    索引项的字段含义如下所示:

    A0属性A0属性是索引分配属性,也是一个索引的基本结构,存储着组成索引的B+树目录索引子节点的定位信息。它总是常驻属性:

    根据上图A0H属性的 Data Run List 可以找到索引区域,偏移到索引区域所在的簇:

    其中,标准索引头的解释如下。要注意,下面的索引项偏移加上0x18。

    索引项的解释如下:

    基于 NTFS 文件定位思路及例子NTFS定位文件大致过程如下:

    根据BDP,获取扇区大小、簇大小以及\$MFT起始扇区
    根据$MFT位置,计算根目录的文件记录,一般在 5 号文件记录
    查找80H、90H、A0H属性,注意常驻属性和非常驻属性
    获取 Data Run,从 Data Run 中定位到起始簇后,再分析索引项可以得到文件名等信息
    根据80H属性中的数据流就可以找到文件真正的数据

    接下来,我来演示怎么使用 NTFS 文件系统定位出 H:\NtfsTest\520.exe 文件。
    首先,我们使用 WinHex 软件,打开 H 盘的分区引导扇区 DBR,我们可以从中获取:每个扇区大小为 0x200 字节;每个簇大小为 0x08 个扇区;\$MFT 开始簇号为 0x0C0000。
    所以,我们根据以上信息计算出 $MFT 开始的偏移地址为:
    0x0C0000 * 0x08 * 0x200 = 0xC000 0000

    然后,我们就开始计算根目录的文件记录,它是 5 号文件记录,而且每个文件记录大小为两个扇区 1024 字节。所以,根目录的偏移地址为:
    0xC000 0000 + 0x5 * 0x2 * 0x200 = 0xC000 1400
    接着,我们跳转到 0xC000 1400 地址处,从从文件记录中,查找 80H、90H、10H 属性,因为是要获取 NtfsTest 文件夹的位置,所以,我们定位到 A0H 属性:

    我们可以从偏移 0x20 处获取 Data Run 的偏移地址 0x0048。然后,在偏移 0x0048 中获取 Data Run 数据:11 01 2C 00。从 Data Run 中,可以知道数据大小为 0x01 个簇,起始簇号为 0x2C。其中,0x2C 簇的偏移地址为 0x2C000。
    我们跳转到 0x2C000 地址处,开始按照标准索引头、索引项的含义,从标准索引头中获取第一个索引项的偏移位置,注意要加上0x18;然后,再从索引项中获取文件名称的偏移位置,查看名称是否为 NtfsTest 文件夹,若不是,则继续获取下一索引项的偏移位置,继续获取名称匹配。若找到名称,则获取文件的 \$MTF 参考号。
    按照上面的顺序,我们找到了 NtfsTest 所在的索引项:

    所以,我们可以获取到文件的 \$MTF 参考号为:0x58E0。那么,偏移地址为:
    0xC000 0000 + 0x58E0 * 2* 0x200 = 0xC163 8000
    我们继续跳转到偏移位置 0xC163 8000,接下来要寻找 520.exe 文件名称.:我们根据文件记录找到第一个属性的偏移位置,然后再根据属性大小,获取下一个属性的偏移位置。以此查找 80H 属性、90H 属性、A0H 属性。
    我们可以在 90H 属性中,从索引项中获取到 520.exe 的文件名称,然后可以得到 520.exe 文件的 \$MTF 参考号为:0x5A0B。那么偏移地址为:
    0xC000 0000 + 0x5A0B * 2* 0x200 = 0xC168 2C00

    然后,我们直接跳转到 0xC168 2C00 地址处,就是 520.exe 的文件记录了。我们直接找到 80H 文件数据属性,从偏移 0x20 获取 Data Run 的偏 移 0x40,然后在偏移 0x40 获取 Data Run 数据:32 42 2E C7 85 64 00。

    根据 Data Run,我们知道数据大小为 0x2E42 个簇,数据起始簇号为 0x6485C7,即偏移地址为:
    0x6485C7 * 0x8 * 0x200 = 0x6 485C 7000
    这样,0x6 485C 7000 地址处就存储着 H:\NtfsTest\520.exe 文件的数据。

    本文参考自《WINDOWS黑客编程技术详解》、内核篇、第十三章 文件管理技术、第三小节 文件管理之NTFS解析
    1  留言 2018-12-20 12:06:28

发送私信

你在背后说我,因为我走在你前面

15
文章数
8
评论数
eject