第 3 章 插件系统

第 3 章 插件系统

反病毒插件是组成核心反病毒软件的若干小部件。它们为一些特定的任务提供支持,但通常并不是反病毒软件内核的核心组成部分。反病毒软件产品内核通过多种技术加载并在运行时使用插件。

插件不是核心库的重要组成部分,旨在强化由反病毒软件内核实现的若干功能特性。你可以将它们视为“功能拓展”。典型的插件例子有:PDF解析器、针对特定EXE文件壳(如UPX壳)的脱壳程序、Intel x86模拟器、基于模拟器的沙盒程序,以及结合其他插件实现的静态启发式引擎。这类插件通常在运行时加载,使用手动创建的加载系统,完成加密、解压、重定位和加载工作。

本章将介绍并分析典型的反病毒插件的加载过程,并逐个分析基于启发式的扫描算法、模拟器,以及基于脚本语言的插件。阅读完本章以后,你将能够:

  • 理解插件加载器的工作原理;

  • 分析插件代码并了解从何处入手查找漏洞;

  • 研究并运用免杀技术。

3.1 插件加载原理

每家反病毒公司设计和执行的插件加载方式各不相同。最常用的办法是分配读/写/执行(RWX)内存页,将插件文件内容解密并解压缩到分配的内存页中,必要时重载代码(Bitdefender就是这么做的),最后移除内存页的相关写入权限。这些新内存页构成了一个插件模块,被加入已加载插件列表中。

另外还有一些反病毒软件公司以动态链接库(DLL)的形式提供插件,依托操作系统的动态链接库加载机制(比如,使用Microsoft Windows操作系统中的API LoadLibrary),使插件的加载过程变得更简单。在这种情况下,为了保护插件代码及其内部逻辑,通常会对DLL文件的代码和数据进行混淆。比如,反病毒软件Avira将其插件DLL文件中的字符串全部进行了加密处理,当插件加载完毕后,又在内存中解密(通过一个简单的XOR算法和预存在插件代码中的固定key实现)。

在另一个案例中,卡巴斯基反病毒软件使用了一种完全不同的插件加载方式:插件更新文件以COFF对象文件格式下载到用户电脑中,接着它们又被链接到反病毒软件内核中。

接下来将讨论各类插件加载方式及其利弊。

3.1.1 反病毒软件的全功能链接器

卡巴斯基的更新文件以通用对象文件格式(common object file format,COFF)提供,而不是动态加载链接库或创建RWX内存页然后将插件的代码逻辑释放到内存页中。在解密和解压缩后,这些COFF文件与带有静态链接的所有插件链接到一起,同时新生成的二进制文件构成了新内核。从反病毒软件开发者的角度来看,该技术内存消耗少且启动速度快;但从另一方面来看,这需要卡巴斯基开发者们编写并维护一个全功能链接器。

提示 通用对象文件格式用于存储已编译的代码和数据,COFF文件用于链接阶段(最后的编译阶段)来生成一个可执行模块。

这些更新文件多为后缀名为*.avc的小文件,比如base001.avc。这类文件的文件头如下:

0000   41 56 50 20 41 6E 74 69 76 69 72 61 6C 20 44 61    AVP Antiviral Da
0010   74 61 62 61 73 65 2E 20 28 63 29 4B 61 73 70 65    tabase. (c)Kaspe
0020   72 73 6B 79 20 4C 61 62 20 31 39 39 37 2D 32 30    rsky Lab 1997-20
0030   31 33 2E 00 00 00 00 00 00 00 00 00 00 00 0D 0A    13..............
0040   4B 61 73 70 65 72 73 6B 79 20 4C 61 62 2E 20 31    Kaspersky Lab. 1
0050   36 20 53 65 70 20 32 30 31 33 20 20 31 30 3A 30    6 Sep 2013  10:0
0060   32 3A 31 38 00 00 00 00 00 00 00 00 00 00 00 00    2:18............
0070   00 00 00 00 00 00 00 00 00 00 00 00 0D 0A 0D 0A    ................
0080   45 4B 2E 38 03 00 00 00 01 00 00 00 E9 66 02 00    EK.8.........f..

在此案例中,ASCII文件头一开始为AVP Antiviral Database. (c)Kaspersky Lab 1997-2013,接着用字符0x00填充,然后更新包发布日期(Kaspersky Lab. 16 Sep 2013 10:02:18),而后又用多个0x00字符填充。偏移0x80是文件头的末尾,接下来就是文件的实际二进制数据。这些二进制数据采用简单的XOR-ADD算法加密。这些数据解密后,将会使用一种定制的算法解压缩。解压缩后,你将会得到一系列链接在一起的COFF文件(使用AvpBase.DLL库中的程序)以供目标操作系统使用。

目前似乎只有卡巴斯基反病毒内核正在使用这种加载插件的方式。本章稍后将详细讨论插件模块加载过程。

3.1.2 理解动态加载

动态加载是最典型的反病毒插件加载方式。这些插件文件不仅存在于容器文件中(比如Panda Antivirus的PAV.SIG文件、Avast的*.VPS文件或Microsoft Antivirus的*.VDB文件),也有可能分布在许多碎片文件中(比如Bitdefender)。这类文件通常会借助zlib进行加密(每个反病毒厂商会使用不同的加密方式)和压缩。在必要的时候,插件文件首次被解密后(比如,Microsoft并没有加密反病毒数据库,而仅仅是压缩了一下)会被加载入内存中。为了将插件文件加载到内存中,反病毒内核通常会在堆上创建一个RWX内存页面,将解密和解压缩后的文件数据复制到新创建的内存页面中,并调整内存页面的权限,必要时重新定位内存中的代码。

