第 2 章 逆向工程核心

第 2 章 逆向工程核心

一款反病毒软件的核心是其内部引擎,也被称作内核。内核在将反病毒软件各个重要部分整合在一起的同时,也为它们提供功能上的支持。比如,扫描器借助反病毒软件内核提供的API,完成针对文件、目录、内存和其他形式的扫描分析。

本章将讨论如何逆向分析反病毒产品内核,并从攻击者的视角介绍反病毒软件中有哪些值得关注的特性。反病毒软件通常会采取一些措施,来保护自己不被逆向分析。因此,本章还将介绍使逆向分析过程更容易的若干技术。在本章最后,你可以使用Python编写一个能够直接与反病毒产品内核交互的独立工具,然后通过该工具进行模糊测试或探索自动化测试反病毒软件绕过技术。

2.1 逆向分析工具

本书中提到的软件逆向分析工具,事实上是指IDA反汇编商业版。接下来关于反病毒软件逆向分析技巧的介绍,是建立在你对IDA有一定了解的基础之上的,因为你将使用它完成一系列静态和动态的分析任务。本章还用到了WinDbg和GDB,它们分别是Windows和Linux平台上的标准调试软件。本章的案例将会使用Python,在IDA内或使用IDAPython插件来完成典型的逆向分析任务,编写不依赖第三方的独立脚本。

由于本章涉及恶意软件和反病毒软件绕过技术,强烈建议你安装虚拟化软件(如VMware、VirtualBox或QEMU),构建安全的虚拟实验环境。接下来的部分将会提到,调试符号对你开展调试工作非常有帮助。通常,Linux版本的反病毒软件很有可能会自带调试符号。

如果你想亲自动手做实验,建议你搭建两个虚拟机环境——一个是Windows环境,另一个是Linux环境。

2.1.1 命令行工具与GUI工具

目前所有的反病毒产品都提供图形用户界面(graphical user interface,GUI),以便用户进行软件配置、结果查看、定时扫描设置等工作。因为GUI扫描器并不专门与反病毒引擎及其他许多模块交互,所以分析起来有一定难度。仅仅是分析哪些GUI扫描器的代码控制着GUI绘图、刷新、窗体事件等,就需要联合运用静态和动态分析手段,这绝对是一项不小的工程。幸运的是,如今有一些反病毒产品还提供独立的命令行扫描器。命令行工具相较于GUI工具小了很多,且通常相对独立。因此,研究命令行工具,成为了我们开启逆向工程之旅的第一步。

有一些反病毒软件是运行在其中央服务器上的,因此,使用这类扫描引擎其实是在使用服务器组件,而不是命令行工具或GUI工具。在这类情况下,反病毒服务器会为命令行工具打开一个网络通信端口,以供连接和交互。不过这并不代表服务器需要在自己的机器上真的有一块提供扫描服务的区域,而是只要在服务器系统上启用一个本地系统服务即可。比如,Linux版Avast和卡巴斯基反病毒产品都有各自的病毒查杀服务器以及与之相连的GUI扫描器或命令行扫描器,会向服务器发送扫描请求,并且等待返回的查杀结果。如果你逆向分析这类命令行工具,最终只能看到有关通信协议的代码。即便你足够幸运,发现了反病毒云服务器的远程漏洞,但还是无法知道这类反病毒软件的内核是如何工作的。想要了解这一点,就必须逆向分析之前提到的服务器端的反病毒引擎模块。

在接下来的部分中,我们将以反病毒软件Linux版Avast的服务器组件作为研究对象。

2.1.2 调试符号

在Windows平台上,反病毒产品提供与之对应的调试符号的情况并不常见。但在类Unix系统中,调试符号通常会随第三方产品提供(通常内置在二进制文件中)。如果没有与逆向分析列表相对应的函数和标签名称,缺少反病毒软件的调试符号,那么逆向分析反病毒产品及其任意模块将会是一项艰巨的任务。正如你将看到的那样,我们可以采用一些技巧和工具来找到目标反病毒产品的部分或全部调试符号。

当一款反病毒软件可以同时兼容多个系统平台时,这并不意味着它在不同的系统中有不同的源代码。同样,对于兼容多系统的反病毒产品来说,在不同系统平台版本间共用反病毒软件内核的部分或全部源代码很常见。在那些情况下,你会发现,只要在一个系统平台上逆向分析了反病毒软件的内核,在另外一个平台上的分析将会变得很容易。

当然也有例外。比如,反病毒产品没有针对特定的系统平台(比如针对Mac OS X)开发与之兼容的内核,而是直接从另外的供应商处获得反病毒引擎的使用授权。如果反病毒软件厂商打算将另一个产品的内核整合进自己的产品,只需要更改产品名、版权声明,以及其他一些资源,比如字符串、图标和图像即可。如今许多厂商都采用了这种办法,他们从Bitdefender那里获得其产品和引擎的使用授权,并融入自己的产品中。

回到最初的问题上来:如何了解反病毒引擎的工作方式?你需要去查看,你的分析目标是否有针对类Unix的操作系统(Linux、BSD或Mac OS X)版本,以及与之对应的调试符号是否内置在二进制文件中。如果你足够幸运的话,就能获取到针对该平台的反病毒产品的调试符号。此外,由于反病毒产品的引擎在不同操作系统平台和版本上几乎是相同的(只有一些细小的差异,如系统特定的API和运行时库),你可以把一个平台上的调试符号用在另一个平台上的逆向分析过程中。

2.1.3 提取调试符号的技巧

我们已经知道,在类Unix的操作系统中,很有可能获取到反病毒产品的调试符号,本节将用反病毒软件F-Secure作为案例。F-Secure的fm库在Windows和Linux平台上分别是fm4av.dll和libfm-lnx32.so。不过,Windows版本针对该库没有内置调试符号,而在Linux版本的二进制文件中,则有许多针对该产品内核和其他模块的调试符号。

图2-1展示了通过IDA获取到的F-Secure Windows版本的函数列表。

图2-1 IDA逆向分析出的F-Secure Windows版本函数列表

在图2-2中,IDA通过Linux版本二进制文件中内置的调试符号,列出了有意义的函数名称。

图2-2 F-Secure Linux版本的libfmx-linux32.so库在IDA中的逆向分析结果

除了不同平台版本间存在的少数例外,反病毒软件内核几乎是一致的。考虑到该项特性,你可以先从反病毒软件的Linux版本着手。大多数相关功能在Windows版本里也是类似的。你可以通过zynamics BinDiff等第三方商业二进制文件对比产品,将Linux版本下的相关调试符号导入到Windows版本下。可以首先针对两个平台上的库进行二进制分析和对比,接着将相匹配的调试符号,通过右击Matched Functions按钮同时勾选Import Functions and Comments,将Linux版本下的调试符号导出到Windows版本下(参见图2-3)。

{%}

图2-3 将调试符号从Linux导出到Windows

在许多时候,与F-Secure反病毒软件只有部分调试符号的情况不同,可能在某些反病毒软件的二进制文件中,你能够提取出带有变量名甚至是标签名的完整调试符号。在那种情况下,上面讨论的相关提取技术同样可行。

图2-4展示了借助完整的调试符号,逆向分析Comodo Antivirus中一个库的部分代码。

{%}

图2-4 借助完整的调试符号,逆向分析Comodo Linux版本的libPE32.so库

出于某些原因,在操作系统间导出调试符号并不是百分之百可靠的。比如,针对Windows、Linux、BSD和Mac OS X的编译器是不同的。在类Unix系统平台上,GCC(有时是Clang)是使用最普遍的编译器;但在Windows平台上,则要使用微软开发的编译器。这就意味着,在不同的系统平台上,即使是相同的C或C++代码,生成的汇编代码也是不同的,这也让比对内部函数和导出调试符号的工作变得更难。在其他一些技术中,还有另外一些导出调试符号的工具,比如本书的作者之一Joxean Koret编写的开源的IDA插件Diaphora,通过使用Hex-Rays反汇编生成的抽象语法树(Abstract Syntax Tree,AST)来进行函数图像比对。

2.2 调试技巧

前面几节仅介绍了通过静态分析技术,从你要逆向分析的反病毒产品中获取有用的信息。本节将介绍如何通过动态分析技术逆向分析你选择的反病毒产品。

和恶意软件一样,反病毒产品通常也会采取措施,阻止被逆向分析。反病毒产品的可执行模块是可以被混淆的,有时甚至会针对每一个二进制文件应用不同的混淆处理手段(反病毒软件Avira的内核就是一个案例)。反病毒软件会采用反调试手段,为研究者了解恶意软件侦测算法的原理设置障碍。这类反调试技巧使调试反病毒软件的模块变得更有难度,从而难以了解它们是如何侦测恶意软件的,或攻击者是如何利用反病毒软件中解析器的bug来控制程序执行恶意代码的。

后续几节将为你提供调试反病毒软件的相关建议。所有调试建议和技巧仅针对Windows平台下的产品,因为据观察,没有反病毒软件会在Linux、FreeBDS和Mac OS X上应用反调试技术。

后门和配置设置

尽管反病毒产品通常会阻止你将相关工具注入到其服务进程上展开调试,不过,如果你采用逆向分析技术,绕过反调试保护并不困难。这些自我保护机制(反病毒公司是这么命名的)通常旨在防止恶意软件注入到反病毒软件的服务进程中,在反病毒软件的进程下创建一个子线程,或者阻止防护进程被强制结束(这是恶意软件经常干的事)。这些措施并不是要阻止用户为了调试反病毒软件或其他任何操作,而禁用其相关服务。事实上,要阻止用户禁用(或卸载)反病毒软件毫无意义。

