黑白程式

黑白程式

导航

vc编程实现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:\n", FileName );
bIsFirst = false;
}
//读取数据流文件内容大小
ZeroMemory( Len64, sizeof( Len64 ) );
//将i64转为DWORD型
sprintf( Len64, "%u", sid.Size );
OrgFileLen = atol( Len64 );
printf( "\t\t[%ws] -> %u Byte\n", 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\"!\n", FileName );
return ;
}
if( !bIsDirectory )//不是目录
{
DWORD dwFileSize;
dwFileSize = ::GetFileSize( hFile, NULL );//得到文件大小
if( dwFileSize >= 0x80000000 )//文件太大,不分析(>=2G)
{
::CloseHandle( hFile );
printf( "File \"%s\" Too Big, Ignore It! (大于2G)\n", FileName );
return ;
}
if( dwFileSize == 0 )//大小为0
{
bIsDirectory = true;//如果文件大小为0,则按目录来处理
}
}
Count = ReadStream( hFile, bIsDirectory, FileName );
if( Count )
{
printf( "\t\t\tCount of Data Stream: %d\n\n", 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( "输入的目录太长!\n" );
return ;
}
if( *(Dir+DirStrLen) == '\\' )//在字符串最后添加"\*.*"
{
*(Dir+DirStrLen) = '\0';//去掉斜线
}
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.cFileName, ".." ) != 0 ) )//不是目录,即文件
{
char Path2[MAX_PATH+1];
sprintf( Path2, "%s\\%s", Dir, FindFile.cFileName );
GetFileDataStream( Path2, false );//查看该文件是否存在数据流
}
else if( strcmp( FindFile.cFileName, "." ) != 0 \
&& strcmp( FindFile.cFileName, ".." ) != 0 )//下一级目录
{//查看子目录中是否有数据流,并且递归查询该目录中的文件(如果有要求)
char Path2[MAX_PATH+1];
sprintf( Path2, "%s\\%s", Dir, FindFile.cFileName );
if( bIsRecursion )//如果需要递归
{
FindAllFilesInDirectory( Path2, true );//递归
}
else//不递归,只查看该子目录
{
GetFileDataStream( Path2, true );//查看该目录是否有数据流
}
}
} while( FindNextFile( hFile, &FindFile ) );//还有文件或目录
}
free( Path );//释放内存
}
此函数的目的就是递归查找目录中的所有文件及子目录中的文件,以对其进行检测,其中利用了FindFirstFile和FindNextFile API。需要我们传入参数有:Dir为目录指针,bIsRecursion为是否检测子目录中的文件。
到此,该程序主要部分就完成了,最后我用两个函数来分别对用户提供帮助,以及实现程序的入口参数的分析。
void Usage()//用法帮助
{
printf( "DataStreamFinder\n\n" );
printf( "Made By Adly\n" );
printf( "2007-10-10\n" );
printf( "It Can Scan NTFS Data Stream!\n" );
printf( "Usage:\n" );
printf( "DSF [-F File | [-S] Directory ]\n\n" );
printf( "-F Check a File\n" );
printf( " –S Recursion Scan Directory And SubDirectory\n\n" );
printf( "Example:\n" );
printf( "DSF -F C:\\boot.ini Scan \"C:\boot.ini\" File\n" );
printf( "DSF C:\\ Scan \"C:\\\" Directory's File And\n" );
printf( "SubDirectory But Recursion\n" );
printf( "DSF -S C:\\ Scan \"C:\\\" Directory's File And\n" );
printf( "All SubDirectory (Recursion)\n" );
}
void main( int argc, char* argv[] )
{
try{
if( argc != 2 && argc != 3 )
{
Usage();//帮助函数
return ;
}
if( argc == 2 )//扫描不递归的目录
{
if( strcmp( argv[1], "/?" ) == 0 )
{
Usage();
}
else
{
FindAllFilesInDirectory( argv[1], false );
}
}
else//加了两个参数来运行
{
if( ( argv[1][0] == '-' ) && ( argv[1][2] == '\0' ) )
//第一个参数的第一个字符为参数号
{
switch( argv[1][1] )
{
case 'f'://文件扫描
case 'F':
GetFileDataStream( argv[2], false );
break;
case 's'://带递归的目录扫描
case 'S':
FindAllFilesInDirectory( argv[2], true );
break;
default:
Usage();
}
}
else if( ( argv[2][0] == '-' ) && ( argv[2][2] == '\0' ) )
//第二个参数的第一个字符为参数号
{
switch( argv[2][1] )
{
case 'f'://文件扫描
case 'F':
GetFileDataStream( argv[1], false );
break;
case 's'://带递归的目录扫描
case 'S':
FindAllFilesInDirectory( argv[1], true );
break;
default:
Usage();
}
}
else//错误的参数
{
Usage();//帮助函数
}
}
}
catch(...)
{
printf( "\n程序发生异常!\n" );
}
}
为了防止未知的异常,我在其中还使用了try catch来进行捕获。
至此,整个程序就编写完毕了,它可以用来对整个分区的文件进行检测,如果有数据流,还可以看出数据流文件的大小,方便于网络管理员的查看。
结语
通过对NTFS数据流的分析,我们可以看出它在系统安全方面存在一些隐患;通过分析它的格式,我们就可以编程实现批量的对文件进行检测,以排除恶意软件和后门。

 

posted on 2010-05-05 11:48  黑白程式  阅读(609)  评论(0编辑  收藏  举报