逆向分析采用动态加载技术的反病毒产品,要比采用静态对象链接技术(卡巴斯基采用的方式)的产品困难得多,因为系统启用的ASLR技术,使每次内核加载的数据块的内存地址是随机的。之所以让逆向过程变得困难,是因为在IDA内所有注释、指定的函数名等不会迁移到插件代码所在的你调试的新内存页面处。这里有一个能部分解决这个问题的方案。比如,使用开源的IDA插件Diaphora或收费版的zynamics BinDiff,在载入内存过程中,对包含注释和函数名的数据库进行二进制文件比较(这个过程也称作BindDiffing)。

通过BindDiffing,你可以从之前的IDA数据中,将相关信息导入到新的相同实例中去(从不同的内存地址中加载)。但令人感到窝火是,每次加载调试器以后,就要重新载入一次插件。还有其他一些开源插件,比如IDA的插件MyNav。你可以通过该插件的导入和导出功能编辑所需的插件代码。然而,使用MyNav插件同样需要你每次执行的时候重新载入一次插件。

有一些反病毒软件内核没有针对它们的插件采取保护措施,这些插件的相关程序库可以直接在IDA中打开并调试。但是这种情况少之又少,目前已知的只有Comodo Antivirus。

关于容器

一些反病毒软件会将所有更新文件置入容器文件中,而不是以单个文件的形式推送更新。如果你研究的反病毒软件使用了容器文件格式,在研究容器内部文件之前,需要好好研究该容器的文件格式。对于反病毒厂商来说,这两种方式均各有利弊。如果使用了容器封装,厂商的代码知识产权得以保护,但对于研究人员来说,研究过程中就需要逆向此类文件格式并编写脱壳程序。另一方面,以单个大文件格式推送更新,会让更新过程耗时耗力。以多个若干字节的小型文件推送更新,意味着更新过程可能只涉及一个有几字节或几千字节的文件而不是一个有数兆字节的文件。根据提供的更新文件的大小和数量,研究者可以大概了解反病毒软件内核的情况:代码越多意味着功能特性越多。

3.1.3 插件打包方式的利弊

在评估两种打包插件方式的利弊时,反病毒工程师和逆向分析者的观点往往不同。对于工程师来说,使用动态加载的方式是最容易实现、也是问题最多的一种办法。对于开发者来说,如果反病毒产品带有加密、压缩且需要动态载入内存中执行的插件,则有以下缺点。

  • 需要占用更多的内存。

  • 开发者必须编写特制的链接器,以便使这些由Microsoft Visual C++、Clang或GCC编写的程序能够兼容反病毒内核。

  • 使用动态加载的方式后,将增大开发者调试的难度。在这种情况下,开发者不得不使用hard-code INT 3 instructions、OutputDebugStringprintf来进行调试。不过这类调用并不适用于所有情况。比如,OutputDebugString方法在Linux和Mac OS X系统中就无法使用。另外,一些插件并不是使用本机语言编写的,比如那些针对Symantec Guest Virtual Machine(GVM)开发的插件。

  • 反病毒开发者不得不针对每一个操作系统开发不同的反病毒插件加载器。因此,尽管可以跨平台共用代码,但是如果操作系统增多(一般需要支持2~3个系统:Windows、Mac OS X和Linux),工作量就会翻倍。

  • 如果复制到内存的代码需要重新分配地址,开发的复杂程度和反病毒插件的加载时间都会增加。

由于相关文件需要被加密和压缩,开发这样一套系统的复杂度无疑会增加。另外,因为插件释放过程中生成的文件不是标准的可执行文件(比如PE文件、MachO文件或ELF文件),所以反病毒软件开发者不得不为反病毒插件开发一种特殊的签名认证机制。但是,反病毒软件通常并不会这么做。实际上,大多数的反病毒软件除了使用一种简单的CRC32算法检查外,并不进行任何其他额外的签名认证。

从一位反病毒工程师的角度来讲,对反病毒内核采用卡巴斯基式的方法有以下优点:

  • 消耗的内存较少;

  • 开发者可以借助任何调试工具调试编写的本机代码。

但同时,该方法也有以下缺点:

  • 对开发者来说,在反病毒内核中内置一个全功能链接器是一项不小的工作;

  • 针对反病毒软件兼容的平台,必须开发并持续维护相关链接器(尽管大部分代码可以跨平台共用)。

每家反病毒软件厂商都要根据自己的需求选择最适合的插件加载方式。遗憾的是,大多数反病毒厂商都会直接采用他们想到的第一种办法,而不考虑可能的后果、插件后期维护甚至是将插件移植到新的操作系统平台上(比如Linux和Android或Mac OS X和iOS)需要耗费多少精力。许多反病毒产品即是如此,在Linux和Mac OS X系统中使用相同的PE文件加载器。这些厂商的插件通常是仅针对当前支持的系统平台(Windows系统)而开发的非标准PE文件(这类插件使用PE文件头作为容器,但是相较于传统PE文件,却使用的是完全不同的文件格式)。他们从未考虑过将来将代码移植到别的系统平台上。许多反病毒厂商犯有同样的设计错误:过分关注对Windows平台的兼容。