除非反病毒产品已经携带命令行分析扫描器(比如Avira扫描器或Ikarus t3扫描器),否则禁用产品的自我保护机制是使用调试工具开展动态分析工作的第一步。命令行扫描器通常不会带有自我保护功能,因为它们并非常驻进程,而是由用户按需、手动开启工作的。

通常情况下,在官方产品帮助文档中,并不会涉及如何禁用反病毒软件的自我保护机制。这是因为反病毒公司认为,此类信息只有支持和开发人员会用到:当用户报告了一个问题以后,他们需要调试相关服务和进程,来定位问题产生的原因。此外,考虑到恶意软件开发者可能会利用公开的相关信息,攻击装有反病毒产品的计算机,所以不会将此类信息公之于众。通常情况下,只要修改某一条注册表单元中的注册表键,你就可以调试反病毒产品的相关服务了。

与旧版本的Panda Global Protection反病毒软件的例子类似,有时借助一个程序员预留的后门,可以暂时禁用反病毒软件的自我保护机制。Panda反病毒软件有一个名叫pavshld.dll(Panda反病毒防护盾)的动态链接库中,输出了一个只接受唯一参数的函数:一个秘密的GUID。通过传入这个GUID参数,可以禁用该反病毒软件。尽管没有可以调用该函数的现成工具,你还是可以轻松编写一个工具来加载这个动态链接库,然后使用GUID调用这个函数,以禁用Panda反病毒软件的防护盾。接下来就可以使用OllyDbg、IDA或者你中意的其他调试工具开展动态分析了。第14章将会深入讨论Panda反病毒软件中的这个漏洞。

反病毒软件可以在用户层通过hook特定的函数并采用反调试技巧,来实现自我保护功能。在内核层,通过加载设备驱动,可以达到同样的效果。如今,反病毒软件通常会通过使用内核驱动,实现自我保护功能,这无疑是正确的做法。出于多种原因,仅依靠在用户层使用hook技术实现自我保护是一个糟糕的决定。一个最简单的原因是,用户层的其他进程可以轻松将反病毒软件设下的hook移除,详见第9章的内容。

如果反病毒软件的内核驱动只是为了防止产品被禁用,那么只要禁止相关内核驱动加载,就可以轻松禁用反病毒软件的自我保护功能。

要想在Windows平台上禁用内核驱动或系统服务,只要打开注册表编辑器(regedit.exe),然后转到HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services,找到反病毒产品安装的驱动,并修改对应的注册表值。比如,你想禁用中国的反病毒软件——360安全卫士的自我保护功能(官方称作“反黑客功能”)。你需要将360反黑客驱动(360AntiHacker.sys)的初始值更改为4(即常量SERVICE_DISABLED,参见图2-5)。通过更改服务的初始值,可以让Windows不加载相关驱动,从而禁用反病毒软件的自我保护功能。不过,更改过注册表值以后,你需要重新启动计算机。

{%}

图2-5 禁用360反黑客驱动的操作截图

值得一提的是,反病毒软件很有可能会通过弹出“拒绝访问”的错误消息窗口或其他没有意义的提示,来阻止你禁用驱动。在这种情况下,你只需要重启电脑,进入安全模式禁用驱动,然后再次重启进入正常的系统模式即可。

一些反病毒产品的自我保护功能包含在实现全部核心功能的驱动中。在这种情况下,如果禁用了驱动,会直接导致反病毒软件无法正常工作,因为其余更高级别的模块需要同该驱动有交互。这时你只有一个选择:内核调试。

  1. 内核调试

    本节聚焦于如何使用内核调试工具,调试反病毒软件驱动和用户态进程。借助调试工具进行内核调试是最不费力的一种手段,因为该过程避开了反病毒软件在用户态下采用的各类反调试手段。你将调试整个操作系统,并在必要的时候调试用户层相应的进程,而不是通过禁用反病毒软件的自我保护驱动开展相关分析。内核调试需要使用针对Windows软件包或WDK(Windows Driver Kit)开发的调试工具中的一个(WinDbg或Kd)。

    {%}

    图2-6 调试软件WinDbg

    要展开内核调试,你需要用付费版的VMware或开源的VirtualBox搭建一台虚拟机。本书中使用的软件是VirtualBox,因为它是免费的。

    搭建完Windows 7或之后版本的Windows虚拟机后,你需要配置操作系统的引导选项,以便开展内核调试。在旧版本的Windows系统中(如Windows XP、 Windows 2000等),可以通过修改c:\boot.ini文件完成相关操作;但从Windows Vista开始,则必须使用系统启动菜单编辑器(bcdedit)。你需要先以管理员权限打开一个命令提示符(cmd.exe),然后执行下面两条指令:

    $ bcdedit /debug on
    $ bcdedit /dbgsettings serial debugport:1 baudrate:115200
    
    

    第一条命令为当前系统启用了内核调试,第二条命令将全局调试配置调整为:使用端口COM1并以115 200波特率(baud-rate)串行通信(参见图2-7)。

    {%}

    图2-7 使用bcdedit在Windows 7系统上配置内核调试

    成功为当前虚拟机系统配置调试功能后,你需要关闭当前虚拟机,在VirtualBox相关配置中完成剩余的配置步骤。

    (1) 右键单击虚拟机,选择Settings,然后在弹出的对话框中,单击左侧的Serial Ports。

    (2) 勾选Enable Serial选项,端口号选择COM1,接着在Port mode下拉菜单中选择Host Pipe选项。

    (3) 勾选Create Pipe选项,接着在Port/File Path栏填入\.\pipe\com_1(如图2-8所示)。

    {%}

    图2-8 配置VirutalBox的调试选项

    (4) 正确完成前面三个步骤后,重启虚拟机,然后进入描述为Debugger Enabled的操作系统。大功告成!现在你不仅可以调试内核驱动,还可以调试用户态下的应用程序,而且再也不用担心相应反病毒软件的自我保护功能妨碍调试了。

    提示 上述步骤的前提是在Windows主机平台上的VirtualBox内运行。在Linux或Mac OS X平台上开展针上述对Windows平台的内核调试会比较麻烦,因为你至少需要两台虚拟机,而且与主机的操作系统版本紧密相关。尽管你可以在Linux或Mac OS X主机系统内同时安装VMWare和VirtualBox,但这是一件相当困难的事情。因此,建议有可能的话,还是在装有Windows的主机内开展内核调试。

  2. 使用内核态调试工具调试用户态下的进程

    使用内核态调试工具来调试用户态下的进程完全可行。不过,为了实现这样的效果,你需要打开内核调试工具(如WinDbg),输入相关指令,让调试工具从当前的运行环境切换到目标进程的运行环境中去。

    具体步骤如下。

    (1) 以管理员权限打开WinDbg,然后在主菜单按File → Kernel Debug顺序选择。

    (2) 在对话框中,进入COM标签,然后输入之前设置的Port或File值。接着,点击Pipe选项。

    (3) 配置WinDbg,使其从Windows符号服务器上下载调试符号,接着通过下列指令重新加载符号:

    .sympath srv*http://msdl.microsoft.com/download/symbols
    .reload
    
    

    当你设置完符号路径后,WinDbg就可以借助公共调试符号开展调试了。

    接下来我们将以F-Secure反病毒软件Windows零售版为例,针对其用户态下的反病毒服务F-Secure Scanner Manager 32-bit(fssm32.exe)展开调试。要想通过WinDbg在内核态开展相关工作,需要先列出调试主机上的所有进程,找到调试目标进程,切换当前执行环境,然后开展调试工作。

    可以通过以下指令,列出从用户态到内核态的所有进程:

    > !process 0 0
    
    

    你可以通过在指令末尾追加进程名进行过滤,使得命令提示符中只显示该进程的相关结果,示例如下:

    > !process 0 0 fssm32.exe
    PROCESS 868c07a0  SessionId: 0  Cid: 0880    Peb: 7ffdf000 \
    ParentCid: 06bc
        DirBase: 62bb7000  ObjectTable: a218da58  HandleCount: 259.
        Image: fssm32.exe
    
    

    从上面现实的结果可以发现,输出字符串868c07a0指向了一个EPROCESS结构体。将EPROCESS的地址带入下列指令:

    .process /r /p 868c07a0.
    
    

    通过运行指令,确定修正符/r /p,之后运行环境会自动在内核态和用户态之间切换。现在,你就可以开始调试fssm32.exe了:

    lkd> .process /r /p 868c07a0
    Implicit process is now 868c07a0
    Loading User Symbols
    ..............................................
    
    

    执行环境切换以后,你就可以通过lm指令列出用户态下进程加载的所有库了,如下:

    lkd> lm
    start    end        module name
    00400000 00531000   fssm32     (deferred)
    006d0000 006ec000   fs_ccf_id_converter32   (deferred)
    00700000 0070b000   profapi    (deferred)
    00750000 00771000   json_c     (deferred)
    007b0000 007cc000   bdcore     (deferred)
    00de0000 00e7d000   fshive2    (deferred)
    01080000 010d2000   fpiaqu     (deferred)
    01e60000 01e76000   fsgem      (deferred)
    02b20000 02b39000   sechost    (deferred)
    07f20000 07f56000   daas2      (deferred)
    0dc60000 0dc9d000   fsuss      (deferred)
    0dce0000 0dd2b000   KERNELBASE   (deferred)
    10000000 10008000   hashlib_x86   (deferred)
    141d0000 14469000   fsgeme     (deferred)
    171c0000 17209000   fsclm      (deferred)
    174b0000 174c4000   orspapi    (deferred)
    178d0000 17aad000   fsusscr    (deferred)
    17ca0000 1801e000   fsecr32    (deferred)
    20000000 20034000   fsas       (deferred)
    21000000 2101e000   fsepx32    (deferred)
    (...)
    
    

    现在你可以在内核态下调试用户态的进程了。如果想要了解更多关于WinDbg的调试技巧,强烈建议你读一读《逆向工程实战》1的第4章。

  3. 使用命令行工具分析反病毒软件

    有时你会幸运地发现,反病毒软件自带命令行工具。在这种情况下,你不需要通过反病毒软件来禁用自我保护机制或开展内核调试。你可以使用任何得心应手的调试工具,动态分析反病毒产品的内核。有不少Windows版本的反病毒软件提供类似的命令行工具(如Avira和Ikarus)。不过也有一些Windows版的反病毒软件,因为厂商移除了这项特性或者因为命令行工具只能被工程师或服务支持人员使用,而不提供独立的命令行工具。在这种情况下,你可以查看一下该款反病毒软件在别的系统平台上有没有相关产品。如果该款反病毒软件有Linux、BSD或Mac OS X版本的话,有可能这些版本提供了可供你调试的独立的自带命令行工具。Avira、Bitdefender、Comodo、F-Secure、Sophos以及其他许多反病毒软件都是这样。

    调试命令行工具并不意味着,你一直要用类似WinDbg、IDA、OllyDbg或GDB调试工具开展调试。你也可以借助调试接口,编写模糊测试工具(Fuzzer),例如LDB binding、Vtrace debugger (由Kenshoto开发)或PyDbg和WinAppDbg Python API。

    提示 模糊测试工具用来向目标程序传入无效或意外的输入数据。根据目标程序的不同,模糊测试输入数据也有所不同。比如,在针对反病毒软件做模糊测试时,使用的就是修改过的或不完整的病毒样本。使用模糊测试工具的目的也各有不同,如发现软件bug或漏洞,发现软件针对传入数据的不同处理方式,等等。编写模糊测试工具,就是要编写自动化输入数据修改工具,并将数据传递给目标程序。一般来说,模糊测试工具要想发现有价值的bug,需要经过成百上千次畸形输入数据的测试(即修改过的输入数据)。

