百度一下 藏锋者 就能快速找到本站! 每日资讯归档 下载藏锋者到桌面一键访问

当前位置:主页 > 网络安全 > 基于文件系统的计算机反取证软件开发

基于文件系统的计算机反取证软件开发

所在栏目:网络安全 时间:04-12 16:58 分享:

计算机网络犯罪演化的各种技术与网络安全实施的各种防御技术之间的对抗,从来就没有停止过,相反,其对抗形势在不断的升级,每种安全技术的背后总会引起Cracker们更多的思索,即如何躲避各种安全技术的防御。计算机取证学作为一门新兴的交叉学科,越来越多的受到计算机安全与法律专家的重视,很多高校、研究机构甚至一些司法鉴定机构都在合作研发自己的计算机取证平台软件,以期侦查、提取出更多的电子证据,将Cracker对victim主机实施的各种攻击行为完整的重现。然而,以此同时,一种新的反计算机取证技术(Anti-Computer Forensics)也正悄然兴起,以对抗计算机取证技术。

反取证技术当前主要包括以下几种技术:(1)数据摧毁技术:一般的文件删除甚至删除后多次覆盖,并不意味着数据的摧毁。如MFM(Magnetic Force Microscopy)和STM(Scanning Tunneling Microscopy)技术可以从磁介质中重新找到被完全重写的数据。美国国防部DOD 5220.22-M(C and E)标准推荐使用3次以上的0、1交替覆盖方案。(2)数据加密技术:当犯罪分子需要保存一些有用数据时,为了使取证分析员无法理解此数据,Cracker们则会选择一种高强度的加密算法对这些数据进行加密处理。(3)数据隐藏技术:在反取证中应用隐秘术,选择一个正常的载体如视音频文件等,将机密信息嵌入其中,已达到混淆视听的效果。隐藏技术方式存在多种手段,而且复杂多变,这对取证技术专家而言是一个巨大的挑战,因为其需要考虑的面太广。(4)数据混淆技术:故意增添大量的冗余信息,使恶意文件难以识别或者采用一些特殊符号(如Backspace、Delete等控制符)来隐藏攻击。(5)防止数据创建技术等。

本文主要围绕数据隐藏技术子题,分析讨论如何在保证文件系统正常工作的前提下,将数据隐藏到指定文件系统的特定物理扇区位置,以逃避取证软件的检测。本文涉及到的文件系统主要包括Ext2/3和NTFS,给出了ExtX及NTFS下可隐藏区域,对每个隐藏区域进行详细分析;最后通过代码剖析的方式,讲解如何实现隐藏和反隐藏,并开发出针对文件系统的反取证软件。

Ext2/3文件系统的信息隐藏区域分析

Ext文件系统在设计之初考虑到未来系统的升级需要,文件系统中引入了一些保留位置以存储因升级而增添的一些额外元数据。在Ext2/3中,应用程序并不会对这些保留位置进行任何读写操作。当数据被写入到此区域也不会覆盖系统中存放的任一文件,同时也不影响文件系统的各种操作。因此,这些保留区域是信息隐藏的理想寄宿地。

Ext2/3的整体组织结构这里就不再赘述了,可以参阅本期的《Linux下基于Ext2/3的数据恢复程序设计》一文。其中存在组描述符表,包含了分区中所有块组的组描述符结构。其结构体定义如下:

struct ext3_group_desc{
__le32 bg_block_bitmap;  /* Blocks bitmap block */
__le32 bg_inode_bitmap;  /* Inodes bitmap block */
__le32 bg_inode_table;  /* Inodes table block */
__le16 bg_free_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__u16 bg_pad;
__le32 bg_reserved[3];};

在组描述符结构中共14个字节可用作隐藏信息:结构体成员变量bg_pad和bg_reserved没有任何含义,前者用来字节对齐,后者为保留字节。虽然这只有14字节大小,但我们完全可以将文件划分为多个14字节,分开存放在多个组描述符结构体中。

