相信大家看过了前两期文章,已经对简单的查毒引擎的工作和PE文件格式有了一定程度的了解。但这些只是为了说明问题而制作的Demo工程。作为一个真正的杀毒软件,工作起来是更为复杂的。拿到一个文件不论三七二十一就开始查,不论从效率还是效果方面来说,都是不能接受的。要先对这个文件进行一定的预处理工作。因为对于一个文件可能存在很多种情况,它可以包含多个文件,比如说压缩包,或者是邮件MIME编码过的文件等等,也可能是经过打包的可执行程序,在这些文件里面,可能还包涵其他的压缩包或者编码的复合文件。杀毒软件在对文件进行了预处理之后,才能更有效的对文件进行病毒扫描工作。
大家不要认为预处理是不重要的,其实在杀毒软件中预处理是一件比较复杂也是非常重要的事情。一个好的预处理工作,可以使杀毒软件在面对经过了某些变化的病毒的时候游刃有余,大大增强一个杀毒软件的查毒数量和杀毒能力。
预处理的工作类型 那么,预处理到底都要做哪些事呢?大体来说来说,杀毒软件的预处理过程主要由两个阶段组成,第一阶段对文件格式进行识别,如果可以正常识别出文件格式,我们就可以判断是否需要进行病毒扫描。判断完格式后进入第二个阶段,第二阶段根据第一阶段传过来的文件格式,选择相应的处理程序。例如,对于压缩包我们就需要根据压缩包的格式进行解压操作,对于经过了加壳的文件,就调用脱壳的处理函数等等。
对于加壳的概念,读者如果不明白没有关系,下期我们会详细的介绍。
经过以上处理后,杀毒软件才会对处理后的内容扫描病毒。下面结合本次我们的代码来介绍以上的几个预处理步骤。由于预处理内容较多,篇幅所限,本次我们只介绍文件格式识别部分和解压缩处理部分。同时为了使我们的杀毒引擎的工程更接近一个真正的工程,我们将工程结构进行了较大的重新整合,加入预处理的部分。本次的升级改动较大,读者在看代码时就能感受到。
代码的变化 以前的所有的功能都放在一个项目中,这样不便于多人的协同开发,当我们的引擎功能越来越复杂的时候,就会发现很难进行协作开发,同时也不便于维护。这次,我们将整个工程变成了六个项目,日后,随着功能的扩展,我们还要不断往工程中增加新的项目,比如用于脱壳的项目,用于MIME编码处理的项目等等。
ConsoleUI是实现了我们以前的代码中Bav.cpp文件实现的内容,用于接受输入、输出扫描结果。同时ConsoleUI还要起到调用引擎的作用。我们引擎的界面现在还是控制台界面,读者如果有兴趣可以自己增加图形界面(MFCUI 甚至QtUI)。
VirusDB的代码大体上与我们的杀毒软件前两个版本类似。
FileOperate项目是对文件操作的包装。可以把它看作是一个跨平台的文件操作包装类,与反病毒并没有直接关系,不过目前只实现了win32部分。本工程所有的内部文件操作都是通过FileOperate来实现,这样可以保证我们的工程有很好的跨平台性。FileOperate是以FileObject 类为基类进行继承或多层继承。现阶段内有关文件操作的共有4个类,分别为CFileObject,CPhyFileObject,MemFileObject和CTxtFileObject。
其中CTxtFileObject 派生于CMemFileObject。CMemFileObject派生于CPhyFileObject, CPhyFileObject 派生于CFileObject。这次
一个比较大的变化就是区分了扫描对象(CScanObject)和文件对象(CFileObject),文件对象如上所述,是和反病毒没有直接关系的,而扫描对象是和反病毒直接相关的。文件对象也不再从扫描对象派生,CFileObject 成为文件操作的基类。每个扫猫对象里面包含一个可以被扫描的文件对象,可以是物理文件对象(CPhyFileObject)或者是内存文件对象(CMemFileObject)。和反病毒相关的操作,例如Compare()就封装在扫描对象中。
在CEngine 类中,从现在开始,我们需要区分ScanOneFile()和ScanOneObject()。ScanOneFile()是比较原始的级别,用来预处理通过文件遍历得到的单个文件,分析文件格式、解压缩。当预处理完成后,所有需要扫描的对象都被包装成了一个扫描对象(CScanObject)然后交给ScanOneObject()进行真正的扫描工作。
在ScanOneFile()中,我们用了一个堆栈来跟踪状态。每个栈顶的文件被取出来分析文件格式,如果是可执行的,就进行查毒,如果是压缩的,就解压缩。解压缩出来的文件形成新的内存文件对象,压入堆栈,等待处理。解压工作目前没有判断被解压的文件大小是否合适存放在内存中,这在将来需要改进,否则会被病毒利用,对反病毒软件进行攻击。这次ScanOneFile()的目的还是为了揭示预处理过程,代码的意义比较明白,但是效率不是很高。例如,如果把每次解压出来的内容立即判断格式,内存中就可以少储存一下东西,但是会把代码变得复杂,所以以后再增加相应处理。
Engine工程生成的动态库导出了以下五个函数。从下图我们还可以看到1 个全局的引擎对象,以后我们把他去掉的时候,就是引擎彻底支持多线程的时候。
VirusDB 工程生成的动态库导出四个函数,这些导出函数,在功能实现上还是使用引擎类CEngine和病毒库类CVirusDB 的代码,读者可参看相关代码。现阶段这两个动态库没有作动态加载,而是直接与ConsoleUI工程绑定了,但是为以后动态加载和支持多线程工作打下了良好的基础。而剩下的其他工程都是生成的静态库,目前是为了让工程比较简单,将来也可以很容易修改为动态库。还有许多修改在此就不一一介绍了,读者可以参看我们网站上提供的代码进行学习。
文件格式识别 文件格式识别是进行预处理过程中的第一阶段。为了适应纷繁复杂的文件类型,其操作可以做到非常复杂,所以在一个真正的杀毒软件中会编写大量的代码对每个文件进行详细的格式分析。对于文件格式的识别,最稳妥也是最准确的办法就是读取文件头部的内容,实际分析文件的格式,例如EXE、ZIP、RAR、JPG等。即便如此,还有很多无格式文件是很难识别的,例如TXT、BAT、COM.,还要引入更高级的统计或者代码分析才能识别。
在本期代码中,我们的文件格式识别的做法是首先判断文件扩展名,如果是Exe文件格式,就调用CParsePE类的BasicParse对PE文件的特征格式进行检查,以分辨出是否为真正的PE格式文件。同时,还将分析出来的结果存储在FSPE结构的变量中供后面的查毒工作使用,代码如下:
if( strExt.CompareNoCase("exe")==0 ) { pObject->m_eFormat = BF_WIN32_PE; // PE parser need BO_MEM_FILE object if(pObject->GetObjectType()==BO_MEM_FILE) { FSPE* pFSPE = new FSPE; if(pFSPE) { CParsePE cParser; i f ( c P a r s e r . B a s i c P a r s e ((CMemFileObject*)pObject->m_pFileObj, pFSPE) ) pObject->m_pFS = pFSPE; else delete pFSPE; } } } else if( strExt.CompareNoCase("com")==0 ) pObject->m_eFormat = BF_DOS_COM; else if( strExt.CompareNoCase("zip")==0 ) pObject->m_eFormat = BF_COMPRESS_ZIP; else if( strExt.CompareNoCase("rar")==0 ) pObject->m_eFormat = BF_COMPRESS_RAR; else pObject->m_eFormat = BF_UNKNOWN; |
对于其他诸如Zip,Rar 格式的文件,我们只是通过扩展名判断,并不进一步对文件格式进行检查。最后CFormatIdentify类的Identify方法会将分析出文件格式赋给传 入的CScanObject 对象中的m_eFormat 成员,以将分析结果传递给主引擎。如果我们分析不出来一个文件的格式,那么说明这种文件不在我们处理的范围之内,我们并没有必要对这种格式的文件进行扫描操作,就返回BF_UNKNOWN。
比如说MP3文件,我们就没有必要写特定的格式分析代码对它进行分析,因为他无法包含病毒代码。
压缩解压缩的概念
这个概念大家可能日常接触的比较多,也大多使用过压缩与解压缩软件。比如我们常用的WinRAR 和WinZIP 就属于此类。其作用主要是为了将一个或多个文件进行打包操作,在打包操作过程中会经过一系列特定的算法(例如常见的哈夫曼编码),将文件的体积变小。压缩软件的种类很多,各种压缩算法也很多。压缩主要是为了节省硬盘的空间或缩短网络传输的时间。当人们通过互联网传输文件时,经常会先制作成压缩包再传递,如果杀毒软件不具备压缩包查毒能力,是无法提供全面有效保护的。目前也有些病毒利用压缩包隐藏自身。所以对压缩包格式支持的数量成为评定一个杀毒软件好坏的标准。通常杀毒软件都支持比较常见的压缩格式,比如:arj,zip,rar,cab等,好的压缩
软件会支持10 数种格式。
杀毒软件是如何处理压缩包的 前面已经说过,通过预处理第一阶段的文件格式识别杀毒软件已经可以断定一个文件的类型。随后,引擎将要根据文件的类型对其进行解压缩操作,当然不同于日常我们使用解压缩软件的操作,将文件解压到磁盘或者硬盘中,而是将文件解压缩到内存里面。这样才能在内存中快速的对文件进行查杀,当然对于非常大的文件,是无法解到内存中的。所以通常都要事先判断文件的大小。在扫描内存文件的过程中,如果发现该文件被病毒感染,则对感染的文件进行清除病毒的操作。如果发现本身就是病毒或木马,则将其删除。最终在扫描完整个压缩包中的文件后,杀毒软件会根据前面的操作,将清除了病毒的文件和没有病毒的文件重新压缩成新的包。目前第三版的BAV还没有判断文件大小,也不支持杀毒及写回。事实上,也只有少数几家杀毒软件支持压缩包写回。有的时候反病毒软件用户会看到提示“请解压缩后杀毒”就说明在一种不支持写回的格式的文件里面发现了病毒。
Decompress 项目 该项目顾名思义,是用来对文件进行解压缩操作的。在的Decompress 项目中,我们使用了CodeProject 上开源的UnZip 类。该类主要实现了一下的函数
HZIP OpenZip(const TCHAR *fn, const char *password); HZIP OpenZip(void *z,unsigned int len, const char *password); HZIP OpenZipHandle(HANDLE h, const char *password); ZRESULT GetZipItem(HZIP hz, int index, ZIPENTRY *ze); ZRESULT FindZipItem(HZIP hz, const TCHAR *name, bool ic, int *index, ZIPENTRY *ze); ZRESULT UnzipItem(HZIP hz, int index, const TCHAR *fn); ZRESULT UnzipItem(HZIP hz, int index, void *z,unsigned int len); ZRESULT UnzipItemHandle(HZIP hz, int index, HANDLE h); ZRESULT SetUnzipBaseDir(HZIP hz, const TCHAR *dir); ZRESULT CloseZip(HZIP hz); |
其中比较主要的是OpenZip 函数、GetZipItem 函数和UnZipItem 函数。OpenZip 函数用来打开一个Zip 文件,并且返回一个HZIP 类型的句柄。随后,利用给GetZipItem函数的第二个参数传递一个-1的值,来获取该Zip 文件中共有多少个对象,并存储在ZIPENTRY. Index 中。这里介绍一下ZIPENTRY 结构,该结构定义如下:
typedef struct { int index; // Zip 文件中文件的索引 TCHAR name[MAX_PATH]; // Zip 文件中的文件名或文件夹名 DWORD attr; // 文件或文件夹的属性 FILETIME atime,ctime,mtime; // 文件访问,创建和修改时间 long comp_size; // 压缩后文件的大小 long unc_size; // 未压缩时文件的大小 } ZIPENTRY; |
随后我们就可以利用UnZipItem()函数将一个对象解压缩到磁盘或者内存中。通常对于文件大小不是很大的,我们就将它解到内存里进行病毒的扫描。
为了适应我们的需求,我们重新将代码进行了封装,成为了CZip 类。该类有3 个主要的成员函数和2个成员变量。
这里重点给大家介绍Open函数和Decompress 函数,首先看Open 函数
bool CZip::Open(CFileObject* pArchiveObj) { m_hZip = OpenZip(pArchiveObj->GetName(), NULL); if(m_hZip) { ZIPENTRY stZipEntry; GetZipItem(m_hZip, -1, &stZipEntry); m_nCount = stZipEntry.index; if(m_nCount) return true; Close(); } return false; } |
可以看到,这个Open 函数封装了打开Zip 文件和获取Zip文件中对象数量的操作。接下来再让我们看一下Decompress
函数。
UNZIP_STATUS CZip::Decompress(int nIndex, CMemFileObject* pFileObj) { ASSERT( nIndex>=0 ); ASSERT( pFileObj ); if( nIndex>=m_nCount ) return UNZIP_NOTHISITME; if( pFileObj ) { ZIPENTRY stZipEntry; if( ZR_OK == GetZipItem(m_hZip, nIndex, &stZipEntry) ) { pFileObj->SetName(stZipEntry.name); // the uncompressed size long nSize; if(stZipEntry.unc_size==-1) nSize = stZipEntry.comp_size*2; nSize = stZipEntry.unc_size; if(nSize==0) return UNZIP_ERROR; pFileObj->SetSize(nSize); i f ( p F i l e O b j - > O p e n ( N U L L , FO_FLAG_WRITE|FO_ATTRIBUTE_MEMTEMP) ) if( ZR_OK == UnzipItem(m_hZip, nIndex, pFileObj->GetBuffer(), nSize) ) return UNZIP_SUCCEED; } } return UNZIP_ERROR; } |
它主要封装了UnZipItem 的操作,将压缩的文件解到CMemFileObject对象的内存中,并将文件名和大小等主要属性也存入对象中。这样,引擎就可以对内存中的文件内容进行扫描,以判断该文件是否为病毒或者被病毒感染。
查毒能力的简单回顾:
BAV v1
标准的eicar.com
BAV v2
新增CIH 1.2
BAV v3
新增zip压缩包查毒,包括eicar.com,即eicar_com.zip,和
CIH。
本期主要就介绍以上这些内容,下期中我们会继续给大家介绍杀毒软件的预处理过程剩下的部分,包括脱壳和解码以及改进的格式识别。大家有任何问题,欢迎来我们的论坛进行讨论,论坛地址http://forum.netsvc.org,也可以写信到bav@netsvc.org。