1原书名为Practical Reverse Engineering,由Wiley出版。中文版《逆向工程实战》由人民邮电出版社出版。请登录图灵社区了解详情或试读:http://www.ituring.com.cn/book/1394。——编者注

2.3 移植内核

本节讨论如何挑选自动化平台和工具。为自动化测试挑选合适的操作系统,以及从反病毒软件中提取正确的工具,能够让你在逆向分析和自动化测试过程中事半功倍。

要想实现基本自动化或自动化模糊测试,最好选用类Unix的操作系统,尤其推荐Linux。这是因为Linux系统占用更小的内存和硬盘空间,而且为自动化相关任务提供了许多工具。通常,使用QEMU、KVM、VirtualBox或VMware搭建Linux虚拟机要比搭建Windows虚拟机更容易一些。因此,建议你在Linux平台上开展针对反病毒软件的自动化测试。和其他普通软件一样,反病毒软件厂商通常会把软件目标兼容平台设定为流行的操作系统,比如Windows。如果反病毒产品没有Linux版本,只有Windows版本的话,还是可以通过Wine(其本身不是一款模拟器)模拟器以接近本机语言的运行速度运行反病毒扫描器。

众所周知,Wine软件可用于在非Windows操作系统中(如Linux)运行Windows平台上的二进制文件。另一方面,Winelib(Wine的支持库)可以将只兼容Windows系统的应用移植到Linux系统中去。使用Winelib成功移植到Linux平台的应用案例有Picasa (谷歌开发的一款数码图片编辑和查看工具)、Kylix(Borland开发的一款编译器和继承开发环境,之后不再继续更新)、Corel开发的WordPerfect9 Linux版和IBM公司开发的WebSphere。Wine或Winelib的工作原理是,运行只兼容Windows平台的命令行工具,借助Wine或逆向分析核心库,为Linux编写一个C/C++的封装程序。借助Winelib调用只兼容Windows的动态链接库(DLL文件)导出的函数。

上面介绍的两种办法都可以帮助开展自动化测试。比如,只兼容Windows平台的命令行工具Ikarus t3 Scan(如图2-9所示)以及Microsoft Security Essentials反病毒电脑软件使用的mpengine.dll库(仅兼容Windows平台)。在没有别的办法让目标反病毒产品自动化运行在Linux平台上时,建议使用Wine模拟器,因为在Windows下开展自动化测试,十分复杂而且耗费系统资源。

{%}

图2-9 借助Wine在Linux平台上运行Ikarus t3 Scan

2.4 实战案例:为Linux版Avast编写Python binding

本节将为你介绍一个实战案例,通过逆向分析反病毒软件的相关模块来编写binding。简而言之,这里的binding指的是为你的模糊测试工具编写嵌入式工具或库。如果你可以使用自己编写的工具或库(而不是反病毒厂商提供的工具)与反病毒软件内核交互,那么在接下来的工作中,你就可以实现自动化了(比如编写你自己的扫描器或模糊测试工具)。本案例将以Avast Linux版作为研究目标,选用Python作为实现自动化的编程语言。之所以选用Avast Linux版作为研究目标,是因为该版本易于逆向分析,编写针对它的binding只需要1~2小时。

2.4.1 Linux版Avast简介

Linux版Avast只有两个可执行组成部分:avastscan。第一个负责解压病毒特征数据库文件(VPS文件)、发起扫描任务、查询URL,等等。第二个则是执行这些查询的客户端工具。顺便说一下,这些分布式二进制文件包含部分调试符号,如图2-10所示。

{%}

图2-10 Avast scan工具的函数列表与针对scan_path函数的反汇编界面

多亏有了部分调试符号,你可以开始用IDA分析文件,并且很容易地了解程序的行为。让我们从main函数开始:

.text:08048930 ; int __cdecl main(int argc, const char **argv,
const char **envp)
.text:08048930                 public main
.text:08048930 main            proc near  ; DATA XREF: _start+17 o
.text:08048930
.text:08048930 argc            = dword ptr  8
.text:08048930 argv            = dword ptr  0Ch
.text:08048930 envp            = dword ptr  10h
.text:08048930
.text:08048930  push    ebp
.text:08048931  mov     ebp, esp
.text:08048933  push    edi
.text:08048934  push    esi
.text:08048935  mov     esi, offset src ; "/var/run/avast/scan.sock"
.text:0804893A  push    ebx
.text:0804893B  and     esp, 0FFFFFFF0h
.text:0804893E  sub     esp, 0B0h
.text:08048944  mov     ebx, [ebp+argv]
.text:08048947  mov     dword ptr [esp+28h], 0
.text:0804894F  mov     dword ptr [esp+20h], 0
.text:08048957  mov     dword ptr [esp+24h], 0
.text:0804895F
.text:0804895F loc_804895F:               ; CODE XREF: main+50 j
.text:0804895F                            ; main+52 j ...
.text:0804895F  mov   eax, [ebp+argc]
.text:08048962  mov   dword ptr [esp+8],offset shortopts ; "hvVfpabs:e:"
.text:0804896A  mov   [esp+4], ebx    ; argv
.text:0804896E  mov   [esp], eax      ; argc
.text:08048971  call  _getopt
.text:08048976  test  eax, eax
.text:08048978  js    short loc_8048989
.text:0804897A  sub   eax, 3Ah        ; switch 61 cases
.text:0804897D  cmp   eax, 3Ch
.text:08048980  ja    short loc_804895F
.text:08048982  jmp   ds:off_804A5BC[eax*4] ; switch jump

在地址0x08048935处,有一个被载入ESI寄存器和指向C字符串/var/run/avast/scan.sock的指针。接着,有一个名为hvVfpabs:e:的字符串,调用了函数getopt。这些是scan工具支持的对象,以及客户端工具需要连接的之前的路径和Unix套接字。你可以在之后的地址0x08048B01处证实这点:

.text:08048B01  lea     edi, [esp+0BCh+socket_copy]
.text:08048B05  mov     [esp+4], esi
.text:08048B05  ; ESI points to our previously set socket's path
.text:08048B09  mov     [esp], edi      ; dest
.text:08048B0C  mov     [esp+18h], dl
.text:08048B10  mov     word ptr [esp+42h], 1
.text:08048B17  call    _strcpy
.text:08048B1C  mov     dword ptr [esp+8], 0 ; protocol
.text:08048B24  mov     dword ptr [esp+4], SOCK_STREAM ; type
.text:08048B2C  mov     dword ptr [esp], AF_UNIX ; domain
.text:08048B33  call    _socket

套接字路径的指针被复制(借助strcpy)到了一个栈变量内(stack_copy)。接着,用它打开了一个Unix域套接字。该套接字通过connect函数调用了scan.sock

.text:08048B50  mov     eax, [esp+0BCh+socket]
.text:08048B54  lea     edx, [esp+42h]
.text:08048B58  mov     [esp+4], edx    ; addr
.text:08048B5C  mov     [esp], eax      ; fd
.text:08048B5F  neg     ecx
.text:08048B61  mov     [esp+8], ecx    ; len
.text:08048B65  call    _connect
.text:08048B6A  test    eax, eax