Ext2/3系统分区中,最前面的2个扇区共1024字节一般都为空,因为其设计中假设此区域为Boot loader所占用。因此,Cracker们可能会借用此区域隐藏电子证据,我们可以使用WinHex工具查看Linux下Ext2/3分区下前2个扇区内容,一般为空。

超级块结构在Ext2/3分区中占用两个扇区,组描述符表紧跟超级块,即在超级块所在块的下一块号中。若每块大小为4096字节,即8个扇区时,则会存在4个空闲扇区。因为引导扇区占2个扇区,超级块占2个扇区,而组描述符表在下一个块号中存放,故4、5、6、7扇区空闲共2048个字节。而且superblock结构体本身所占字节数也远远小于2个扇区,同样也可挪用它剩余的字节数存放电子证据。当未激活稀疏超级块特征时,每个块组中都存有超级块和组描述表的备份,这些区域在系统未崩溃之前,是不会访问的,完全可以使用这些区域来存放大量的隐私。

在索引节点表中,并不是每个inode节点都有实际意义,即对应一个特定的文件或目录。Ext2中,inodes 7~10共4个inode节点没有使用。每个inode节点占128字节,共512字节可以利用。Ext3中,inode 9~10两个节点可以用来存放隐私。

程序设计实现ExtX下机密信息的隐藏

如何通过程序设计读写Linux下的Ext2/3分区内容呢?在Windows XP/2000下,我们可以直接调用CreateFile打开指定分区并返回分区对应的句柄,然后调用ReadFile和WriteFile函数对指定扇区进行读写操作。在Linux下其实只需使用简单的C函数即可实现对指定扇区位置进行读写操作。调用fopen函数打开Linux下的Ext2/3分区,返回一个文件指针,然后调用fread和fwrite函数即可实现指定扇区的读写操作。下面以Ext3分区为例,笔者的Linux平台为Linux Redhat-2.6.18,原始状态下,引导扇区所占的2个扇区为空,下面笔者使用这几个函数实现将文件test.txt内容写入到这两个扇区中。

#include <fcntl.h>
#include <stdio.h>
#include <malloc.h>
#include "readfile.h"
int main(int argc, char *argv[]){
FILE *pfile, *pf;
char *buff = NULL;
struct ext3_super_block super_block;
//定义超级块结构体对象
pfile = fopen("/dev/sda9","rw+");
//打开Ext3所在分区sda9
if(pfile==NULL){
printf("open file failed!\n");
return -1;
}
pf = fopen("/home/hide.txt", "rb+");
buff = (char*)malloc(1024);
fread(buff, 1, 1024, pf);
fwrite(buff,1,1024,pfile);
//将hide.txt文件内容写入到Ext3引导扇区中
free(buff);
fseek(pfile,1024,SEEK_SET);
//移动文件指针,指向superblock起始扇区位置
fread(&super_block, 1, sizeof(super_block), pfile);
//填充superblock结构体
printf("%d\n", super_block.s_inodes_count);
fseek(pfile, 512 * 4, SEEK_SET);
//移动文件指针,定位到0号块组的第4个逻辑扇区处
pf = fopen("/home/hide2.doc", "rb+");
buff = (char*)malloc(512 * 4);
fread(buff, 1, 512 *4, pf);
fwrite(buff, 1, 512 *4, pfile);
//将文件hide2.doc内容全部写入到逻辑扇区4~7中
free(buff);
fclose(pf);
fclose(pfile);
return 0;
}

上面这段简单的代码即可实现将文件hide.txt和hide2.doc内容分别写入到Ext3分区的引导扇区和0号块组中superblock扇区后4个空闲扇区处。上述代码成功后,并不会影响Ext3文件系统任何性能,其照常正常工作。在不指定这些特定位置时,如EnCase、FTK及iLook等取证分析工具都难以检测此文件系统中存在任何异常或者隐藏有机密信息。图1和图2是笔者执行上述代码后,Boot引导扇区和superblock后4个空闲扇区的实际内容。

