基于NTFS的数据流创建与检测
文/ 饶建华
NTFS作为Windows的一种文件系统格式得到了大家的认识,但其所包含的NTFS数据流,了解的人却并不是很多,而它又有很强的隐藏性,这给系统的安全带来了隐患。有些网站服务器的分区为NTFS,当黑客入侵了该服务器后,可能会利用NTFS的特性将一些后门程序放到网站文件中,以达到通过IE等浏览器进行控制的目的。
本文是针对NTFS的一些分析,并写出程序来对其中的数据流进行检测,以解决这个安全问题。
NTFS数据流的创建原理
使用DOS命令“type 将要附加的数据流文件>附加到的宿主文件(或文件夹):数据流文件名”即可创建。比如现在想将“1.exe”附加到“2.txt”文件中,并且访问名为“3.exe”,则可运行命令:“type 1.exe>2.txt:3.exe”,这样就将“1.exe”作为NTFS数据流附加到“2.txt”上了。
NTFS数据流的特性
NTFS数据流在Windows系统环境下是不可显示的。如果上面的“2.txt”文件在附加NTFS数据流之前为4byte,则附加之后仍为4byte,大小不会改变,因为用户看不出任何效果,从而达到了很好的隐藏特性。
因为NTFS数据流只存在于NTFS文件格式的系统中,因而,如果将含有NTFS的数据流文件(如这里的“2.txt”)先放到非NTFS分区(如FAT32分区),然后再移动到NTFS分区,则数据流消失。如果将NTFS数据流的宿主文件(或文件夹)删除,则NTFS数据流自己也会删除。
NTFS数据流的应用
之前我们已经把“1.exe”附加到了“2.txt”文件中,附加后的文件名为“2.txt:3.exe”。此时,如果我们要运行这个数据流文件(“2.txt:2.exe”即原先的文件“1.exe”),则可使用命令:“start ./2.txt:3.exe”。要注意的是,这里的“./”是必不可少的,否则会报“拒绝访问”的错误。
NTFS数据流的检测
现在我们知道了NTFS数据流的特性,它具有很强的隐蔽性,用户很难发现,所以,我们现在就来写一个程序,用于检测它。我们可以利用BackupRead API来对其进行读取,除此之外,用到的API还有BackupSeek,此API用来对数据流进行定位。
本文的实现思路为:利用BackupRead读取,看其是否存在文件流,如果存在,则用BackupSeek跳过数据内容,再对NTFS数据流的名字进行读取,并显示出来。下面我们就开始编程实现它。
int ReadStream( HANDLE hFile, bool bIsDirectory, char* FileName )
{
WIN32_STREAM_ID sid; //数据流头结构
LPVOID lpContext = NULL;
//环境指针,读取数据流时必须为空
DWORD dwRead = 1; //实际读取的大小
int Success;
int Count = 0; //数据流的个数
UCHAR *Buffer; //动态分配的空间指针
bool bIsFirst = true; //是否为所查找到的第一个数据流名
ZeroMemory( &sid, sizeof( WIN32_STREAM_ID ) ); //清空sid
//数据流头大小,实际为20字节
DWORD dwStreamHeaderSize = (LPBYTE)&sid.cStreamName - (LPBYTE)&sid;
if( !bIsDirectory ) //如果不是目录,就执行此段
{//读取原始文件头
Success = ::BackupRead( hFile, (LPBYTE)&sid, dwStreamHeaderSize, &dwRead, false, false, &lpContext );
if( !Success ) //读取原始文件头失败
{
return 0;
}
//读取源文件内容
char Len64[25];
DWORD OrgFileLen;
ZeroMemory( Len64, sizeof( Len64 ) );
//将i64转为DWORD型
sprintf( Len64, "%u", sid.Size );
OrgFileLen = atol( Len64 );//跳过文件内容
DWORD FileLenL, FileLenH;
Success = ::BackupSeek( hFile, OrgFileLen, NULL, &FileLenL, &FileLenH, &lpContext );
if( !Success )
{
return 0;
}
}
while( dwRead )
{//读取源文件内容
char Len64[25];
DWORD OrgFileLen;//读取数据流头
Success = ::BackupRead( hFile, (LPBYTE)&sid, dwStreamHeaderSize, &dwRead, false, false, &lpContext );
if( !Success )
{
break;
}//读取数据流名称
Buffer = (UCHAR*)malloc( sid.dwStreamNameSize + 2 );//动态申请缓存
memset( Buffer, 0, sid.dwStreamNameSize + 2 );//缓存清空
Success = ::BackupRead( hFile, (LPBYTE)Buffer, sid.dwStreamNameSize, &dwRead, false, false, &lpContext );
if( !Success )
{
free( Buffer );//释放缓存
break;
}
if( dwRead )//读取数不为0
{
if( bIsFirst )//输出的第一个数据流名
{
printf( ""%s" Have Data Stream:
", FileName );
bIsFirst = false;
}
//读取数据流文件内容大小
ZeroMemory( Len64, sizeof( Len64 ) );
//将i64转为DWORD型
sprintf( Len64, "%u", sid.Size );
OrgFileLen = atol( Len64 );
printf( " [%ws] -> %u Byte
", Buffer, OrgFileLen );//结果输出,直接输出数据流名称,用空格分隔
free( Buffer );//释放缓存
Count ++;//数据流个数加1
}
//跳过数据流文件内容
DWORD FileLenL, FileLenH;
Success = ::BackupSeek( hFile, OrgFileLen, NULL, &FileLenL, &FileLenH, &lpContext );
if( !Success )
{
break;
}
}
return Count;
}
在上面的代码中,我自己写了一个ReadStream函数,用于实现主要的数据流查找代码。其中需要传入的参数有:hFile为已打开的文件句柄,bIsDirectory是否为目录(因为目录的NTFS数据流读取与文件的NTFS数据流读取有点不太一样),FileName为正在检测的NTFS数据流的文件名(用于显示,如果存在NTFS数据流)。
NTFS数据流有一个结构体:WIN32_STREAM_ID(数据流头结构),大小为20字节。而数据流的整体结构为:
首先,如果宿主是非空文件(空文件看作文件夹来处理,后面详细说):1)原始文件头(20字节)->2)文件内容->3)数据流头(20字节)->4)数据流名字(Unicode编码)->5)数据流内容……(后面就一直重复3、4、5,直到读取完毕)。
其次,如果宿主为文件夹或空文件:1)数据流头(20字节)->2)数据流名字(Unicode编码)->3)数据流内容……(后面一直重复1、2、3,直到读取完毕)。
第二种情况就第一种情况少了前面的两步,因为它们没有文件内容。需要说明的是,上面的BackupSeek是用于直接跳过文件内容的,如果想要读取出NTFS数据流的内容,也可以用BackupRead,但应该注意,当NTFS数据流文件很大的时候,分配内存可能失败!
当宿主为非空文件时,我们首先也是读取前20字节到WIN32_STREAM_ID结构体中,里面的Size就标识了文件内容的大小,我们可以根据这个值来跳过文件内容,从而读取文件流的信息。
接下来我又编写了一个GetFileDataStream函数来用于文件的打开、大小判断,及对上面函数ReadStream的调用。
void GetFileDataStream( char* FileName, bool bIsDirectory )
{
int Count;
HANDLE hFile = ::CreateFile( FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL );
if( hFile == INVALID_HANDLE_VALUE )
{
printf( "ERROR When Open File "%s"!
", FileName );
return ;
}
if( !bIsDirectory )//不是目录
{
DWORD dwFileSize;
dwFileSize = ::GetFileSize( hFile, NULL );//得到文件大小
if( dwFileSize >= 0x80000000 )//文件太大,不分析(>=2G)
{
::CloseHandle( hFile );
printf( "File "%s" Too Big, Ignore It! (大于2G)
", FileName );
return ;
}
if( dwFileSize == 0 )//大小为0
{
bIsDirectory = true;//如果文件大小为0,则按目录来处理
}
}
Count = ReadStream( hFile, bIsDirectory, FileName );
if( Count )
{
printf( " Count of Data Stream: %d
", Count );
}
::CloseHandle( hFile );
}
这个函数需要传入的参数有:FileName为待检测的文件名,bIsDirectory用于判断是否为目录。首先使用CreateFile打开文件(或目录),如果不是目录的话,则检测它的大小,这里取的是0x80000000,即2G,超过此大小的文件不分析(文件过大可能出错)。如果文件大小为0,则bIsDirectory=true,将其作为目录来处理,然后调用ReadStream函数,对其进行检测,最后关闭文件句柄。
为了实现对NTFS数据流的批量查找,之后我又实现了FindAllFilesInDirectory函数。
void FindAllFilesInDirectory( char* Dir, bool bIsRecursion )
{
GetFileDataStream( Dir, true );//查看目录是否存在数据流
ULONG DirStrLen = 0;
while( *(Dir+DirStrLen) ) DirStrLen++;//计算目录字符串长度
DirStrLen--;
if( DirStrLen+4 > (ULONG)MAX_PATH )//目录字符串过长
{
printf( "输入的目录太长!
" );
return ;
}
if( *(Dir+DirStrLen) == \ )//在字符串最后添加"*.*"
{
*(Dir+DirStrLen) = ;//去掉斜线
}
char* Path = (char*)malloc( MAX_PATH + 1 );//申请内存
memset( Path, 0, MAX_PATH + 1 );
memcpy( Path, Dir, MAX_PATH );
strcat( Path, "\*.*" );//查找当前目录下的*.*文件(即所有文件)
HANDLE hFile;
WIN32_FIND_DATA FindFile;
//开始查找文件夹下的所有文件及目录
hFile = FindFirstFile( Path, &FindFile );
if( hFile != INVALID_HANDLE_VALUE )//存在文件或目录
{
do
{
if ( !( FindFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY )
&& ( strcmp( FindFile.cFileName, "." ) != 0 )
&& ( strcmp( FindFile.cFi
补充:综合编程 , 安全编程 ,