通过上面的梳理,现在我们已经清楚了:命令行扫描客户端通过套接字连接服务器,然后向它发送扫描请求。下一节将阐释扫描客户端与服务器的通信原理。

2.4.2 为Linux版Avast编写简单的Python binding

相信通过上一节的学习,你已经知道Avast的命令行扫描客户端的工作流程了。现在,你将通过Python命令提示符框连接套接字,验证之前的理论:

$ python
>>> import socket
>>> s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
>>> sock_name="/var/run/avast/scan.sock"
>>> s.connect(sock_name)

确实可以成功连接到套接字!现在你需要弄清楚客户端和服务器之间的请求和响应的数据。当连接调用结束后,程序又调用了parse_reponse函数,其理想结果是魔术值220:

.text:08048B72  mov     eax, [esp+0BCh+socket]
.text:08048B76  lea     edx, [esp+0BCh+response]
.text:08048B7A  call    parse_response
.text:08048B7F  cmp     eax, 220

连上套接字后,现在试着从中读取1024个字节:

$ python
>>> import socket
>>> s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
>>> sock_name="/var/run/avast/scan.sock"
>>> s.connect(sock_name)
>>> s.recv(1024)
'220 DAEMON\r\n'

谜团解开了:之前的错误响应码220是直接从服务器应答的。你的Python binding程序需要获取Avast后台进程发送的欢迎消息,然后确认应答是否为220。如果是,那就意味着一切正常。

在函数main之后,程序又调用了函数av_close。下面是该部分的反汇编结果:

.text:08049580 av_close        proc near
.text:08049580 fd              = dword ptr -1Ch
.text:08049580 buf             = dword ptr -18h
.text:08049580 n               = dword ptr -14h
.text:08049580
.text:08049580  push    ebx
.text:08049581  mov     ebx, eax
.text:08049583  sub     esp, 18h
.text:08049586  mov     [esp+1Ch+n], 5  ; n
.text:0804958E  mov     [esp+1Ch+buf], offset aQuit ; "QUIT\n"
.text:08049596  mov     [esp+1Ch+fd], eax ; fd
.text:08049599  call    _write
.text:0804959E  test    eax, eax
.text:080495A0  js      short loc_80495C1
.text:080495A2
.text:080495A2 loc_80495A2:             ; CODE XREF: av_close+4D
.text:080495A2  mov     [esp+1Ch+fd], ebx ; fd
.text:080495A5  call    _close
.text:080495AA  test    eax, eax
.text:080495AC  js      short loc_80495B3

完成任务后,客户端就会调用av_close函数,发送字符串QUIT\n给后台进程,示意自己已经完成了所有工作,现在应该关闭客户端连接了。

现在你需要使用Python编写一个与Avast后台程序通信的迷你类,主要是先连接,然后关闭连接。basic_avast_client1.py包含了第一次要执行的代码内容,如下所示:

#!/usr/bin/python

import socket

SOCKET_PATH = "/var/run/avast/scan.sock"

class CBasicAvastClient:
  def __init__(self, socket_name):
    self.socket_name = socket_name
    self.s = None

  def connect(self):
    self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    self.s.connect(self.socket_name)
    banner = self.s.recv(1024)
    return repr(banner)

  def close(self):
    self.s.send("QUIT\n")

def main():
  cli = CBasicAvastClient(SOCKET_PATH)
  print(cli.connect())
  cli.close()

if __name__ == "__main__":
  main()

试着运行一下脚本:

$ python basic_avast_cli1.py
'220 DAEMON\r\n'

一切工作正常。编写的程序成功连接到了后台服务器,接着又成功关闭了连接。接下来,我们要深入探索更多的命令,而其中最有意思的一个是:分析样本文件或目录的命令。

在地址0x0804083B处,有一次有趣的函数调用过程:

.text:08048D34                 mov     edx, [ebx+esi*4]
.text:08048D37                 mov     eax, [esp+0BCh+socket]
.text:08048D3B                 call    scan_path

因为有部分调试符号的帮助,我们可以轻而易举地确定这个函数的功能:扫描一个指定路径。接下来,让我们来看看scan_path函数:

.text:08049F00 scan_path       proc near         ; CODE XREF: main+40B
.text:08049F00                                   ; .text:08049EF1
.text:08049F00
.text:08049F00 name            = dword ptr -103Ch
.text:08049F00 resolved        = dword ptr -1038h
.text:08049F00 n               = dword ptr -1034h
.text:08049F00 var_1030        = dword ptr -1030h
.text:08049F00 var_102C        = dword ptr -102Ch
.text:08049F00 var_1028        = dword ptr -1028h
.text:08049F00 var_1024        = dword ptr -1024h
.text:08049F00 var_1020        = dword ptr -1020h
.text:08049F00 var_101C        = byte ptr -101Ch
.text:08049F00 var_10          = dword ptr -10h
.text:08049F00 var_C           = dword ptr -0Ch
.text:08049F00 var_8           = dword ptr -8
.text:08049F00 var_4           = dword ptr -4
.text:08049F00
.text:08049F00  sub     esp, 103Ch
.text:08049F06  mov     [esp+103Ch+resolved], 0 ; resolved
.text:08049F0E  mov     [esp+103Ch+name], edx ; name
.text:08049F11  mov     [esp+103Ch+var_10], ebx
.text:08049F18  mov     ebx, eax
.text:08049F1A  mov     [esp+103Ch+var_8], edi
.text:08049F21  mov     edi, edx
.text:08049F23  mov     [esp+103Ch+var_C], esi
.text:08049F2A  mov     [esp+103Ch+var_4], ebp
.text:08049F31  mov     [esp+103Ch+var_102C], offset storage
.text:08049F39  mov     [esp+103Ch+var_1028], 1000h
.text:08049F41  mov     [esp+103Ch+var_1024], 0
.text:08049F49  mov     [esp+103Ch+var_1020], 0
.text:08049F51  call    _realpath
.text:08049F56  test    eax, eax
.text:08049F58  jz      loc_804A040
.text:08049F5E
.text:08049F5E loc_8049F5E:        ; CODE XREF: scan_path+1CE j
.text:08049F5E  mov     ds:storage, 'NACS'
.text:08049F68  mov     esi, eax
.text:08049F6A  mov     ds:word_804BDE4, ' '

你会发现上述过程中调用了realpath函数(获取文件或目录的真实路径)。同时,你还会发现4字节的字符串SCAN,紧随其后的是一些空格。不必逆向分析整个函数结果,也不必考虑之前为Avast编写的Python binding中针对close方法执行的命令格式,你会发现,向后台进程发送的扫描文件或目录的命令就是SCAN/some/path

现在,在之前的代码中加入向后台进程发送扫描命令的代码,然后运行查看结果:

#!/usr/bin/python

import socket

SOCKET_PATH = "/var/run/avast/scan.sock"

class CBasicAvastClient:
  def __init__(self, socket_name):
    self.socket_name = socket_name
    self.s = None

  def connect(self):
    self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    self.s.connect(self.socket_name)
    banner = self.s.recv(1024)
    return repr(banner)

  def close(self):
    self.s.send("QUIT\n")

  def scan(self, path):
    self.s.send("SCAN %s\n" % path)
    return repr(self.s.recv(1024))

def main():
  cli = CBasicAvastClient(SOCKET_PATH)
  print(cli.connect())
  print(cli.scan("malware/xpaj"))
  cli.close()

if __name__ == "__main__":
  main()

脚本的运行结果如下:

$ python basic_avast_cli1.py
'220 DAEMON\r\n'
'210 SCAN DATA\r\n'

运行上面的代码不会产生有用的数据,因为你需要从套接字中读取更多的数据包。指令210 SCAN DATA\r\n就是告诉客户端,接收到这样的服务器响应报文后,需要发送更多的数据包。事实上,你需要一直读取数据包,直到接收到200 SCAN OK\n的服务器响应。现在,你需要按照下面的示例,修正之前编写的Python部分代码(这是一个有用的懒办法):

  def scan(self, path):
    self.s.send("SCAN %s\n" % path)
    while 1:
      ret = self.s.recv(8192)
      print(repr(ret))
      if ret.find("200 SCAN OK") > -1:
        break

将代码修改好以后,再运行一次。这次,你将会得到完全不同的输出结果,带有一个期望值:

$ python basic_avast_cli1.py
'220 DAEMON\r\n'
'210 SCAN DATA\r\n'
'SCAN /some/path/malware/xpaj/00908235ee9e267fa2f4c83fb4304c63af976cbc\t
[L]0.0\t0 Win32:Hoblig\\ [Heur]\r\n'
'200 SCAN OK\r\n'
None

太神奇了!Avast服务器的响应报文中,将文件00908235ee9e267fa2f4c83fb4304c63af976cbc标识为Win32:Hobling。现在,虽然你的Python binding只有一些基本功能,但至少可以工作了,可以扫描指定路径(文件或目录),然后获取到扫描结果。因此,你可以对代码做些调整,基于文件格式编写一些模糊测试工具。你可能想了解Avast Windows版是不是也采用了相同的通信协议,如果是,接着将你刚刚写的Python binding移植到Windows平台上去。如果不能的话,你肯定想在Linux平台上继续进行模糊测试,将GDB或其他调试工具绑定到后台进程/bin/avast上,接着使用你编写的binding向Avast的病毒查杀服务器发送大量修改过的样本文件,而后等待它崩溃。你要记住,无论在Windows平台还是Linux平台上,Avast的内核都是一样的(尽管Avast官方表示,Linux版本的内核不一定是最新的)。如果你在Linux版本中遇到了一个崩溃,那么在WIndows平台上也存在相同错误的可能性非常大。一个影响Avast全平台版本的RPM文件解析漏洞就是采用同样的办法首先在Linux平台版本中发现的。