Boot引导扇区即0号块0扇区中内容
图1  Boot引导扇区即0号块0扇区中内容

Ext3分区下第4号逻辑扇区部分内容 
图2  Ext3分区下第4号逻辑扇区部分内容

由图可知,在0号块组的0号扇区及其4号扇区位置都已成功写入了文件内容。覆盖上面提到的保留区域,需要注意不要修改其它区域,否则Ext 2/3系统会出现故障,系统无法启动,如在覆盖Boot扇区时,意外修改了superblock则会引发异常。

总结上述分析,我们完全可以通过C++ Qt3编程设计出一个取证与反取证软件,实现对这些区域的覆写与内容提取,选择一个指定的文件,然后选择上述提到的可覆写区域进行覆盖,当文件内容大于选定的覆写区域时,则提示覆写失败,拒绝写入操作。同样,也可对这些区域进行扫描读取,查看这些区域是否存在机密信息。

NTFS文件系统下信息隐藏区域分析

NTFS文件系统中,其核心结构为主文件表MFT,存储了文件的所有基本信息,包括文件MAC时间、文件大小、文件内容或其存放位置等。MFT占用2个扇区共1024字节,一般情况下,文件对应的MFT表项总存在或多或少的空闲空间。此类情形主要发生在:(1)对应的MFT表项为非目录文件表项且文件内容很大(即不足以在MFT表项中存放),此时MFT表项只需要描述文件的相关属性信息(因为若其为目录文件,则90属性下可能会占用大量的字节空间来描述它的子目录及子文件信息),而且80属性下也只会占用小部分字节,即DataRuns来描述文件内容的存放位置。(2)MFT表项为目录文件表项,但其下对应的子目录及子文件很少,或者其下的子目录及子文件均已删除(当文件删除后,其父目录下的MFT表项中90属性内容会被清除)。一般情况下,MFT只占用前一个扇区的一部分内容,后一扇区其本为空。而在NTFS下,每个文件至少对应一个MFT表项,假设NTFS分区下存在10000个文件,且每个文件平均都存在1个扇区的空闲空间,则总空闲大小为10000*512=5000Kb,可以利用此5000Kb存放大量的隐私信息。

当文件大小大于1500字节时,文件会被存放到指定的簇号中。一般情况下,簇大小为4KB即8个扇区大小。显然,当文件介于1500字节与4KB大小之间时,会存在一部分的空闲空间;而且文件大小正好为簇大小整数倍的概率太小,故总存在一些扇区是空闲的。注意,扫描扇区时,读者可能会发现扇区空闲的几率很小,几乎都存在数据。其实这是一种假象,设想以下情形:文件A占用3个簇大小空间,用户在某一时刻删除了此文件,然后再创建一个新文件B,B的大小为9Kb,假设文件B正好从文件A原来所占的起始簇开始覆盖A的内容,而B文件在第3簇中只占用1Kb大小,故剩余6个扇区的内容并没有覆盖,但也不为空,这6个扇区仍然存放着A文件的部分内容。文件末簇遗留的空间往往存在几个扇区大小,将分区下所有的类似文件遗留的空间累加起来,将会是一个很大的空闲空间,足以被利用以存放大量的隐私信息。

现已经知道文件在末簇总会存在一定的空闲空间,但扫描分区时又很难发现这些空扇区,那么如何判断哪些文件存在空闲空间呢?进一步分析,指定文件可利用的扇区数有多少呢?这里我们可以遍历整个分区下的所有MFT表项,MFT表项的80属性下存放了指定文件的实际大小:相对80属性头偏移0x30位置的4字节内容即为文件实际大小;而文件所占簇数则存放在80属性的DataRuns簇运行结构列表中。根据文件实际大小和文件所占簇数,即可确定文件末簇空闲空间大小,此时我们就可借助WriteFile函数和SetFilePointer函数对指定的空闲扇区进行写入操作,实现机密数据的隐藏。某一文件大小及所占簇数如图3所示。

