背景之前在上一篇博文中 “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