然而,从逆向分析角度来说,这有很大的好处:我们的分析对象就是在机器上链接起来的运行反病毒产品的对象文件。有许多原因使反病毒产品的加载机制更容易被逆向分析。

  • 如果反病毒软件带有链接器,并以COFF文件格式的方式分发所有的插件文件,这些COFF对象文件可以直接用IDA打开。由于链接器的需要,这些文件自带调试符号。这类调试符号使得分析目标反病毒插件的内部结构变得相当容易。

  • 如果这些插件文件是简单的支持操作系统的二进制文件,在分析工作一开始,你就可以在IDA中加载查看。根据系统的差异,有时你可以获取到调试符号(最典型的有Linux、*BSD和Mac OS X系列)。

如果反病毒软件动态加载了非系统标准模块,你需要解密插件,将它们解密成可以被IDA或其他逆向分析软件加载的格式。另外,由于代码被载入堆中,而ASRL保护技术使这些模块经常会被载入不同的内存地址,除非IDA数据库被正确重定位,否则每次启动调试器,代码就会被定位到一个完全不同的位置,所有注释、函数名和之前反汇编过程中所做的标注都将丢失,整个过程真的非常繁琐无味。IDA在调试时并不能正确重定位代码。设置断点的时候也如此——如果你在一些指令处设下断点然后重新启动调试器,因为基础地址变更的缘故,这时候断点可能位于一个无效的内存地址处。

提示 你可能认为采用动态加载的方式可以更好地保护反病毒软件产品的知识产权。但是,在分析工作之初设置一些难度并不能起到任何保护的作用。使用动态加载技术只会使得产品分析更具挑战性,让前面几步分析过程略具难度罢了。

3.2 反病毒插件的种类

反病毒插件有许多种:一些仅仅是让反病毒产品能够支持更多的压缩文件种类,还有一些用于执行深度扫描和查杀修复感染型病毒(比如Sality病毒或Virut病毒)。一些插件可能是反病毒工程师的好帮手(因为这些插件可以帮助通用病毒查杀和感染修复,比如反汇编引擎、模拟器甚至是新的特征码种类),也可能属于一些全新、完全不同的插件种类,比如针对特定的反病毒虚拟机开发的反病毒插件(就像为了提取许可文件而解开受VMProtect保护的第一层程序)或为了支持某些脚本语言而开发的插件。对所有反病毒软件分析者来说,想要了解一款反病毒软件的工作原理,就必须理解反病毒插件的加载系统及其支持的插件类型。这是因为反病毒内核最有趣的地方不是内核本身,而是内核加载的模块。

接下来将详细介绍一些反病毒软件通常会带有的插件功能.

3.2.1 扫描器和通用侦测程序

扫描器是任何一款反病毒软件中最常见的插件类型。它是一款对某些文件格式、目录、用户和内核内存等开展特定种类扫描的插件。ADS(alternate data stream,文件数据流)扫描器是这类插件的典型案例。反病毒软件的核心内核通常仅能够使用操作系统提供的方法(CreateFileopen syscall)来分析文件和目录(有时,还会分析用户态内存)。但是在类似Mac OS X采用的HFS+和Windows采用的NTFS的一些文件系统中,文件可以隐藏在交换数据流中,所以内核程序无法检测这类文件。这类扫描器是拓展反病毒内核功能的插件,用于对在ADS中发现的所有文件进行枚举、迭代并加载其他扫描程序进行检测。

还有一些扫描器支持内存扫描,但反病毒产品并不直接支持本项功能,或通过内核驱动直接接触内核内存(正如Microsoft Antivirus做的那样)。另外一些种类的扫描器只能在一个插件被启动以后才能被加载。比如,当扫描器扫描文件的时候,如果在文件内部发现了一个URL链接,那么这时候URL扫描插件就会被加载。URL扫描器会检测文件包含的是否为恶意链接。

当你通过逆向工程技术查找反病毒软件内的安全缺陷或绕过反病毒软件的方法时,应该着重注意以下信息:

  • 一个文件如何以及何时被标记为恶意软件;

  • 文件解析器、解压缩模块和EXE脱壳程序是如何被加载的;

  • 什么时候调用通用检测程序扫描样本文件;

  • 如果反病毒产品带有沙盒功能的话,样本什么时候会被放入其中执行。

分析扫描器的时候,可以确定使用了哪些类型的特征码,以及这些特征码是如何用于文件或缓冲区扫描的。

另外还有一些插件类型可以归为通用扫描程序。这类扫描插件用于特殊文件、目录、注册KEY等的扫描(也有可能是修复文件感染)。比如,有一种插件被开发用于侦测Sality文件感染型病毒及其变种,为接下来的感染文件修复收集相关信息。如果可以的话,将这些信息整合进内部结构中,这样其他插件(比如感染文件修复程序)就可以直接使用了。

从逆向工程角度来说,当提到漏洞的产生时,通用扫描程序通常会表现得十分有趣,因为它们往往是安全缺陷的重要来源。处理复杂病毒文件的代码常常容易出错,当病毒流行势头过了以后,由于开发者们认为病毒几乎已经销声匿迹了,处理相关病毒的代码往往会几年都没有人维护更新。因此,潜藏在这类程序代码中的缺陷往往鲜有人问津。在用于查杀29A team、MS-DOC以及早期Microsoft Windows版本中病毒的通用扫描程序中发现可利用的安全缺陷,并不是一件稀奇的事情。