某一文件对应的MFT表项中80属性的内容
图3 某一文件对应的MFT表项中80属性的内容

由图可知,文件实际大小为0x15BA3E=1423934字节,故其所占簇数N=1423934/4096+1=348,末簇所占字节数为M=1423934 % 4096=2622,故此文件末簇剩余空间为:4096-2622=1474字节。从图3中也可以得出文件所占簇数N=0x15C=348。根据文件的起始簇号0x20B284=2142852,通过SetFilePointer函数将文件指针移到(2142852+348-1)*8+2=17145594号逻辑扇区处,再调用WriteFile函数对此扇区写数据,即可利用图3中的文件隐藏1474字节的机密信息。OK,原理部分就谈到这里,下面将重点分析代码设计过程中的关键技术,通过代码剖析讲解如何开发基于NTFS的反取证软件。

NTFS下反取证软件设计与研发

因为MFT下剩余的空间往往很小,每个子MFT剩余空间平均为512字节左右,而文件末簇剩余的空间往往为几个KB大小。故为了使得文件隐藏与提取过程不至于将文件分离成多个小片,应尽量选择较大的空闲扇区进行隐藏。因此,笔者研发的反取证软件只分析NTFS下的文件末簇,这些文件末簇剩余的空闲扇区足以隐藏文件信息。

如何寻找NTFS分区下所有文件的末簇是否存在空闲空间呢?因为每个文件末簇剩余的空间是不一样的,而且可能存在有些文件的末簇正好被文件全部使用完的情况。故需要判断每个文件末簇的使用情况;而且在实施文件隐藏的时候,我们肯定需要计算出文件隐藏的起始扇区数,然后才能调用WriteFile写文件到指定的扇区中,故同样需要遍历每个文件。下面给出遍历每个子MFT表项的代码。