2.4.3 Python binding的最终版本

你可以从GitHub项目页面下载到Python binding的最终版本:https://github.com/joxeankoret/pyavast

该版本的binding几乎涵盖了2014年4月份Avast中所有的协议特性,可谓十分透彻、全面。

2.5 实战案例:为Linux版Comodo编写本机C/C++工具

如果有服务器的话,监听指定端口与服务器接口通信的命令是针对各类反病毒产品开展自动化任务的捷径。不过,并不是所有反病毒产品都像AVG或Avast一样,有类似的服务器接口。在这种情况下,如果有命令行扫描器与核心库的话,你需要对它们展开逆向分析,来重建必要的内部结构、相应函数及其原型,以便能够了解如何在自动化测试过程中调用这些函数。

本案例为Comodo Linux版编写了一款非官方的C/C++开发工具包(SDK)。幸运的是,Comodo Linux版提供了完整的调试符号。因此,了解其接口、结构等会变得相对简单。

首先,让我们来分析Comodo Linux版的命令行扫描器(称作cmdscan),它的安装目录如下:

/opt/COMODO/cmdscan

在IDA中打开对应的二进制文件,等待最初的自动分析结束,然后跳转到函数main。你将会看到如下反汇编结果:

.text:00000000004015C0 ; __int64 __fastcall main(int argc, char **argv,
char **envp)
.text:00000000004015C0 main proc near
.text:00000000004015C0
.text:00000000004015C0 var_A0= dword ptr -0A0h
.text:00000000004015C0 var_20= dword ptr -20h
.text:00000000004015C0 var_1C= dword ptr -1Ch
.text:00000000004015C0
.text:00000000004015C0     push    rbp
.text:00000000004015C1     mov     ebp, edi
.text:00000000004015C3     push    rbx
.text:00000000004015C4     mov     rbx, rsi            ; argv
.text:00000000004015C7     sub     rsp, 0A8h
.text:00000000004015CE     mov     [rsp+0B8h+var_1C], 0
.text:00000000004015D9     mov     [rsp+0B8h+var_20], 0
.text:00000000004015E4
.text:00000000004015E4 loc_4015E4:
.text:00000000004015E4
.text:00000000004015E4     mov     edx, offset shortopts       ; "s:vh"
.text:00000000004015E9     mov     rsi, rbx                    ; argv
.text:00000000004015EC     mov     edi, ebp                    ; argc
.text:00000000004015EE     call    _getopt
.text:00000000004015F3     cmp     eax, 0FFFFFFFFh

这里,通过标准函数getopt检查命令行选项s:vh。如果你不带参数,直接运行/opt/COMODO/cmdscan命令,打印出来的结果将会是该命令行扫描工具的用法:

$ /opt/COMODO/cmdscan
USAGE: /opt/COMODO/cmdscan -s [FILE] [OPTION...]
-s: scan a file or directory
-v: verbose mode, display more detailed output
-h: this help screen

命令行选项s:vh的反汇编结果如下。最有意思的地方是-s标志,它规定了扫描器需要扫描的文件或目录。让我们继续反汇编,来了解这个标志的工作原理:

.text:00000000004015F8     cmp     eax, 's'
.text:00000000004015FB     jz      short loc_401613
(...)
.text:0000000000401613 loc_401613:
.text:0000000000401613     mov     rdi, cs:optarg       ; name
.text:000000000040161A     xor     esi, esi             ; type
.text:000000000040161C     call    _access
.text:0000000000401621     test    eax, eax
.text:0000000000401623     jnz     loc_40172D
.text:0000000000401629     mov     rax, cs:optarg
.text:0000000000401630     mov     cs:src, rax          ; Path to scan
.text:0000000000401637     jmp     short next_cmdline_option

当使用-s后缀标志时,程序通过调用access检查紧随其后的参数是否为一个存在的路径。如果参数路径存在的话,将待扫描路径的指针(一个文件名或目录)保存为src静态变量,接着继续分析更多的命令行参数。命令行参数解析完毕后,就可以分析相关代码了:

.text:0000000000401649 loc_401649:            ; CODE XREF: main+36 j
.text:0000000000401649     cmp     cs:src, 0
.text:0000000000401651     jz      no_filename_specified
.text:0000000000401657     mov     edi, offset dev_aflt_fd     ; a2
.text:000000000040165C     call    open_dev_avflt
.text:0000000000401661     call    load_framework
.text:0000000000401666     call    maybe_IFrameWork_CreateInstance

上述代码检测了代表需要扫描路径的变量src是否已经被赋值。如果没有被赋值的话,将显示使用帮助,然后退出。否则,程序将调用名为open_dev_avflt的函数,然后是load_framework函数,最后调用maybe_IFramework_CreateInstance函数。你不必去分析函数open_dev_avflt,因为实际上扫描过程中不会用到/dev/avflt方法。跳过函数open_dev_avflt,直接分析用于加载Comodo内核的函数load_framework。该函数的伪代码如下:

void *load_framework()
{
  int filename_size; // eax@1
  char *self_dir; // rax@2
  int *v2; // rax@3
  char *v3; // rax@3
  void *hFramework; // rax@6
  void *CreateInstance; // rax@7
  char *v6; // rax@9
  char filename[2056]; // [sp+0h] [bp-808h]@1

  filename_size = readlink("/proc/self/exe", filename, 0x800uLL);
  if ( filename_size == -1 ||
      (filename[filename_size] = 0,
       self_dir = dirname(filename), chdir(self_dir)) )
  {
    v2 = __errno_location();
    v3 = strerror(*v2);
LABEL_4:
    fprintf(stderr, "%s\n", v3);
    exit(1);
  }
  _hFramework_ = dlopen("./libFRAMEWORK.so", 1);
  hFrameworkSo = hFramework;
  if ( !hFramework )
  {
    v6 = dlerror();
    fprintf(stderr, "error is %s\n", v6);
    goto LABEL_10;
  }
  CreateInstance = dlsym(hFramework, "CreateInstance");
  FnCreateInstance = (int (__fastcall *)
  (_QWORD, _QWORD, _QWORD, _QWORD))CreateInstance;
  if ( !CreateInstance )
  {
LABEL_10:
    v3 = dlerror();
    goto LABEL_4;
  }
  return CreateInstance;
}

反编译出来的代码看起来很棒,不是吗?你可以复制上述函数的伪代码,然后直接保存成C/C++源代码文件。概括来说,上面程序的伪代码行为如下。

  • 借助Linux内核创建的符号链接/proc/self/exe,程序解析了自身的路径,接着将该路径设置为当前工作目录。

  • 程序动态加载libFRAMEWORK.so文件,然后解析函数CreateInstance,接着将指针保存到全局变量FnCreateInstance中。

  • libFRAMEWORK.so内的函数CreateInstance加载内核,同时解析负责创建新框架实例的函数。

接下来,你需要对函数maybe_IFramework_CreateInstance开展逆向分析:

.text:0000000000401A50 maybe_IFrameWork_CreateInstance proc near
.text:0000000000401A50
.text:0000000000401A50 hInstance= qword ptr -40h
.text:0000000000401A50 var_38= qword ptr -38h
.text:0000000000401A50 maybe_flags= qword ptr -28h
.text:0000000000401A50
.text:0000000000401A50     push    rbp
.text:0000000000401A51     xor     esi, esi
.text:0000000000401A53     xor     edi, edi
.text:0000000000401A55     mov     edx, 0F0000h
.text:0000000000401A5A     push    rbx
.text:0000000000401A5B     sub     rsp, 38h
.text:0000000000401A5F     mov     [rsp+48h+hInstance], 0
.text:0000000000401A68     lea     rcx, [rsp+48h+hInstance]
.text:0000000000401A6D     call    cs:FnCreateInstance

此处调用了之前解析的函数FnCreateInstance,同时传递了一个名为hInstance的本地变量,创建了一个Comodo Antivirus的接口实例。实例创建后,将执行如下伪代码:

  BYTE4(maybe_flags) = 0;
  LODWORD(maybe_flags) = -1;
  g_FrameworkInstance = hInstance;
  cur_dir = get_current_dir_name();
  hFramework = g_FrameworkInstance;
  cur_dir_len = strlen(cur_dir);
  if ( hFramework->baseclass_0->CFrameWork_Init(
  hFramework,
  cur_dir_len + 1,
  cur_dir,
  maybe_flags, 0LL) < 0 )
  {
    fwrite("IFrameWork Init failed!\n", 1uLL, 0x18uLL, stderr);
    exit(1);
  }
  free(cur_dir);