代码重用的安全实现方式

尽管通用查杀程序及其相应的感染修复模块似乎都是基础功能模块,但是一些反病毒内核并没有提供内部插件模块通信的方式。由于类似的功能短板,没有模块间交互通信的反病毒内核会在感染文件的修复模块中重复使用通用病毒检测模块中的相关代码。文件感染修复模块代码中的bug被修复后,可能不会同步修复通用查杀程序中重用使用的相关代码。也正因为这样,通用病毒检测模块中已经修复的bug在文件感染修复模块的代码中仍然存在。当使用扫描器修复感染文件逻辑的时候,相关bug就会被触发。感染文件修复模块中的bug是反病毒软件中较少涉足的领域之一。

3.2.2 支持文件格式和协议

一些插件用于分析文件格式和协议。这类插件提升了反病毒内核解析、打开和分析新型文件格式和协议(比如文件壳或EXE封装程序)的能力。旨在分析协议的插件通常会内置在网关或服务器产品中,在桌面个人版产品中则鲜有此类插件的身影。不过,有一些反病毒产品的桌面个人版也会提供分析基础网络协议(比如HTTP协议)的功能。

这类插件可以是针对UPX、Armadillo、FSG、PeLite或ASPack EXE packer的脱壳程序,可以是PDF、OLE2、LNK、SIS、CLASS、DEX或SWF的文件解析器,也可以是针对zlib、gzip、RAR、ACE、XZ和7z等文件的解压缩程序。反病毒内核包含形形色色的插件,这些插件正是反病毒产品bug的最大来源。Adobe公司的Acrobat Reader解析PDF格式文件出现漏洞的可能性有多大?如果你仔细去看CVE(Common Vulnerabilities and Exposure,通用漏洞)公开列表的话,就会发现正确解析这类文件格式的难度有多大了。因此,反病毒厂商会有多大的可能性去开发一个毫无bug的文件解析程序,用于解析一份1310页(除去目录还有1159页)的文档呢?

当然,上述可能性取决于反病毒工程师。PDF格式解析引擎已有提及,但在反病毒软件中,支持扫描Microsoft Word、Excel、Visio和PowerPoint文件的OLE2引擎,ASF格式视频引擎、支持Mac OS X操作系统平台下可执行程序分析的Mach0引擎、针对ELF可执行文件以及一长串更为复杂的文件格式的引擎,这些引擎不出bug的可能性又有多大呢?要回答这个问题很简单,由于反病毒软件的解析引擎插件要解析这么多文件格式,其中相关模块潜在的漏洞数量也十分庞大。如果再考虑一下反病毒软件需要支持的协议,其中有些协议还是没有相关文档规范或者规范模糊的(比如Oracle公司的TNS协议或CIFS协议),你就会幡然醒悟,这类模块对任意一款反病毒软件来说都是最易受到攻击的地方。

解析和解密插件的复杂性

反病毒软件经常需要处理不完整的代码。但是,在编写文件解析器或解密器时,反病毒工程师常常会把软件需要处理的文件当作结构正常的文件来处理。这导致反病毒软件在解析文件和协议过程中经常出错。另外,还有一些反病毒工程师想让反病毒软件的检测范围覆盖到边缘文件,这就大大增加了反病毒插件的复杂性,也给反病毒软件带来不少潜在的缺陷。安全研究者和反病毒工程师需要特别关注反病毒软件中的文件解析器和解密器插件。

3.2.3 启发式检测

启发式检测引擎位于核心反病毒引擎结构的顶端,用来与其他插件模块通信或综合其他插件提供的病毒检测信息。开源的反病毒软件ClamAV就是使用启发式检测引擎的典型例子之一。ZIP启发式引擎用来检测加密过的ZIP文件,在此过程中会使用到其他插件提供的前期信息。比如针对ZIP压缩文件开发的文件格式检测插件,在前期会尽可能多的收集与待检测文件相关的信息。ZIP引擎会首先通过扫描引擎确认ZIP文件格式可以被反病毒内核解析。启发式引擎会根据设置的启发式检测敏感级别,综合前期收集的信息,最终判定文件是否安全,是否需要对用户发出警告提示。

启发式检测引擎很容易产生误报,因为其实现原理是基于相关证据盘点文件是否恶意。比如,一份PDF文件看似畸形、十分可疑,因为它包含JavaScript代码,嵌入了通过多种加密手段的数据流(有一些甚至是重复的,比如针对一个附件重复采用了FlasteDecode或ASCII85Decode),并包含各类以ASCII、十六进制和八进制编码的字符串。因此,在扫描这类文件的时候,启发式引擎很有可能会认为该文件是一个漏洞利用攻击程序。但是,存在bug的PDF文件生成程序也会生成此类畸形文件,而Adobe Reader会忽略文件的畸形部分直接打开文件。这也是反病毒开发者面临的一大挑战:尽可能避免将正常软件生成的畸形鉴定为病毒而进行误报。

有两种启发式引擎:静态和动态。静态启发式引擎不需要执行样本来判定其是否是恶意文件,而动态启发式引擎则恰恰相反,需要在虚拟系统中执行程序并监控文件行为,比如开发基于Intel ARM架构的沙盒程序或JavaScript脚本程序模拟器。前面讨论的针对PDF和ZIP文件的检测可以归类为静态检测,在接下来的“基于权重的启发式引擎”一节,我们将讨论动态启发式引擎的相关技术。