void CNtfsTree::ConstructMFTTree(CNtfsTree *pDirHeadTree, DWORD dwStartSec,DWORD dwTotalSec){
DWORD dwBytesPerSec,dwSecPerClus,dwTotalSecTemp;
BOOL isDirectoryFlag; //标记是否为目录
wchar_t fileName[256] = {0}; //暂存文件名
char MFTFlag[5] = {0};
LPBYTE lpBuffer,lpTempBuffer;WORD wMFTHeadAttrLen = 0;
DWORD dwMFT10AttrLen,dwMFT20AttrLen,dwMFT30AttrLen,dwMFT40AttrLen,dwMFT50AttrLen;
DWORD dwMFT60AttrLen,dwMFT70AttrLen,dwMFT80AttrLen;
isDirectoryFlag = FALSE; 
dwBytesPerSec = pDirHeadTree->m_dwBytesPerSec;
dwSecPerClus = pDirHeadTree->m_dwSecPerClus;
dwTotalSecTemp = dwStartSec + dwTotalSec;
while(dwStartSec < dwTotalSecTemp){  //循环遍历每个子MFT表项
if(g_processBlock > g_totalMFT) break;
g_showDlg.ShowProcBlock(++g_processBlock); //显示扫描的块号
lpBuffer = (LPBYTE)malloc(2 * dwBytesPerSec);
//分配1024字节空间即MFT表项大小,读取分区下指定扇区dwStartSec位置存放的内容,即MFT表项内容,共1024字节,后面会给出如何对指定扇区进行读写的代码
ReadDisk(pDirHeadTree->m_hPartition, dwStartSec, 2, lpBuffer);
memset(MFTFlag, 0, sizeof(MFTFlag));
memcpy(MFTFlag, lpBuffer, 4);
if(strcmp(MFTFlag, "FILE") != 0 || *(LPWORD)(lpBuffer + 0x14) == 0){
dwStartSec += 2;  //直接跳过此MFT,忽略非MFT表项
free(lpBuffer);lpBuffer = NULL;continue;
}
//忽略已删除文件及目录文件,这里只考虑正常文件
if((*(LPWORD)(lpBuffer + 0x16) == 0) || (*(LPWORD)(lpBuffer + 0x16) == 2) || (*(LPWORD)(lpBuffer + 0x16) == 3) ) {
dwStartSec += 2;  //直接跳过此MFT,忽略正常文件
free(lpBuffer);lpBuffer = NULL;continue;
}
wMFTHeadAttrLen = *(LPWORD)(lpBuffer + 0x14);
//获取$MFT头部的属性长度
CNtfsTree *pNTFSTree = new CNtfsTree;
//创建一个新节点,存放文件属性信息
pNTFSTree->m_dwSecPerClus = dwSecPerClus;
pNTFSTree->m_hPartition = pDirHeadTree->m_hPartition;
//保存NTFS分区句柄
pNTFSTree->isDirectory = FALSE;
lpTempBuffer = lpBuffer;
lpTempBuffer += wMFTHeadAttrLen;
if(*(LPDWORD)lpTempBuffer == 0x10) {//表示存在属性
DWORD dwFileAttr;  //获取文件属性状态
DWORD dw10AttrOff = *(LPWORD)(lpTempBuffer + 0x14);
dwFileAttr = *(LPWORD)(lpTempBuffer + dw10AttrOff + 0x20);
if(dwFileAttr == 0x22){
//忽略隐藏文件,这里只考虑可读写的正常文件
delete pNTFSTree;pNTFSTree = NULL;
free(lpBuffer);lpBuffer = NULL;
dwStartSec += 2; 
continue;
}
dwMFT10AttrLen = *(LPWORD)(lpTempBuffer + 0x04);
lpTempBuffer += dwMFT10AttrLen;
}
if(*(LPDWORD)lpTempBuffer == 0x20){  //表示存在属性
dwMFT20AttrLen = *(LPWORD)(lpTempBuffer + 0x04);
lpTempBuffer += dwMFT20AttrLen;
}
if(*(LPDWORD)lpTempBuffer == 0x30) { //表示存在属性
dwMFT30AttrLen = *(LPWORD)(lpTempBuffer + 0x04);
//获取$MFT的属性总长度
lpTempBuffer += dwMFT30AttrLen;
//测试是否存在两个属性
if(*(LPDWORD)lpTempBuffer != 0x30)  lpTempBuffer -= dwMFT30AttrLen; 
DWORD NeedBytes;
DWORD dwParentMFTNum; //保存父目录文件参考号
dwMFT30AttrLen = *(LPWORD)(lpTempBuffer + 0x04);
//获取$MFT的属性总长度
DWORD AttrValOffset = *(LPWORD)(lpTempBuffer + 0x14);
//得到属性值的偏移
lpTempBuffer += AttrValOffset; //属性值偏移
dwParentMFTNum = *(LPDWORD)(lpTempBuffer);
//得到父目录的文件参考号
pNTFSTree->m_dwParentMFTNum = dwParentMFTNum;
DWORD fileNum = *(lpTempBuffer + 0x40);
//直接转到描述文件名偏移处,获取文件名属性
memcpy(fileName, lpTempBuffer + 0x42, fileNum * 2);
NeedBytes = WideCharToMultiByte(CP_ACP, NULL,fileName, wcslen(fileName), NULL, 0, NULL, NULL);
WideCharToMultiByte(CP_ACP, NULL, fileName, wcslen(fileName),
pNTFSTree->m_fileName, NeedBytes , NULL, NULL);
memset(fileName , 0, sizeof(fileName));
pNTFSTree->m_dwStartSec = dwStartSec;
//保存当前文件的MFT起始扇区
lpTempBuffer += dwMFT30AttrLen - AttrValOffset;
//直接跳转到属性后面的属性
}
if(*(LPDWORD)lpTempBuffer == 0x40){  //表示存在属性
dwMFT40AttrLen = *(LPWORD)(lpTempBuffer + 0x04);
lpTempBuffer += dwMFT40AttrLen;
}
if(*(LPDWORD)lpTempBuffer == 0x50){  //表示存在属性
dwMFT50AttrLen = *(LPWORD)(lpTempBuffer + 0x04);
lpTempBuffer += dwMFT50AttrLen;
}
if(*(LPDWORD)lpTempBuffer == 0x60){  //表示存在属性
dwMFT60AttrLen = *(LPWORD)(lpTempBuffer + 0x04);
lpTempBuffer += dwMFT50AttrLen;
}
if(*(LPDWORD)lpTempBuffer == 0x70){  //表示存在属性
dwMFT70AttrLen = *(LPWORD)(lpTempBuffer + 0x04);
lpTempBuffer += dwMFT50AttrLen;
}
LPBYTE tempBuff = lpTempBuffer;  //暂存指针值
if(*(LPDWORD)tempBuff == 0x80){ //表示存在属性
dwMFT80AttrLen = *(LPWORD)(tempBuff + 0x04);  //得到属性总大小
if(*(tempBuff + 0x08) == 0) {
//表示为常驻属性,直接忽略。因为在常驻属性下,文件内容直接存放在MFT表项的80属性下,故不存在末簇情况
delete pNTFSTree;pNTFSTree = NULL;
free(lpBuffer);lpBuffer = NULL;
dwStartSec += 2; continue;
}
else { //表示为非常驻属性
WORD RunValOffset = *(LPWORD)(tempBuff + 0x20);  //运行偏移
DWORD runBytes = dwMFT80AttrLen - RunValOffset;
pNTFSTree->m_fileLen = *(LPDWORD)(tempBuff + 0x30); //文件实际长度
tempBuff += RunValOffset;
//读取簇运行列表,获取文件的起始扇区及所占簇数,只有通过它才可以分析出文件末簇剩余多少空闲空间
GetContentUseClus(tempBuff, runBytes, pNTFSTree);
DWORD len = pNTFSTree->m_fileLen;
DWORD dwFileTotalSec = 0, dwFileSec= 0;
DWORD dwFileSecNum = 0;
double dwFileFreeSize = 0.0;
unsigned int vect_size = pNTFSTree->pVRunVal.size();
for(unsigned int i=0;i<vect_size; i++){
//计算出文件所占扇区总数
dwFileTotalSec += pNTFSTree->pVRunVal[i]->dwTotalSecNum;
}
if(len % 512 == 0) dwFileSecNum = len / 512;
else dwFileSecNum = len / 512 + 1;
//计算出文件实际大小所占扇区数
dwFileFreeSize = (double)(dwFileTotalSec - dwFileSecNum) / 2.0;
if(dwFileFreeSize < 1.0){
//忽略可隐藏区域小于1KB大小的文件
delete pNTFSTree; pNTFSTree = NULL;
free(lpBuffer); lpBuffer = NULL;
dwStartSec += 2; continue;
}
else{
//计算出文件所占扇区数(除去末簇所占扇区数)
dwFileLastClusSec = pNTFSTree->pVRunVal[vect_size-1]->dwTotalSecNum;
dwFileSec = dwFileTotalSec - dwFileLastClusSec ;
//获取文件末簇的起始扇区数
LastClusStartSec = pNTFSTree->pVRunVal[vect_size-1]->dwStartSecNum;
//计算出文件末簇未使用扇区的起始扇区数,即文件可隐藏的起始扇区数。用文件实际所占扇区数减去文件所占扇区数(除去末簇所占扇区数),即得出文件末簇实际使用的空间,然后再用文件末簇的起始扇区数加上末簇实际使用的扇区数即可计算出文件末簇未使用空间的起始扇区
DWORD dwHideStartSec = LastClusStartSec + dwFileSecNum - dwFileSec;
pNTFSTree->m_fileHideSize = dwFileFreeSize; //保存文件空闲大小
pNTFSTree->m_HideStartSec = dwHideStartSec; //保存可隐藏的起始扇区数
}
}
}
DWORD dwParentMFTNum = pNTFSTree->m_dwParentMFTNum; //保存父目录文件参考号
if(dwParentMFTNum != 0x05){
//若不是根目录文件,则需要深层次递归调用GetParentFileName函数,通过父目录的文件参考号定位到父目录的MFT表项,获取其父母录文件属性信息,以构建一个满足前面分析的各种条件的完整目录树
GetParentFileName(dwParentMFTNum, pNTFSTree)); //填充父目录文件名
}
if(!InsertNode(pDirHeadTree, pNTFSTree)){ //将文件节点插入到目录树中
for(int i = 0; i< pNTFSTree->pVRunVal.size(); i++){
delete pNTFSTree->pVRunVal[i];pNTFSTree->pVRunVal[i] = NULL;
}
delete pNTFSTree;pNTFSTree = NULL;
if(lpBuffer != NULL) free(lpBuffer);
dwStartSec += 2; continue;
}
dwStartSec += 2; //遍历下一个MFT表项
}
}

