查看: 2638|回复: 1
收起左侧

[分享] PE 结构分析技术在反病毒中的应用

[复制链接]
伯夷叔齐
发表于 2007-5-6 19:12:12 | 显示全部楼层 |阅读模式

PE的意思就是Portable Executable(可移植的执行体)。它是 Win32 环境自身所带的执行体文件格式,如:Exe文件,DLL 文件均为PE 格式。它的一些特性继承自 Unix 的Coff (common object file format)文件格式。"portable executable"(可移植的执行体)意味着此文件格式是跨win32平台的:即使Windows 运行在非Intel 的CPU 上,任何win32平台的PE装载器都能识别和使用该文件格式。当然,移植到不同的CPU上PE执行体必然得有一些改变。所有 win32执行体 (除了VxD和16位的Dll)都使用PE文件格式,包括NT的内核模式驱动程序(kernel mode drivers)。我们着重讨论的事Win32 平台下的PE文件。

  所有常见的PE结构定义在winnt.h头文件中都有。我们一般关心以下几项:

  IMAGE_DOS_HEADER
  IMAGE_FILE_HEADER
  IMAGE_OPTIONAL_HEADER32
  IMAGE_SECTION_HEADER
  IMAGE_NT_HEADERS
  IMAGE_IMPORT_DESCRIPTOR
  IMAGE_EXPORT_DIRECTORY
  IMAGE_RESOURCE_DIRECTORY

  由于我们的重点是从PE文件格式分析病毒表现和反病毒的原理,所以过多的关于PE文件格式的细节内容就不占用篇幅详细的介绍了,读者朋友可以参考相关的文章来学习其中的细节内容。我们只对要用到的PE文件结构中的内容进行简单的介绍。

  首先我们看到PE文件最开头的是DOS MZ Header部分,有了它一旦程序在DOS下执行,DOS就能识别出这是有效的执行体。并且操作系统中的PE文件装载器可以通过DOS MZ Header 中的内容定位到PE Header 的起始偏移量。在这里我们最关心的部分是IMAGE_DOS_HEADER的e_lfanew这个成员,通过它我们可以找到PE Header。

  紧随DOS MZ Header 部分的是DOS stub,这部分我们不关心,因为它只是为了在在不支持 PE文件格式的操作系统中,简单显示一个错误提示,如" This program cannot run in DOS mode”。这个部分通常由编译器自动生成,当然程序员也可以自定义该部分的代码。

  接下来是最重要的PE Header,PE Header 是PE 相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多PE 装载器用到的重要域。我们非常关心的入口点AddressOfEntryPoint 就在IMAGE_NT_HEADERS 中IMAGE_OPTIONAL_HEADER32 中。PE Header 后面的内容被称为节表,ection Table(节表)。 每个结构包含对应节的属性、文件偏移量、虚拟偏移量等。如果PE 文件里有5 个节,那么此结构数组内就有5个成员。如此PE装载器就可以根据节表定位每个节的属性和位置。PE文件的真正内容划分成块,称之为Section(节)。每节是一块拥有共同属性的数据,比如代码/ 数据、读/写等。

  PE 文件有效性检查

  前面我们已经对PE文件结构进行了简单的介绍,下面我们来分部份讲解杀毒软件引擎是如何根据病毒的原理和PE结构进行病毒查杀的。由于大多数感染型病毒都是感染的PE文件,因为这样才可以在PE文件运行的同时运行自身的病毒代码。从而继续感染其他的正常文件,以达到传播的自身的目的。所以从杀毒角度讲,应该先判断一个文件是否为PE结构,再去进一步决定应该使用何种方法对文件进行扫描处理。那么,如何判断一个文件是否为PE结构呢。

  我们可以检测PE文件中的每个数据结构,但是PE文件中包含的数据结构很多,如果逐一进行检查,则会耗费过多的CPU 时间,影响效率。通常的检查利用前面提到过的PE Header。PE Header实际上是一个IMAGE_NT_HEADERS STRUCT结构,定义如下:

typedef struct _IMAGE_NT_HEADERS {
 DWORD Signature;
 IMAGE_FILE_HEADER FileHeader;
 IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;  


  其中的Signature属性如果是等于"PE\0\0",那么就是PE格式的文件。所以杀毒软件在检测时只需要先通过DOS MZ Header 中的内容定位到PE Header 的具体位置,然后通过检查Signature 即可断定一个文件是否为PE文件。

//check "PE\0\0" signature
if( IMAGE_NT_SIGNATURE != pFSPE->m_pNtHeaders->Signature)
{
 pFSPE->m_bPEFile = false;
 return true;
}
pFSPE->m_bPEFile = true;  


  当然即使Signature 检测通过了,也可能不是一个有效的PE文件,还有各种可能性导致这个PE文件破损,您可以自己分析CParsePE类的代码,其中有各种不同的检测点。这个PE分析类还是不完善的,实际的反病毒引擎中的有效性检测都是经过多年积累的对于错误文件的分析,这是一个长期的过程。

  判断PE 文件中程序入口点是否异常

  在前面我们提到过入口点,即AddressOfEntryPoint,这个成员的含义是PE装载器准备运行的PE文件的第一个指令的RVA(相对虚拟地址)。若要改变整个PE程序的执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。很多病毒在感染了PE文件后,通常都会向PE文件中添加一部分代码, 然后更改P E 头中的AddressOfEntryPoint,将他指向的地址定位到病毒插入的代码处,这样每当这个文件运行时,病毒代码都会最先得到运行。

  一般情况,很多病毒都将插入到PE文件中的代码放到PE文件的后面,然后在代码尾部放置一条语句再跳回到原来PE文件真正入口点处。使得用户在毫无察觉的情况下执行病毒代码。杀毒软件可以根据PE文件入口点是否异常来判断文件是否有被病毒感染的嫌疑。通常入口点所指的相对虚拟地址比较靠前,不会在靠近文件末尾处,或者指向最后一个节后的内容,如果一个PE文件的入口点的指向不是这样,那么就说明这个文件有被病毒感染的嫌疑。当然,这种主观的判断不一定准确,但是可以算是一种判断的依据。上期我们提到的启发式扫描就会用到这样的特征来帮助判断未知病毒。

  有些病毒为了防止反病毒软件的这种探测,也想出了很多不修改入口点改变程序流程的方法。例如,改变原入
口点程序的代码,再跳转到病毒体。



根据PE 结构取特征码

  在上一期文章中我们说过,特征码的提取是将文件划分为不同的部分,然后从每部分中提取一定长度的内容作为特征码。这样提取特征码的方法存在着一个问题就是很多病毒的特征码有类似的部分,例如我们讨论的PE结构,很多PE文件开头部分有很大一部分是相同的,所以按照等分划分文件的方法来提取特征码是不理想的。这时我们考虑到了利用PE结构,从每个节中提取一定的内容作为特征码,或者以各种关键点为参照,在附近找特征码。这样一来,可以大大避免了上面所提到的等分划分文件提取特征码方法的弊端,增强了不同病毒间特征码的差异性。例如本次对于CIH 病毒的检测,就检查了PE Header 附近和入口点附近的特征。

  杀毒引擎的架构

  上期文章中,由于篇幅所限,并没有向大家介绍整个杀毒引擎的架构,只是详细讲解了关于查毒的部分代码片段。本期前面我们已经为大家讲解了PE 文件的格式等内容,所以为了能使大家对实际的杀毒引擎的工作有感性的认识,本次对于上次的代码进行了修改,加入了CIH 病毒的查毒功能,同时也增加了相关的操作内存扫描对象的类以及PE文件结构分析的类。可以说这次的程序已经很像一个真正的杀毒软件中所用的引擎了。下面就给大家详细的说明一下整个杀毒引擎的架构。

类名 实现功能
CEngine  引擎的主要类,实现杀毒主流程的控制。文件遍历,创建被查杀对象CScanObject,调用病毒库对象CVirusDB来检测,并将结果记录下来。
CFileObject  派生于CScanObject 对象,实现CScanObject 中定义的虚函数,用于物理文件的相关操作。
CMemFileObject  派生于CScanObject 对象,实现CScanObject 中定义的虚函数,用于内存文件对象的相关操作。
CParsePE  根据传入的CMemFileObject 对象参数,实现对PE文件的确定以及PE 格式的分析,
CScanObject  扫描对象基类,定义扫描病毒时需要用到的对各种病毒对象操作方法,但仅仅虚函数,真正的功能需要在派生于它的对象中进行实现。
CVirusDB  实现病毒库的加载和卸载操作,主要暴露了Search()方法,用于将扫描对象在本病毒库中检测。调用扫描对象里的Compare()方法进行特征码匹配法。
CVirusInfo 根据病毒ID获取病毒相关信息,由于用于演示,故只能返回病毒的名字。
  
  根据面向对象的设计方法,我们把整个杀毒引擎划分为不同的类。每个类对应的功能如下表所示那么,为什么需要定义这些类呢。这就要从我们拿到一个病毒时如何分析着手。首先查毒前提是我们要有一个已知病毒的特征库,其中存放了我们所有可以查得到的病毒的特征码。这样我们在遇见病毒时,才可以根据病毒库中的特征码分析这个文件(当然病毒有可能在内存中或者其他地方,这里只是举例)是否是病毒。所以我们定义了CVirusDB 类。

  随后我们就要对这个病毒进行分析,这个病毒有可能是磁盘上的某个文件,也有可能是邮箱中的一封信,也有可能是在内存中,对于不同的位置,就需要有不同的操作方法。这就是CScanObject 及其各个派生类(CFileObject,CMemFileObject)的由来,以后我们可能还会需要实现更多地派生类用于处理不同的对象,到时候只需要实现相应的方法,而不需要影响整个引擎的框架。

  在对病毒进行详细的分析时,就需要对格式进行分析。例如PE 格式分析,我们就要利用前面讲过的PE文件相关结构对其进行拆解。定义CParsePE的用途就是分析PE格式文件。我们以后还可以定义CParseOLE 等针对不同格式的分析器。在确定了一个病毒后,为了将病毒信息告知用户,又定义了CVirusInfo 对象去进行与病毒信息相关的操作。

  最后,对以上这些类进行统一的调度,决定何时应该创建或销毁某些对象等操作都交于CEngine 处理。并且CEngine类还实现了遍历被扫描对象以及与主程序传送扫描结果的功能。以便主程序可以最终生成病毒扫描结果反馈给用户。所以,整个项目的架构我们就使按照对这种流程的分析进行构建的。

  代码的变更

  通过前面总体的对各个类的功能以及病毒分析流程的概述,相信大家已经对我们的查毒引擎的架构有了一个整体的理解。但可能对于新增加的类以及PE分析部分的详细代码还不是很熟悉。同时由于本次对上次的代码进行了一定程度的修改,以适应功能的增加,所以某些重要部分也需要向大家解释一下。

  首先,先要说一下这次新增的一些重要结构体,其中最主要的就是FSPE

typedef struct tagFSPE
{
// control the struct version
INT nSize;
INT m_nSectionCount;
INT m_nImportCount;
bool m_bMZFile;
bool m_bPEFile;
PIMAGE_DOS_HEADER m_pImageDosHeader;
PIMAGE_FILE_HEADER m_pFileHeader;
PIMAGE_OPTIONAL_HEADER32 m_pOptionalHeader;
PIMAGE_SECTION_HEADER m_aSectionHeaders[MAX_SECTIONS];
PIMAGE_NT_HEADERS m_pNtHeaders;
PIMAGE_IMPORT_DESCRIPTOR
m_aImportDescriptors[MAX_IMPORTS];
PIMAGE_EXPORT_DIRECTORY m_pExportDirectory;
PIMAGE_RESOURCE_DIRECTORY m_pResourceDirectory;
// common use
LPBYTE m_pEntryPoint;
}FSPE, *PFSPE;  

  这个结构用于存储PE结构的主要信息,供之后的分析使用。

  其次就是我们新增的CParsePE类,这个类目前仅有两个方法,分别是:

bool CParsePE::BasicParse(IN CMemFileObject* pScanObj,
OUT FSPE* pFSPE);
DWORD CParsePE::AddrM2F(IN FSPE* pFSPE, IN LPVOID lpMemAddr);  
  BasicParse主要实现将一个内存中执行对象的PE结构进行分析存放于传入F S P E 结构体指针所指的变量中。而AddrM2F 主要实现的是虚拟地址到文件地址的转换,这个函数目前实现的还不太完善,有一些假设在极端的情况下是不正确的,我们会在以后改进。我们还从CScanObject 类派生了内存影射文件对象CMemFileObject,这个类实现了每一个CScanObject 中定义的虚函数,操作内存影射文件,同
时新增了自身的LPBYTE CMemFileObject::GetBuffer()方法,以便返回内存指针。

  这次有一个重要的改进,我们扩展了特征码的类型BAV_SIGN_TYPE。上次只有BS_PHY_FILE,也就是纯文件偏移,这次增加了BS_STRUCT_OFFSET,就是以某个结构的参考点偏移,实现了两个方法BS_SUB_NT_HEADERS和BS_SUB_ENTRY_POINT。BS_SUB_NT_HEADERS 是以PE
Header为参考点,BS_SUB_ENTRY_POINT是以入口点为参考点。

  在VirusDB 类的Search()方法中可以看到新增了的对于BS_STRUCT_OFFSET类型病毒特征的检查。这样就可以实现我们前面说过的对CIH 病毒的检查了。增加的代码如下:

case BS_STRUCT_OFFSET:
{
 // determine which parser we need
 if(pVSing->dwSubType>=BS_SUB_PE_BEGIN && pVSing->dwSubType<=BS_SUB_PE_END)
 {
  // PE parser need BO_MEM_FILE object
  if(pScanObj->GetObjType()!=BO_MEM_FILE)
  {
   ASSERT(false);
   bVirus = false; }
  FSPE stFSPE;
  CParsePE cParser;
  if( cParser.BasicParse((CMemFileObject*)pScanObj, &stFSPE) )
  {
   switch(pVSing->dwSubType) {
    case BS_SUB_NT_HEADERS:
     bVirus&=pScanObj-> Compare((LPBYTE)stFSPE.m_pNtHeaders+
      pVSing->nOffset,pVSing->nSize,pVSing->Signature, pVSing->eLogicOp);
     break;
    case BS_SUB_ENTRY_POINT:
     bVirus&=pScanObj->Compare((LPBYTE)stFSPE.m_pEntryPoint+pVSing->nOffset,
               pVSing->nSize, pVSing->Signature, pVSing->eLogicOp);
     break;
    default:
     bVirus = false;
     ASSERT(false);
   }
  }
  else
  {
   bVirus = false;
  }
 }
 else
 {
  ASSERT(false);
 }
}  
  CScanObject 及其继承类的Compare()方法进行了增加,增加了逻辑操作参数(BAV_SIGN_LOGIC_OPERATION)。也就是不仅仅可以判断相等(BL_EQUAL),还可以判断不等(BL_NOT_EQUAL),以后还可以扩展更多地逻辑比较参数。其他一些细小的变动,在此就不占用篇幅赘述了,读者可以参考代码进行分析。

  CIH 的识别

  这次增加了一个真正的病毒的识别,也给大家一个信心,这个演示的引擎离真实的情况也并不遥远。CIH的原理在网上有很多介绍,源代码也可以下载,我在这里就不介绍了。CIH 的识别我们取了3 个特征。首先是在PE Header前一个字节如果非0,就有可能感染了,CIH 自己
也利用了这个来判断。但是这个特征是不一定可靠的,没有感染CIH病毒的程序这个地方也可能因为各种原因变成非0,所以我们还加了两条代码特征。CIH会改变代码入口,指向自己,根据这个,我们取了入口点偏移特征,将sidt动作和后面挂文件系统钩子两个动作作为了特征,这样就比较可靠了。当然,这3个特征都集中在病毒头部,如果要更可靠,避免家族内的误报,还可以增加一些病毒体后面的代码。


转自(天极网)







buycard
发表于 2007-5-6 19:14:37 | 显示全部楼层
看了半天,原来是10年前的 技术,用于Win 9x上CIH 的识别
您需要登录后才可以回帖 登录 | 快速注册

本版积分规则

手机版|杀毒软件|软件论坛| 卡饭论坛

Copyright © KaFan  KaFan.cn All Rights Reserved.

Powered by Discuz! X3.4( 沪ICP备2020031077号-2 ) GMT+8, 2024-5-3 17:36 , Processed in 0.118820 second(s), 17 queries .

卡饭网所发布的一切软件、样本、工具、文章等仅限用于学习和研究,不得将上述内容用于商业或者其他非法用途,否则产生的一切后果自负,本站信息来自网络,版权争议问题与本站无关,您必须在下载后的24小时之内从您的电脑中彻底删除上述信息,如有问题请通过邮件与我们联系。

快速回复 客服 返回顶部 返回列表