本节讨论了反病毒软件中一些简单的启发式检测引擎的实现。然而,我们经常从反病毒软件中发现一些更为复杂的启发式引擎,后面会对此进行相关介绍。

  1. 贝叶斯网络

    贝叶斯网络(信度网络)是反病毒产品采用的使用统计模型代表一组变量的方式。这些变量通常是条件依赖关系、PE文件头以及其他一些启发式检测标志,如文件是否加壳或被压缩,部分文件熵是否过高,等等。贝叶斯网络用以揭示不同恶意软件间的概率关系。反病毒工程师会使用恶意文件和正常文件来训练基于贝叶斯网络的启发式病毒检测引擎。一般来说,贝叶斯网络只会在反病毒软件内部版本和一些零售版本中使用。尽管使用贝叶斯网络是一种强有力的启发式检测手段,但其误报率非常高。反病毒厂商通常会通过以下方式训练基于贝叶斯网络的启发式检测引擎:

    (1) 反病毒工程师将一个新样本传递给贝叶斯网络;

    (2) 检测引擎收集样本的启发式检测标志,并将检测状态保存在内部变量中;

    (3) 如果收集的标志与已知恶意软件样本族完全吻合或十分相似,贝叶斯网络就会给出相关评级;

    (4) 使用贝叶斯网络给出的相关评级数值,反病毒软件就可以判断对应样本文件“很有可能是恶意软件”或“很有可能是正常文件”。

    当然在使用贝叶斯网络的情况下,我们也会遇到相同的困惑:如果恶意软件和正常文件具有相同的PE文件头或其他启发式检测标志(压缩方式、熵等),或者几乎所有检测标志都相似怎么办?反病毒软件将会产生漏报(将恶意软件归为正常文件)。如果正常文件使用了加壳或虚拟机保护技术,而且启发式检测标志和一些恶意软件族相类似又会发生什么呢?结果显而易见:产生误报。

    和反病毒引擎实现的任何一种启发式引擎一样,绕过基于贝叶斯网络十分容易。用一句话来总结就是:让编写出来的病毒尽可能与正常文件类似。

    通常情况下,基于贝叶斯网络技术的反病毒引擎有以下两种目的:

    • 侦测可能是病毒的新样本;

    • 收集新型病毒样本文件。

    反病毒厂商通常会询问用户是否要加入反病毒社区,以便发送用户电脑上的可疑文件以供分析。在将相关可疑文件发送给反病毒厂商之前,反病毒软件会先使用贝叶斯网络筛选出一些潜在候选恶意文件(当可疑文件数量过多的时候)。

  2. Bloom过滤器

    Bloom过滤器是反病毒软件用来判断文件是否已知恶意软件的数据结构。Bloom过滤器会判断对应文件是完全不在恶意软件数据集中还是很可能在数据集中。如果其他插件模块收集的启发式标志通过了Bloom过滤器,那么样本绝对不是恶意软件,反病毒软件也不必将文件或缓冲区内容分发给其他更为复杂(检测速度会更慢)的检测模块了。只有无法通过Bloom过滤器的样本文件才会传递进入更复杂的启发式引擎检测模块。

    下面是一个假设的Bloom过滤器,通常被用来阐释其原理。Bloom过滤器背后有一个存储着特征MD5的数据库。假如在数据库中,有包含以下散列的样本:

    99754106633f94d350db34d548d6091a9fe934c7a727864763bff7eddba8bd49
    e6e5fd26daa9bca985675f67015fd882e87cdcaeed6aa12fb52ed552de99d1aa
    
    

    如果分析中的新样本文件或缓冲区内容不以9E开头,我们可以认为它不在恶意文件特征集当中,也不需要再发送给深度启发式扫描程序做检测了。但是,如果以9E开头,那么样本文件可能属于恶意文件特征集,这时候就需要进行更复杂的查询侦测来判定对应样本文件是否是恶意软件。上面的例子仅仅从理论层面阐释了Bloom过滤器的工作方式。在真实工作环境下,有许多更好的方式来判断对应样本文件的散列是否在已知恶意软件特征数据库中。

    几乎所有反病毒产品中的启发式检测引擎都会使用到基于散列(无论是加密散列还是模糊散列)的Bloom过滤器。总的来说,Bloom过滤器一般被用来判定样本文件是否需要进行更深层次的扫描或直接判定为正常文件。

  3. 基于权重的启发式引擎

    在许多反病毒引擎中都可以发现基于权重的启发式引擎。在插件收集完关于样本文件或待扫描缓冲区的信息后,启发式检测标志会被计算收集起来。接着,基于这些标志,反病毒引擎将会分配对应权重。比如说,样本文件在反病毒软件的沙盒环境或模拟器中运行。在此过程中,相关文件特征行为将会被记录。基于权重的启发式引擎将会对不同的文件操作行为分配不同的权重值(可正可负)。当针对样本文件执行的所有操作分配完权重值后,反病毒引擎会最终判定对应文件是否是恶意软件。举个例子,反病毒软件会将以下恶意软件行为记录:

    (1) 恶意软件读取了运行目录下纯文本格式文件内容;

    (2) 恶意软件弹出让用户进行确定或取消操作的对话框;

    (3) 从未知域名下载一个可执行文件;

    (4) 将可执行文件复制至%SystemDir%

    (5) 执行下载的文件;

    (6) 最终,样本文件运行一个用以结束自身进程并自删除的批处理文件。

    基于权重的启发式引擎会对上述步骤中的前两步分配负数数值(因为类似启动行为),但会为接下来的操作步骤分配正数数值(因为这些操作是典型的下载者行为)。当对每个文件操作行为分配了权重数值以后,基于用户的相关扫描配置,对应样本文件的最终权重将会被计算出来,从而判定文件是否是恶意软件。