深层次递归搜索父目录文件属性函数GetParentFileName()和构建目录树的函数InsertNode()这里不再给出,下面给出如何对指定逻辑扇区进行读写操作的代码。

//从指定扇区号dwSector开始,读取dwLength长度的数据,放入lpData指向的缓冲区
BOOL ReadDisk( HANDLE hpartition,DWORD dwSector, DWORD dwLength, LPVOID lpData){
unsigned char*  lpBuffer = (unsigned char*)malloc( dwLength * 512);
DWORD dwCB;LARGE_INTEGER  offset;
offset.QuadPart  =  UInt32x32To64(dwSector ,  512);
SetFilePointer(hpartition,  offset.LowPart,  &offset.HighPart,  FILE_BEGIN);
ReadFile(hpartition, lpBuffer, dwLength * 512, &dwCB, NULL);
memcpy( lpData, lpBuffer, dwLength * 512);
free( lpBuffer);
return TRUE;
}
//从指定扇区号dwSector开始,写dwLength长度的数据
BOOL WriteDisk( HANDLE hpartition, DWORD dwSector, DWORD dwLength, char* lpBuffer){
DWORD dwCB;
LARGE_INTEGER  offset;
offset.QuadPart  =  UInt32x32To64(dwSector ,  512);
SetFilePointer(hpartition,  offset.LowPart,  &offset.HighPart,  FILE_BEGIN);
if(!WriteFile(hpartition, lpBuffer, dwLength * 512, &dwCB, NULL)){
DWORD error = GetLastError();
MessageBox(NULL,"写入文件失败!",NULL,IDOK);
return FALSE;
}
return TRUE;
}