上述代码通过调用hFramework->baseclass_0->CFrameWork_Init初始化框架,接收刚刚创建的实例hFramework、其他所有内核文件的目录、给定文件目录路径缓冲区的大小,以及给CFrameWork_Init设置的功能标志。由于之前更改了当前工作目录,当前的文件目录就是程序cmdscan的真实路径/opt/COMODO/。这些操作完成后,程序调用了更多的函数,以便能够正确加载反病毒软件的内核:

  LODWORD(v8) = -1;
  BYTE4(v8) = 0;
  if ( g_FrameworkInstance->baseclass_0->CFrameWork_LoadScanners(
  g_FrameworkInstance,
  v8) < 0 )
  {
    fwrite("IFrameWork LoadScanners failed!\n", 1uLL, 0x20uLL, stderr);
    exit(1);
  }
  if ( g_FrameworkInstance->baseclass_0->CFrameWork_CreateEngine(
  g_FrameworkInstance, (IAEEngineDispatch **)&g_Engine) < 0 )
  {
    fwrite("IFrameWork CreateEngine failed!\n", 1uLL, 0x20uLL, stderr);
    exit(1);
  }
  if ( g_Engine->baseclass_0->CAEEngineDispatch_GetBaseComponent(
         g_Engine,
         (CAECLSID)0x20001,
         (IUnknown **)&g_base_component_0x20001) < 0 )
  {
    fwrite("IAEEngineDispatch GetBaseComponent failed!\n",
  1uLL,
  0x2BuLL, stderr);
    exit(1);
  }

上面的伪代码通过调用CFrameWork_LoadScanners加载了扫描程序。程序通过调用CFrameWork_CreateEngine创建了扫描引擎,同时,通过调用CAEEngineDispatch_GetBaseComponent,直接加载了基础调度模块。接下来要讲的东西虽然可以直接略过,但我们最好还是了解一下它的功能:

  v4 = operator new(0xB8uLL);
  v5 = (IAEUserCallBack *)v4;
  *(_QWORD *)v4 = &vtable_403310;
  pthread_mutex_init((pthread_mutex_t *)(v4 + 144), 0LL);
  memset(&v5[12], 0, 0x7EuLL);
  g_user_callbacks = (__int64)v5;
  result = g_Engine->baseclass_0->CAEEngineDispatch_SetUserCallBack
(g_Engine, v5);
  if ( result < 0 )
  {
    fwrite("SetUserCallBack() failed!\n", 1uLL, 0x1AuLL, stderr);
    exit(1);
  }

上面这段代码可以用来设置回调。例如,你可以通过设置回调,实现每当新文件有打开、创建、读取、写入等操作时,就发出提示的功能。你想借用Comodo引擎编写一个脱壳程序吗?其实只要设置一个通知回调,等待其被调用,复制临时文件或缓冲区,这样就大功告成了。基于反病毒引擎的通用脱壳程序很流行。

相信你也一定觉得上面的演示很有趣,但我们的最终目的是逆向分析反病毒软件内核,为编写能够与Comodo内核交互的C/C++软件开发工具包,收集充足的信息。函数maybe_IFrameWork_CreateInstance目前已经分析完毕,让我们回过头来分析函数main。接下来这部分代码在之前分析的函数被调用之后运行,其伪代码类似下面这样:

if ( __lxstat(1, filename, &v7) == -1 )
  {
    v5 = __errno_location();
    v6 = strerror(*v5);
    fprintf(stderr, "%s: %s\n", filename, v6);
  }
  else
  {
    if ( verbose )
      fwrite("-----== Scan Start ==-----\n", 1uLL, 0x1BuLL, stdout);
    if ( (v8 & 0xF000) == 0x4000 )
      scan_directory(filename, verbose, (__int64)&scanned_files,
                    (__int64)&virus_found);
    else
      scan_stream(filename, verbose, &scanned_files,
                  &virus_found);
    if ( verbose )
      fwrite("-----== Scan End ==-----\n", 1uLL, 0x19uLL, stdout);
    fprintf(stdout, "Number of Scanned Files: %d\n",
           (unsigned int)scanned_files);
    fprintf(stdout, "Number of Found Viruses: %d\n",
           (unsigned int)virus_found);
  }

上述代码的功能是检查全局变量src中存储的路径信息是否存在。如果有这个路径,就会根据调用__lxstat后返回的功能标志(flag),继续调用函数scan_directoryscan_stream。用来扫描目录的函数为每个已发现的元素都调用了scan_stream。现在,让我们来深入探究该函数的具体行为:

int __fastcall scan_stream(
char *filename,
char verbose,
_DWORD *scanned_files,
_DWORD *virus_found)
(…)
  SCANRESULT scan_result; // [sp+10h] [bp-118h]@1
  SCANOPTION scan_option; // [sp+90h] [bp-98h]@1
  ICAVStream *inited_to_zero; // [sp+E8h] [bp-40h]@1

  memset(&scan_option, 0, 0x49uLL);
  memset(&scan_result, 0, 0x7EuLL);
  scan_option.ScanCfgInfo = (x1)-1;
  scan_option.bScanPackers = 1;
  scan_option.bScanArchives = 1;
  scan_option.bUseHeur = 1;
  scan_option.eSHeurLevel = 2;
  base_component_0x20001 =
  *(struct_base_component_0x20001_t **)g_base_comp;
  scan_option.dwMaxFileSize = 0x2800000;
  scan_option.eOwnerFlag = 1;
  inited_to_zero = 0LL;
  result = base_component_0x20001->pfunc50(
             g_base_comp,
             (__int64 *)&inited_to_zero,
             (__int64)filename,
             1LL,
             3LL,
             0LL);