3.3 高级插件

除了之前讨论的插件模块外,反病毒产品中还有许多各异的模块。本部分将介绍反病毒产品中常见的高级插件模块。

3.3.1 内存扫描器

扫描器是反病毒产品使用最多的插件。一个高级扫描器就是我们常能在反病毒产品中发现的内存扫描器。内存扫描器可以读取进程内存,通过特征码和通用检测等对内存中的缓冲区进行扫描。几乎所有反病毒软件都会提供形式各异的内存分析工具。

内存扫描器通常分为两种:用户态扫描器和内核态扫描器。前者通常扫描用户程序所在内存块,后者则扫描内核驱动、进程等。两者的共同点是都非常慢,并且经常只能在具体事件发生之后进行扫描,比如潜在的恶意程序启动之后。当然,大多数时候,用户可以使用病毒扫描引擎进行完整的扫描。同时,用户态内存扫描器也能被系统接口(比如基于Windows的操作系统中的OpenProcess和ReadProcessMemory)或者第三方内核态扫描器调用。

使用系统接口来调用用户态扫描器并不总是明智之选,因为它可以被其他程序干扰,恶意软件开发者们也有诸多方法来绕过它。例如,有些恶意软件已经预先写好了绕过扫描器的方法,比如进入休眠状态、删除部分特征文件或者直接阻止扫描。内建保护机制的恶意软件能直接使扫描器发生错误进而导致拒绝服务。这正是反病毒程序开发者不喜欢这种方式,而是更喜欢使用内核驱动程序来读取外部进程内存的原因。除非恶意软件与另一内核组件建立连接,否则我们无法得知进程的内存是否被读取。要读取内核内存,反病毒软件公司必须编写内核驱动程序。反病毒引擎研发公司已经开发出了能够同时读取用户进程和内核进程内存的内核驱动程序,相当于在用户进程与内核进程之间插入通信子层,以传递缓冲区内容至扫描程序进行分析。

当然,如果这些程序组件不经过安全验证也能造成不少的bug。如果内核驱动程序不验证是哪一个应用在调用I/O控制代码(I/O Control Code,IOCTL)来请求内核内存的读取权限,会出现什么样的状况?毫无疑问,这会造成任意应用读取内核内存的严重安全问题,任何知道这一通信层和恰当IOCTL的用户态应用都可以读取内核内存。如果内核驱动提供对内核内存的写入组件的话(通过额外的IOCTL),将会使得问题更加严重。

负载模块分析与内存分析

有些反病毒产品声称支持内存分析,但是这种表述并不准确。这些产品仅仅分析正在执行的进程和使用硬盘文件的负载模块。内存分析技术会被外界程序干扰,使用时需要相当小心,因为它能够被自身的调试引擎、文件检测引擎以及逆向引擎甄别出来,从而导致无法正常工作。在某种程度上说,这种设计有助于保护软件程序的知识产权。反病毒程序公司会尽量让自家产品静默地运行。一些公司干脆使引擎不去干扰正在读取内存的进程,因为这会妨碍合法应用程序的正常运行,他们的观点便是让反病毒引擎能够充分读取磁盘上的文件模块。

3.3.2 非本机代码

出于性能的考虑,反病毒软件内核通常使用C或C++语言编写,但也可以使用更高级的编程语言编写插件模块。一些反病毒产品使用.NET或其他需要使用虚拟机解释执行特定的编程语言来创建插件(比如通用检测插件、感染修复插件或启发式检测引擎)。反病毒厂商采取该项措施,有以下几个方面的考虑。

  • 复杂性 使用高级语言编写扫描程序、感染修复程序或启发式引擎会更容易。

  • 安全性 如果编写插件模块使用的语言运行在虚拟机中,在解析复杂文件格式或修复感染型病毒的过程中出现了bug,也不会影响整个产品,而只会影响进程运行的虚拟机、模拟器或解释器。

  • 调试能力 如果使用特定的编程语言编写通用扫描程序、感染修复程序或启发式引擎,且反病毒软件提供封装的API,反病毒软件开发者就可以使用对应编程语言提供的相关工具调试代码。

出于安全目的使用非本机语言编写程序时,上述第一和第三点原因常常被忽略。例如,一些反病毒产品会创建一个名为matrix的沙盒环境,来运行解析器和通用查杀程序的代码,而不是直接运行本机语言编写的代码。这也就意味着,如果反病毒软件中存在漏洞,比如存在一个缓冲区溢出,也不会直接影响到整个扫描器的工作(比如通常以SYSTEM或root权限运行的反病毒后台常驻程序)。反病毒软件采取的这项措施迫使攻击者们在编写反病毒软件漏洞利用程序的同时,为了能够绕过利用限制,而去研究相应的虚拟机。这往往需要多个漏洞利用程序。另一方面,一些反病毒产品创建一个完整的指令集,并提供了API接口,但没有提供调试代码的调试器,这给反病毒工程师的工作带了不小的挑战。