ReadFile和WriteFile函数的第一个参数即NTFS分区的句柄值,需要调用CreateFile函数获取,代码为:
partition->m_hPartition = CreateFile(szDrive, FILE_READ_DATA | FILE_WRITE_DATA, FILE_SHARE_READ |FILE_SHARE_WRITE,NULL, OPEN_EXISTING, NULL, 0);

注意对逻辑磁盘操作时,szDrive文件名必须为“\\.\X:”形式。故每次New一个节点时,都需要保存分区的句柄值,以便后面调用WriteFile函数对指定扇区进行写入操作。下面给出文件隐藏代码:

void CAntiForensicsDlg::OnBnClickedHide(){
int selectItem = -1;
unsigned long HideFile_size = 0;
if(g_strFilePath == ""){ MessageBox("请先选择您需要隐藏的文件!");return;}
for(int index=0; m_listCtrl.GetItemCount(); index++){
if(m_listCtrl.GetCheck(index)) {
selectItem = index; //获取listCtrl控件中选中的文件
break;
}
}
if(selectItem < 0) {MessageBox("请选择寄宿文件,即作为载体实施隐藏的文件!");return;}
DWORD dwStartSecNum, dwTotalSec;
selectItem = m_listCtrl.GetItemCount() - selectItem - 1;
dwStartSecNum = vpNtfsTree[selectItem]->m_HideStartSec;
dwTotalSec = vpNtfsTree[selectItem]->m_fileHideSize * 2;
FILE *fp;
if((fp = fopen(g_SeclectFilePath,"rb+")) == NULL){
MessageBox("需要隐藏的文件打开失败!");return;
}
fseek(fp, 0L, SEEK_END);
HideFile_size = ftell(fp);//测定需要隐藏的文件大小
if(HideFile_size > dwTotalSec * 512) {
MessageBox("隐藏文件太大,执意操作可能会覆盖其它扇区,导致文件系统出错!");
return;
}
fseek(fp, 0, SEEK_SET);
char *buff = new char[HideFile_size];
fread(buff,sizeof(char), HideFile_size, fp);
WriteDisk(vpNtfsTree[selectItem]->m_hPartition,dwStartSecNum,dwTotalSec,buff);
delete []buff;fclose(fp);
}