这部分代码非常有意思。首先,它分别初始化了对象SCANRESULT和对象SCANOPTION,并规定了必要的功能标志,比如是否要扫描归档文件,是否要启用启发引擎等。接着,代码调用了成员函数pfunc50,向其传递了许多参数,如基础组成部分、文件名等。虽然我们不知道函数pfunc50到底有什么用,但是也没有必要去弄清楚。要记住,当前的任务不是充分理解Comodo内核的工作原理,而是要弄清楚如何与其交互。接下来的代码是:

  err = result;
  if ( result >= 0 )
  {
    memset((void *)(g_user_callbacks + 12), 0, 0x7EuLL);
    err = g_Engine->baseclass_0->CAEEngineDispatch_ScanStream(g_Engine,
                    inited_to_zero, &scan_option, &scan_result);
(…)

这部分代码实际上是在实现文件扫描功能。函数pfunc50传入了本地变量inited_to_zero,其中包括分析文件过程中需要的所有信息。同样,代码调用了函数CAEEngineDispatch_ScanStream,并同时声明了一些其他参数。这些参数中最有意思的是SCANOPTIONSCANRESULT,两者的作用很明显——规定扫描选项,获取扫描结果。CAEEngineDispatch_ScanStream同样初始化了一些为0的全局回调值,但你可以跳过该函数中使用了这些回调值的代码。接下来的代码也很有意思:

    if ( err >= 0 )
    {
      ++*scanned_files;
      if ( verbose )
      {
        if ( scan_result.bFound )
        {
          fprintf(stdout, "%s ---> Found Virus, Malware Name is %s\n",
                  filename, scan_result.szMalwareName);
          result = fflush(stdout);
        }
        else
        {
          fprintf(stdout, "%s ---> Not Virus\n", filename);
          result = fflush(stdout);
        }
      }
    }

上面这部分代码片段检查了本地变量err是否不为0,增加了变量scanned_files的数值,同时,如果对象SCANRESULT的成员值bFoundtrue,则显示发现的恶意软件名称。该函数的最后一部分代码的作用是,每发现一个恶意软件,就增加已发现的病毒数量:

  if ( scan_result.bFound )
  {
    if ( err >= 0 )
      ++*virus_found;
  }

让我们再次回到函数main,调用函数scan_*后的最后一部分代码为:

  uninit_framework();
  dlclose_framework();
  close_dev_aflt_fd(&dev_aflt_fd);

上面这段代码与清理工作有关。反初始化框架的同时,取消所有正在进行的扫描工作:

  g_base_component_0x20001 = 0LL;
  if ( g_Engine )
  {
    g_Engine->baseclass_0->CAEEngineDispatch_Cancel(g_Engine);
    result = g_Engine->baseclass_0->CAEEngineDispatch_UnInit(
  g_Engine, 0LL);
    g_Engine = 0LL;
  }
  if ( g_FrameworkInstance )
  {
    result = g_FrameworkInstance->baseclass_0->CFrameWork_UnInit(
  g_FrameworkInstance, 0LL);
    g_FrameworkInstance = 0LL;
  }

最终关闭被占用的libFRAMEWORK.so库:

void __cdecl dlclose_framework()
{
  if ( hFrameworkSo )
    dlclose(hFrameworkSo);
}

现在,我们收集齐了所有为Comodo Linux版编写C/C++工具的必要信息。而且幸运的是,Comodo反病毒软件提供了所有需要的结构。因此,你可以将这些结构和枚举类型导出到一个头文件(header file)中。要完成该操作,需要在IDA内选择View → Open Subviews → Local Types,右击Local Types窗口,从弹出菜单中选择Export to Header File选项。勾选Generate Compilable Header File选项,填入正确的导出头文件路径,接着点击Export。修正头文件中的一些编译错误后,就可以在C/C++项目中使用它了。不过,修正头文件中的编译错误,让它能够被编译器正常编译的过程,实在是个噩梦。但这次你不需要经历这个过程,可以直接从https://github.com/joxeankoret/tahh/tree/master/comodo下载刚刚提到的头文件。

从GitHub上把头文件下载下来后,就可以开始接下来的相关工作了。首先,你需要创建一个类似Comodo cmdscan的命令行工具,不过相较于cmdscan,我们编写的程序能够输出更多有趣的信息。编写时,首先添加一段导入宏文件的代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <dlfcn.h>
#include <libgen.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "comodo.h"

上面展示的这些是程序需要的头文件。接下来,你可以把Hex-Rays反汇编生成的伪代码复制到你的项目中。不过要注意的是,这个时候不要把整个反汇编生成的文件复制过来,而要一步一步复制过来。

int main(int argc, char **argv)
{
  int scanned_files = 0;
  int virus_found = 0;

  if ( argc == 1 )
    return 1;

  load_framework();
  maybe_IFrameWork_CreateInstance();

  scan_stream(argv[1], verbose, &scanned_files, &virus_found);
  printf("Final number of Scanned Files: %d\n", scanned_files);
  printf("Final number of Found Viruses: %d\n", virus_found);

  uninit_framework();
  dlclose_framework();
  return 0;
}

在上面这段代码中,命令行第一个参数代表着要扫描的文件。通过加载框架和创建实例,开始扫描任务。程序接着会调用函数scan_stream,实时显示扫描文件情况,继而反初始化框架和反加载库。此处需要调用多个函数:load_frameworkmaybe_IFrameWork_CreateInstancescan_streamuninit_frameworkdlclose_framework。你可以直接将Hex-Rays的反汇编结果,按照一个个函数的顺序,把伪代码复制过来。最终伪代码效果如下:

//----------------------------------------------------------------------
void uninit_framework()
{
  g_base_component_0x20001 = 0;
  if ( g_Engine )
  {
    g_Engine->baseclass_0->CAEEngineDispatch_Cancel(g_Engine);
    g_Engine->baseclass_0->CAEEngineDispatch_UnInit(g_Engine, 0);
    g_Engine = 0;
  }
  if ( g_FrameworkInstance )
  {
    g_FrameworkInstance->baseclass_0->CFrameWork_UnInit(
  g_FrameworkInstance, 0);
    g_FrameworkInstance = 0;
  }
}

//----------------------------------------------------------------------
int scan_stream(char *src, char verbosed,
   int *scanned_files,
   int *virus_found)
{
  struct_base_component_0x20001_t *base_component_0x20001;
  int result;
  HRESULT err;
  SCANRESULT scan_result;
  SCANOPTION scan_option;
  ICAVStream *inited_to_zero;

  memset(&scan_option, 0, sizeof(SCANOPTION));
  memset(&scan_result, 0, sizeof(SCANRESULT));
  scan_option.ScanCfgInfo = -1;
  scan_option.bScanPackers = 1;
  scan_option.bScanArchives = 1;
  scan_option.bUseHeur = 1;
  scan_option.eSHeurLevel = enum_SHEURLEVEL_HIGH;
  base_component_0x20001 = *
     (struct_base_component_0x20001_t **)g_base_component_0x20001;
  scan_option.dwMaxFileSize = 0x2800000;
  scan_option.eOwnerFlag = enum_OWNER_ONDEMAND;
  scan_option.bDunpackRealTime = 1;
  scan_option.bNotReportPackName = 0;

  inited_to_zero = 0;
  result = base_component_0x20001->pfunc50(
             g_base_component_0x20001,
             (__int64 *)&inited_to_zero,
             (__int64)src,
             1LL,
             3LL,
             0);
  err = result;
  if ( result >= 0 )
  {
    err = g_Engine->baseclass_0->CAEEngineDispatch_ScanStream
(g_Engine, inited_to_zero, &scan_option, &scan_result);
    if ( err >= 0 )
    {
      (*scanned_files)++;
      if ( scanned_files )
      {
        //printf("Got scan result? %d\n", scan_result.bFound);
        if ( scan_result.bFound )
        {
          printf("%s ---> Found Virus, Malware Name is %s\n", src,
 scan_result.szMalwareName);
          result = fflush(stdout);
        }
        else
        {
          printf("%s ---> Not Virus\n", src);
          result = fflush(stdout);
        }
      }
    }
  }
  if ( scan_result.bFound )
  {
    if ( err >= 0 )
      (*virus_found)++;
  }
  return result;
}

//----------------------------------------------------------------------
int maybe_IFrameWork_CreateInstance()
{
  char *cur_dir;
  CFrameWork *hFramework;
  int cur_dir_len;
  CFrameWork *hInstance;
  int *v8;
  int *maybe_flags;

  hInstance = 0;
  if ( FnCreateInstance(0, 0, 0xF0000, &hInstance) < 0 )
  {
    fwrite("CreateInstance failed!\n", 1uLL, 0x17uLL, stderr);
    exit(1);
  }

  BYTE4(maybe_flags) = 0;
  LODWORD(maybe_flags) = -1;
  g_FrameworkInstance = hInstance;
  cur_dir = get_current_dir_name();
  hFramework = g_FrameworkInstance;
  cur_dir_len = strlen(cur_dir);
  if ( hFramework->baseclass_0->CFrameWork_Init
(hFramework, cur_dir_len + 1, cur_dir, maybe_flags, 0) < 0 )
  {
    fwrite("IFrameWork Init failed!\n", 1uLL, 0x18uLL, stderr);
    exit(1);
  }
  free(cur_dir);
  LODWORD(v8) = -1;
  BYTE4(v8) = 0;
  if ( g_FrameworkInstance->baseclass_0-
>CFrameWork_LoadScanners(g_FrameworkInstance, v8) < 0 )
  {
    fwrite("IFrameWork LoadScanners failed!\n", 1uLL, 0x20uLL, stderr);
    exit(1);
  }
  if ( g_FrameworkInstance->baseclass_0-
>CFrameWork_CreateEngine(g_FrameworkInstance, (IAEEngineDispatch **)
&g_Engine) < 0 )
  {
    fwrite("IFrameWork CreateEngine failed!\n", 1uLL, 0x20uLL, stderr);
    exit(1);
  }
  if ( g_Engine->baseclass_0->CAEEngineDispatch_GetBaseComponent(
         g_Engine,
         (CAECLSID)0x20001,
         (IUnknown **)&g_base_component_0x20001) < 0 )
  {
    fwrite("IAEEngineDispatch GetBaseComponent failed!\n",
1uLL, 0x2BuLL, stderr);
    exit(1);
  }
  return 0;
}

//----------------------------------------------------------------------
void dlclose_framework()
{
  if ( hFrameworkSo )
    dlclose(hFrameworkSo);
}

//----------------------------------------------------------------------
void load_framework()
{
  int filename_size;
  char *self_dir;
  int *v2;
  char *v3;
  void *hFramework;
  char *v6;
  char filename[2056];

  filename_size = readlink("/proc/self/exe", filename, 0x800uLL);
  if ( filename_size == -1 || (filename[filename_size] = 0, self_dir =
dirname(filename), chdir(self_dir)) )
  {
    v2 = __errno_location();
    v3 = strerror(*v2);
    fprintf(stderr, "Directory error: %s\n", v3);
    exit(1);
  }

  hFramework = dlopen("./libFRAMEWORK.so", 1);
  hFrameworkSo = hFramework;
  if ( !hFramework )
  {
    v6 = dlerror();
    fprintf(stderr, "Error loading libFRAMEWORK: %s\n", v6);
    exit(1);
  }

  FnCreateInstance = (FnCreateInstance_t)dlsym(hFramework,
"CreateInstance");
  if ( !FnCreateInstance )
  {
    v3 = dlerror();
    fprintf(stderr, "%s\n", v3);
    exit(1);
  }
}

你只需要在include指令后,添加函数的前置声明,以及全局变量:

//----------------------------------------------------------------------
// 变量声明
int main(int argc, char **argv, char **envp);
void uninit_framework();
int scan_stream(char *src, char verbosed,
                int *scanned_files,
                int *virus_found);
int maybe_IFrameWork_CreateInstance();
void dlclose_framework();
void load_framework();
void scan_directory(char *src,
                    unsigned __int8 a2,
                    __int64 a3, __int64 a4);

//----------------------------------------------------------------------
// 数据声明
char *optarg;
char *src;
char verbose;
__int64 g_base_component_0x20001;
__int64 g_user_callbacks;
CAEEngineDispatch *g_Engine;
CFrameWork *g_FrameworkInstance;

typedef int (__fastcall *FnCreateInstance_t)(_QWORD, _QWORD, _QWORD,
CFrameWork **);
int (__fastcall *FnCreateInstance)(
_QWORD, _QWORD, _QWORD, CFrameWork **);
void *hFrameworkSo;
vtable_403310_t *vtable_403310;

现在,你已经完成了一个基础的Comodo命令行扫描程序代码编写任务。接下来,你可以在Linux平台上使用以下命令将程序编译出来:

$ g++ cmdscan.c -o mycmdscan -fpermissive \
                -Wno-unused-local-typedefs -ldl

为了测试程序能否运行,你需要使用以下命令,将程序复制到/opt/COMODO目录:

$ sudo cp mycmdscan /opt/COMODO

现在就可以测试刚刚编译出来的程序是否能够像Comodo的原生命令行扫描器cmdscan一样工作了:

$ /opt/COMODO/mycmdscan /home/joxean/malware/eicar.com.txt
/home/joxean/malware/eicar.com.txt ---> Found Virus , \
                                        Malware Name is Malware
Number of Scanned Files: 1
Number of Found Viruses: 1

一切工作正常!现在让我们来修改程序,使其能够打印出关于已扫描或未扫描的文件情况信息。如果你查看结构SCANRESULT,会发现一些非常有趣的成员结构:

struct SCANRESULT
{
  char bFound;
  int unSignID;
  char szMalwareName[64];
  int eFileType;
  int eOwnerFlag;
  int unCureID;
  int unScannerID;
  int eHandledStatus;
  int dwPid;
  __int64 ullTotalSize;
  __int64 ullScanedSize;
  int ucrc1;
  int ucrc2;
  char bInWhiteList;
  int nReserved[2];
};

比如,你可以获取与你样本相匹配的特征码标识符、扫描器标识符,以及用于检测样本的CRC文件校验码,还有了解样本文件是不是在反病毒软件的白名单中。在程序scan_stream中,你可以通过修改下面若干行代码替换已侦测样本的病毒名:

          printf("%s ---> Malware: %s\n",
                    src,
                    scan_result.szMalwareName);
          if ( scan_result.unSignID )
            printf("Signature ID: 0x%x\n", scan_result.unSignID);
          if ( scan_result.unScannerID )
            printf("Scanner     : %d (%s)\n",
                 scan_result.unScannerID,
                 get_scanner_name(scan_result.unScannerID));
          if ( scan_result.ullTotalSize )
            printf("Total size  : %lld\n", scan_result.ullTotalSize);
          if ( scan_result.ullScanedSize )
            printf("Scanned size: %lld\n", scan_result.ullScanedSize);
          if ( scan_result.ucrc1 || scan_result.ucrc2 )
            printf("CRCs        : 0x%x 0x%x\n",
                  scan_result.ucrc1,
                  scan_result.ucrc2);
          result = fflush(stdout);

现在,将Not virus这行代码替换成以下代码:

            printf("%s ---> Not Virus\n", src);
            if ( scan_result.bInWhiteList )
              printf("INFO: The file is white-listed.\n");
            result = fflush(stdout);

最后一步是将下列函数代码添加到scan_stream程序前,将扫描器标识符解析为扫描器名称:

//----------------------------------------------------------------------
const char *get_scanner_name(int id)
{
  switch ( id )
  {
    case 15:
      return "UNARCHIVE";
    case 28:
      return "SCANNER_PE64";
    case 27:
      return "SCANNER_MBR";
    case 12:
      return "ENGINEDISPATCH";
    case 7:
      return "UNPACK_STATIC";
    case 22:
      return "SCANNER_EXTRA";
    case 29:
      return "SCANNER_SMART";
    case 16:
      return "CAVSEVM32";
    case 6:
      return "SCANNER_SCRIPT";
    case 9:
      return "SIGNMGR";
    case 21:
      return "UNPACK_DUNPACK";
    case 13:
      return "SCANNER_WHITE";
    case 24:
      return "SCANNER_RULES";
    case 8:
      return "UNPACK_GUNPACK";
    case 10:
      return "FRAMEWORK";
    case 3:
      return "SCANNER_PE32";
    case 5:
      return "MEMORY_ENGINE";
    case 23:
      return "UNPATCH";
    case 2:
      return "SCANNER_DOSMZ";
    case 4:
      return "SCANNER_PENEW";
    case 0:
      return "Default";
    case 17:
      return "CAVSEVM64";
    case 20:
      return "UNSFX";
    case 19:
      return "SCANNER_MEM";
    case 14:
      return "MTENGINE";
    case 1:
      return "SCANNER_FIRST";
    case 18:
      return "SCANNER_HEUR";
    case 26:
      return "SCANNER_ADVHEUR";
    case 11:
      return "MEMTARGET";
    case 25:
      return "FILEID";
    default:
      return "Unknown";
  }
}

上述信息是从以下枚举值中提取的,它们已存在于IDA数据库中了(不要忘了你有完整的调试符号):

enum MemMgrType
{
  enumMemMgr_Default = 0x0,
  enumMemMgr_SCANNER_FIRST = 0x1,
  enumMemMgr_SCANNER_DOSMZ = 0x2,
  enumMemMgr_SCANNER_PE32 = 0x3,
  enumMemMgr_SCANNER_PENEW = 0x4,
  enumMemMgr_MEMORY_ENGINE = 0x5,
  enumMemMgr_SCANNER_SCRIPT = 0x6,
  enumMemMgr_UNPACK_STATIC = 0x7,
  enumMemMgr_UNPACK_GUNPACK = 0x8,
  enumMemMgr_SIGNMGR = 0x9,
  enumMemMgr_FRAMEWORK = 0xA,
  enumMemMgr_MEMTARGET = 0xB,
  enumMemMgr_ENGINEDISPATCH = 0xC,
  enumMemMgr_SCANNER_WHITE = 0xD,
  enumMemMgr_MTENGINE = 0xE,
  enumMemMgr_UNARCHIVE = 0xF,
  enumMemMgr_CAVSEVM32 = 0x10,
  enumMemMgr_CAVSEVM64 = 0x11,
  enumMemMgr_SCANNER_HEUR = 0x12,
  enumMemMgr_SCANNER_MEM = 0x13,
  enumMemMgr_UNSFX = 0x14,
  enumMemMgr_UNPACK_DUNPACK = 0x15,
  enumMemMgr_SCANNER_EXTRA = 0x16,
  enumMemMgr_UNPATCH = 0x17,
  enumMemMgr_SCANNER_RULES = 0x18,
  enumMemMgr_FILEID = 0x19,
  enumMemMgr_SCANNER_ADVHEUR = 0x1A,
  enumMemMgr_SCANNER_MBR = 0x1B,
  enumMemMgr_SCANNER_PE64 = 0x1C,
  enumMemMgr_SCANNER_SMART = 0x1D,
};

使用g++命令编译之前的文件,将其复制至/opt/COMODO目录,然后重新运行程序,收尾工作就全部完成了。这次,你将得到更多的信息:

$ g++ cmdscan.c -o mycmdscan -fpermissive \
                -Wno-unused-local-typedefs –ldl

$ sudo cp mycmdscan /opt/COMODO

$ /opt/COMODO/mycmdscan /home/joxean/malware/eicar.com.txt
/home/joxean/malware/eicar.com.txt ---> Found Virus,
                                        Malware Name is Malware
Scanner     : 12 (ENGINEDISPATCH)
CRCs        : 0x486d0e3 0xa03f08f7
Number of Scanned Files: 1
Number of Found Viruses: 1

根据上面的信息,我们得知使用CRC文件特征码的文件扫描引擎名叫ENGINEDISPATCH。上面的例子使用的是EICAR测试文件,不过如果你使用的是不同的文件的话,就可以通过改变文件的CRC校验值躲避反病毒软件的侦测。你可以向该程序添加更多的功能:添加递归检测目录功能,以及只展示有用信息(比如,白名单文件和已侦测文件)的静默模式。你还可以将它作为库的基础,整合进自己的研究工具集中。

本工具的最终版本相较于Comodo的原生命令行扫描器,增加了不少新的功能。你可以移步相关GitHub页面下载:https://github.com/joxeankoret/tahh/tree/master/comodo

2.6 内核加载的其他部分

反病毒软件内核常用于打开文件、遍历压缩文件或缓冲区内的所有文件,开展基于特征码的病毒扫描或通用扫描,以及移除已知的恶意软件。但有些任务并不是由内核完成的,而是由反病毒软件的其他模块完成的,比如插件、通用检测模块、启发式引擎等。这些模块(尤其是插件)由反病毒软件的内核加载,来完成一些有意思的任务。比如Microsoft Security Essentials Antivirus反病毒引擎(mpengine.dll)就会加载由C++/.NET和Lua脚本语言编写的病毒检测和查杀程序,随后将它们从跟随软件发布的数据库文件以及每日更新中抽取出来。Bitdefender也有类似的行为,它会动态加载包含相关代码的二进制插件(XMD文件)。卡巴斯基通过将随更新发布的新对象文件重新链接到内核,加载自身插件和查杀程序。简而言之,每款反病毒软件的加载方式各不相同。

逆向分析特征码、通用扫描等模块的关键是:静态或动态逆向分析同插件交互的内核模块。如果你不分析这些插件是如何加密、压缩、加载和执行的,就无法完全了解反病毒软件的工作原理。

2.7 总结

本章涵盖的知识为本书后面的内容作了很好的铺垫。本章阐释了在厂商未提供现成命令行工具的情况下,为了编写一个用来完成自动化测试和模糊测试的客户端库,需要进行的逆向分析反病毒产品内核和其他组成部分的相关工作。

我们还讨论了其他一些重要的知识点。

  • 借助调试符号,让逆向分析过程更容易 因为反病毒产品的基础代码类似,所以在提供了调试符号的操作系统平台上逆向分析相关模块,接着再将符号移植到没有提供调试符号的平台上是可行的。本章中提到了与此相关的两款工具,分别是zynamics BinDiff和Joxean Koret的Diaphora。

  • Linux是开展模糊测试和自动化测试工作首选的操作系统 模拟器Wine和它的姊妹项目Windlib可以帮助你在Linux平台上移植或运行Windows上的命令行扫描工具。

  • 绕过反病毒自我保护 与Windows平台的版本不同,反病毒软件的Linux版本通常不带自我保护。为了能够调试反病毒软件,本章介绍了一些绕过反病毒软件自我保护的技巧。

  • 搭建实验环境 为了开展反病毒软件驱动和服务的调试工作,本章我们学习了如何搭建虚拟机环境。另外,本章还涉及了WinDbg及其调试命令,为你展示了如何在内核态下开展内核和用户态调试。

最后,本章结合实战案例,详细介绍了如何为Comodo反病毒软件编写一个客户端库。

下一章将讨论插件是如何加载的,以及如何提取和理解这项功能。

目录