如果你向Symantec公司之前的老员工提起GVM(Guest Virtual Machine),他们将告诉你它的各种“劣迹”。在过去,GVM不允许通过调试器调试代码。这迫使开发者们发明独立的调试技术来搞清楚代码到底哪里出了问题。更糟糕的是,由于在这类虚拟机中没有针对相关代码的解释器或编译器,反病毒软件常常会将相关检测逻辑直接用汇编语言编写。在这种情况下,如果你使用一些熟悉的反汇编软件(比如OllyDbg、GDB和IDA)进行调试,就会了解反病毒行业中用户虚拟机技术的工程师少的可怜的原因了。

反病毒软件常用的非本机语言是Lua和.NET,一些反病毒软件厂商会因地制宜地针对自家虚拟机支持的格式编写.NET字节码解释器,还有一些厂商则会直接将现成的.NET虚拟机直接内置在他们的反病毒产品中;另外有些厂商会将Lua作为编程高级语言,因为Lua轻巧、运行速度快,同时能够很好地处理字符串,此外还允许在商业闭源版本的反病毒软件中被使用。

对于反病毒软件开发者来说,尽管使用非本机语言编写会带来难以调试的问题,但使用.NET类的语言(比如C#)比使用C或C++来编写相关程序要容易得多。另外很重要的一点是,显而易见,在程序出现bug的情况下,使用托管式语言会比非托管式语言要安全得多;如果代码在虚拟机内运行,漏洞利用程序编写者需要结合不止一个bug来突破虚拟机运行环境的限制,使漏洞利用的过程更加复杂。另外,相比使用C或C++语言编写的程序,使用托管式语言编写的程序出现漏洞的概率会小很多。

但从逆向分析角度来看,如果目标反病毒产品使用了某些虚拟机技术,那这真是一个噩梦。拿反病毒软件ACME AV来说,在开发过程中,该反病毒软件实现了自己的一套虚拟机,其大多数病毒侦测、感染修复以及启发式扫描程序都围绕这套虚拟机进行开发。但如果不是标准虚拟机的话,可怜的分析员就需要通过下列步骤进行分析了。

(1) 找到编写虚拟机使用的代码。当一位逆向工程师开展相关逆向工作时,有关虚拟机的信息当然必不可少。

(2) 找出虚拟机支持的所有指令集。

(3) 针对新找出的指令集,编写反汇编工具,这类工具常常会使IDA的模块处理插件。

(4) 找出反病毒插件模块程序释放的二进制文件位置(通常可以在插件安装目录文件或内存中找到),并将找到的二进制文件提取出来。

(5) 使用IDA或第3步中定制的反汇编程序着手分析运行在虚拟机中的相关插件。

但真实情况远远不止这些,可能还会更糟;虽然并不常见,但在Themida或VMProtect等软件防护工具中还是能见到。如果相关虚拟机随机生成,每一版本都会完全不同,那么分析代码的难度便会呈指数增加。因此,每当新版本的虚拟机发布后,新的反汇编工具,可能是模拟器或基于上一版本虚拟机指令集开发的逆向分析软件,需要被更新或彻底重写一次。对于安全研究者来说,问题还不仅限于此,如果开发者们都无法使用工具调试自己的代码,对于安全研究者来说就更不可能了。因此,他们需要针对这种情况编写一个模拟器或调试程序。

研究这类插件的过程十分复杂,但如果你选取研究的虚拟机已经被广泛使用,比如.NET虚拟机,就可能幸运地发现潜藏在角落里完整的.NET库或可执行文件,进而使用普通的反编译软件比如开源的ILSpy或其他商业版工具(.NET Reflector)开展逆向分析了。这样整个分析过程大大简化,你可以直接阅读高级语言(带有变量和函数名),而不是那些不太友好的汇编语言了。

3.3.3 脚本语言

反病毒产品可能会使用脚本语言来执行通用扫描程序、感染修复程序、启发式引擎,等等。脚本语言可能是Lua甚至是JavaScript。在之前的案例中,使用脚本语言执行前面提到的多个功能的原因是一致的:安全性、可调试性和开发复杂性。当然,使用脚本语言也有商业层面的考虑:招聘好的高级编程语言的程序员,要比招聘好的C或C++程序员容易得多。因此,新进入反病毒软件公司的工程师事实上并不需要了解如何使用C或C++甚至是汇编语言,因为他们只需要使用Lua、JavaScript或其他反病毒软件内核支持的脚本语言编写。这意味着,程序员只需要了解反病毒软件支持的API,就可以编写相关插件模块了。

和前面的示例一样,我们也从两个角度阐释反病毒产品中的插件模块通过脚本语言执行的方式:反病毒软件开发者角度以及研究者角度。对反病毒厂商来说,使用高级语言编写程序代码更容易,因为这样更安全,而且更容易招聘到好的程序员。对于逆向分析者来说,与一般的虚拟机技术相反,如果反病毒产品直接通过脚本执行相关操作,研究者只需要找到脚本在哪里,然后导出并开始分析真实的源代码。如果脚本被编译成了某种字节码,运气好的话,研究者就会发现反病毒产品中的虚拟机嵌入的是标准的脚本语言,比如Lua,接着找到一款已经编写好的反编译程序,比如开源的unluac程序。研究者需要针对脚本语言,对这些反编译工具做一些小的改动,以正确获取到真实的脚本代码,而这仅仅需要花费几个小时的时间。

3.3.4 模拟器

模拟器是反病毒软件中十分重要的一个部分。它们可以用来完成许多工作,比如分析可疑样本行为、对加壳或使用未知算法加密的样本做解包分析、分析嵌在文件中的Shellcode,等等。除ClamAV外,大部分反病毒引擎都会至少使用一个模拟器:Intel 8086模拟器。模拟器一般会借助其他加载模块(有时会和模拟器的代码写在一起)、引导扇区以及Shellcode模拟分析PE文件。一些反病毒产品也会用模拟器来分析ELF文件,但目前还没有发现有反病毒软件用模拟器来分析MachO文件。

Intel x86模拟器并不是反病毒引擎的唯一选择。一些模拟器也会用于ARM、x88_64、.NET字节码,甚至是JavaScript或ActionScript的分析。如果恶意软件进行了许多系统或API调用,那么模拟器的作用就会被削弱。这是因为模拟器会限制API调用的数量,以免其中断模拟过程。支持指令集及其相关架构就实现了模拟二进制文件功能的一半。另一半是要正确模拟API的调用。模拟器的另一项职责是支持真实操作系统或模拟环境的系统调用或API调用。通常,反病毒软件会支持调用类似ntdll.dll或kernel32.dll的Windows动态链接库的常见调用操作。大多数情况下,被执行的函数除了返回成功执行有返回值的代码外并不会做其他操作。模拟用户态的程序也是一样的:模拟产品(比如Internet Explorer或Acrobat Reader)提供的API操作。这样做相关代码不会失效,而是会完成相关操作。无论操作行为是否恶意,模拟器都会一一记录并分析。

由于几乎每天都有恶意软件制作者和商业保护软件的开发者开发并使用新的反模拟技术,模拟器会经常更新。当反病毒工程师发现新的指令或API正在被恶意软件或保护壳使用时,模拟器中相关指令或API就会被更新,以兼容这些恶意软件或保护壳的操作。接着恶意软件作者和保护软件的开发者会找到并使用更多的指令或API。在反病毒领域,一直上演着猫捉老鼠的游戏。原因很简单,支持整个CPU架构无疑是一项大工程。让桌面版反病毒软件中的模拟器不仅支持整个CPU还要支持操作系统的API的模拟,同时不产生巨大的性能消耗,是一项根本不可能完成的任务。反病毒厂商试图做的是,在不模拟所有指令集或API的情况下,权衡需要支持的API和指令,并尽可能多地模拟恶意软件行为。因此,他们会等到新的反模拟技术出现在恶意软件、封装工具或保护工具出现后,再进行相关调整。

3.4 总结

本章主要讲述了反病毒软件中的插件模块是如何加载的、插件的种类以及插件的功能特性。简而言之,本章讨论了以下几点。

  • 反病毒软件中的插件模块是其重要组成部分,在有需要的时候插件模块会被调用。

  • 反病毒软件通过多种方式加载插件。一些反病毒软件加载插件依赖于操作系统提供的API,而另一些会自己定制插件解密和加载机制。

  • 反病毒软件的插件模块加载过程揭示了,对于逆向分析者来说,逆向分析它们的内部功能是多么困难的一件事。

  • 了解插件的功能实现时,有一些固定的步骤可供逆向分析工程师参考。

  • 反病毒插件模块五花八门,有简单的也有复杂的。相对简单的插件包括扫描器以及通用检测程序、文件格式解析器、协议解析器、可指定文件和档案文件解压缩程序以及启发式引擎,等等。

  • 启发式引擎用于对传入文件进行异常判断。这类引擎一般基于简单或更为复杂的检测逻辑,比如有一些基于统计建模(贝叶斯网络)或权重启发式检测。

  • 有两种类型的启发式引擎:静态和动态。静态引擎直接对文件进行静态分析,不用执行或模拟执行文件。例如,PE文件的文件头内有畸形的区块或PDF文件内引入了使用多种加密手段多次加密的文件流,就可以触发静态启发式引擎检测规则。动态启发式引擎则尝试通过直接执行或模拟执行文件代码,捕获文件操作,并以此为依据查杀恶意软件。

  • 文件格式或协议解析器在解析复杂或畸形格式的时候,常常会产生安全漏洞。

  • 高级反病毒插件模块包括内存扫描器、使用解释型语言编写并在虚拟机中执行的插件,以及模拟器。

  • 内存扫描插件可以分别从用户态和内核态扫描内存。用户态扫描器容易受干扰,并可能会因此影响程序的执行。内核态扫描器往往具有较强的抗干扰性,但如果执行不当的话,往往会出现安全漏洞。

  • 使用脚本语言编写的插件模块不仅容易编写和维护,而且在原有基础上多了一层编译器的保护。逆向分析此类插件的时候,由于代码运行在定制的虚拟机中,逆向过程会变得困难重重。

  • 模拟器是一款反病毒软件的核心部分。针对不同的架构编写万无一失、性能良好的模拟器不是一项容易的工作。然而,编写模拟器对于解析压缩或加密可执行文件以及分析内置在文件中的Shellcode大有帮助。

下一章将讨论反病毒特征码的工作原理及其绕过方式。

目录