OK,就分析这么多了,根据上面的分析再结合09年1期和5期的相关文章,即可很好的开发出一款反取证软件,下面看看软件测试结果和界面效果吧,如图4所示。

基于NTFS的反取证软件界面效果
图4 基于NTFS的反取证软件界面效果

下面笔者选择Persistent BIOS infection.txt文件作为寄宿文件进行隐藏,需要隐藏的文件内容为:“Can you help me? I want to hide some secret information.How can I hide it to make nobody can find it?”。从图4中可知,寄宿文件可利用的大小空间为1.00KB,足以隐藏前面的内容。隐藏的起始扇区数为85670号扇区,点击浏览按钮,选择需要隐藏的文件,然后点击隐藏按钮即可完成文件的完好隐藏,效果如图5所示。

文件test.txt成功寄宿到指定文件末簇的空闲空间
图5 文件test.txt成功寄宿到指定文件末簇的空闲空间

在进行文件隐藏时,尽可能的选择一个拥有较大空闲空间的文件作为寄宿文件,以便将隐藏文件很好的隐藏到寄宿文件中。软件设计过程中,要十分准确的计算出寄宿文件末簇空闲扇区的起始扇区号和空闲扇区数大小,否则将导致寄宿文件可能被覆盖掉了,导致原始的寄宿文件无法打开,甚至整个NTFS文件系统崩溃,那将是致命的。

借助正常文件末簇未使用完的空间实施文件隐藏,能够很好的躲避当前的计算机取证软件的分析,如EnCase等高级取证软件。除非取证分析员事先知道文件被隐藏到了指定文件中,否则是无法提取出隐藏的机密文件的。可能有些读者会想到NTFS下的文件流特性,即Alternate Data Stream(ADS)。在命令行提示符下键入“type c:\test.pdf > ExistAds.txt : test.pdf”命令即可实现在原文件ExistAds.txt中插入数据流文件test.pdf。而且在命令行下键入“dir /a”命令查看原文件大小时,文件大小并没有发生变化。通过这种简单的文件流方式,确实是可以实施文件隐藏,但是针对ADS的取证分析工具太多了,而且这种ADS方式健壮性很差,当将带有ADS文件流的文件拷贝到非NTFS分区时,此文件流会自动删除。即使你采用可压缩的ADS,虽然可以实施拷贝了,但是当你利用WinRAR工具解压时,会自动显示出文件中包含有流文件,故借助ADS流实施文件隐藏已经极为不可靠了。

小结

本文详细分析了利用Linux下主流文件系统Ext 2/3和Windows下NTFS文件系统实施文件机密信息隐藏的技术;详细给出了这两种文件系统的可隐藏区域点,并通过代码设计给出了研发基于文件系统反取证软件的关键技术,最后通过代码解析的方式,进一步阐述了编写代码中需要注意的细节。借助正常文件所占空间的剩余扇区数实施文件隐藏,可以有效的对抗当前的各种计算机取证工具的分析,健壮性良好。

基于文件系统的计算机反取证软件开发 免费邮件订阅: 邮件订阅

图片推荐

热点排行榜

CopyRight? 2013 www.cangfengzhe.com All rights reserved