zoukankan      html  css  js  c++  java
  • 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数据流的分析,我们可以看出它在系统安全方面存在一些隐患;通过分析它的格式,我们就可以编程实现批量的对文件进行检测,以排除恶意软件和后门。

  • 相关阅读:
    Docker手动搭建sentry错误日志系统
    Flask源码解析:Flask应用执行流程及原理
    django Rest Framework---缓存通过drf-extensions扩展来实现
    Python实现 -- 冒泡排序、选择排序、插入排序
    Python查找算法之 -- 列表查找和二分查找
    java设计模式之单例模式
    中文乱码解决方案
    web应用中Filter过滤器之开发应用
    web应用中的Filter过滤器之基础概述
    会话跟踪技术
  • 原文地址:https://www.cnblogs.com/feng801/p/1727865.html
Copyright © 2011-2022 走看看