安徽师范大学——计算机与信息学院————作者(授课老师):周文
PE的意思就是Portable Executable
(可移植、可执行),它是Win32
可执行文件的标准格式
由于大量的EXE
文件被执行,且传播的可能性最大,因此,Win32
病毒感染文件时,基本上都会将EXE
文件作为目标
一般来说,病毒往往先于HOST程序获得控制权。运行Win32
病毒的一般流程示意如下:
HOST程序
;HOST程序
到内存;AddressOfEntryPoint
加 ImageBase
之和,定位第一条语句的位置(程序入口
);病毒代码
)HOST程序
原来的入口代码;HOST程序
继续执行。HOST程序
之前执行?PE的名词
入口点(entry point
)
第一行代码
偏移地址
(File offset)PE文件存储在磁盘上,各数据段的地址称为文件偏移地址或物理地址(raw offset)。
基地址
(Image base)
EXE:0x00400000
, DLL:0x1000000
。/BASE选项
改变该值虚拟地址
(Virtual Address, VA
)
386保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址(VA),也称内存偏移地址(memory offset)
相对虚拟地址(Relative Virtual Address,RVA)
RVA=VA – imagebase
DOS头与DOS插桩程序
PE结构中紧随MZ文件头
之后的DOS插桩程序
(DOS Stub)
可以通过IMAGE_DOS_HEADER结构
来识别一个合法的DOS头
– IMAGE_DOS_SIGNATURE
(IMAGE_DOS_HEADER)
– IMAGE_NT_HEADER
(PE header)
PE文件格式
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部
USHORT e_magic; // 魔术数字
USHORT e_cblp; // 文件最后页的字节数
USHORT e_cp; // 文件页数
USHORT e_crlc; // 重定义元素个数
USHORT e_cparhdr; // 头部尺寸,以段落为单位
USHORT e_minalloc; // 所需的最小附加段
USHORT e_maxalloc; // 所需的最大附加段
USHORT e_ss; // 初始的SS值(相对偏移量)
USHORT e_sp; // 初始的SP值
USHORT e_csum; // 校验和
USHORT e_ip; // 初始的IP值
USHORT e_cs; // 初始的CS值(相对偏移量)
USHORT e_lfarlc; // 重分配表文件地址
USHORT e_ovno; // 覆盖号
USHORT e_res[4]; // 保留字
USHORT e_oemid; // OEM标识符(相对e_oeminfo)
USHORT e_oeminfo; // OEM信息
USHORT e_res2[10]; // 保留字
LONG e_lfanew; // 新EXE头部的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
PE文件头
DOS Stub
的是PE headerPE header
是IMAGE_NT_HEADERS
的简称,即NT映像头(PE文件头),存放PE整个文件信息分布的重要字段,包含了许多PE装载器用到的重要域。执行体在支持PE文件结构的操作系统中执行时DOS MZ header
中找到PE header
的起始偏移量
,从而跳过DOS Stub
直接定位到真正的文件头PE header
PE文件头的结构
xxxxxxxxxx
IMAGE_NT_HEADERS STRUCT {
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
}IMAGE_NT_HEADERS ENDS
紧跟着“PE\0\0”的是映像文件头,是NT映像头的主要部分,它包含有PE文件的基本信息
xxxxxxxxxx
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 0x04,该程序要执行的环境及平台
WORD NumberOfSections; // 0x06,文件中节的个数
DWORD TimeDateStamp; // 0x08,文件建立的时间
DWORD PointerToSymbolTable; // 0x0c,COFF符号表的偏移
DWORD NumberOfSymbols; // 0x10,符号数目
WORD SizeOfOptionalHeader; // 0x14,可选头的长度
WORD Characteristics; // 0x16,标志集合
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
紧跟映像文件头后面的就是可选映像头
xtypedef struct _IMAGE_OPTIONAL_HEADER {
// 标准域:
WORD Magic; // 0x18,一般是0x010B
BYTE MajorLinkerVersion; // 0x1a,链接器的主/次版本号,
BYTE MinorLinkerVersion; // 0x1b,这两个值都不可靠
DWORD SizeOfCode; // 0x1c,可执行代码的长度
DWORD SizeOfInitializedData; // 0x20,初始化数据的长度(数据节)
DWORD SizeOfUninitializedData; // 0x24,未初始化数据的长度(bss节)
DWORD AddressOfEntryPoint; // 0x28,代码的入口RVA地址,程序从这开始执行
DWORD BaseOfCode; // 0x2c,可执行代码起始位置,意义不大
DWORD BaseOfData; // 0x30,初始化数据起始位置,意义不大
// NT 附加域:
DWORD ImageBase; // 0x34,载入程序首选的RVA地址
DWORD SectionAlignment; // 0x38,加载后节在内存中的对齐方式
DWORD FileAlignment; // 0x3c,节在文件中的对齐方式
WORD MajorOperatingSystemVersion; // 0x3e,操作系统主/次版本,
WORD MinorOperatingSystemVersion; // 0x40,Loader并没有用这两个值
WORD MajorImageVersion; // 0x42,可执行文件主/次版本
WORD MinorImageVersion; // 0x44
WORD MajorSubsystemVersion; // 0x46,子系统版本号
WORD MinorSubsystemVersion; // 0x48
DWORD Win32VersionValue; // 0x4c,Win32版本,一般是0
DWORD SizeOfImage; // 0x50,程序调入后占用内存大小(字节)
DWORD SizeOfHeaders; // 0x54,文件头的长度之和
DWORD CheckSum; // 0x58,校验和
WORD Subsystem; // 0x5c,可执行文件的子系统
WORD DllCharacteristics; // 0x5e,何时DllMain被调用,一般为0
DWORD SizeOfStackReserve; // 0x60,初始化线程时保留的堆栈大小
DWORD SizeOfStackCommit; // 0x64,初始化线程时提交的堆栈大小
DWORD SizeOfHeapReserve; // 0x68,进程初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 0x6c,进程初始化时提交的堆大小
DWORD LoaderFlags; // 0x70,装载标志,与调试相关
DWORD NumberOfRvaAndSizes; // 0x74,数据目录的项数,一般是16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
节通过节表
实现索引
节的内容
才是要真正执行的程序和相关数据修改节表
NumberOfSections
决定xxxxxxxxxx
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
UCHAR Name[IMAGE_SIZEOF_SHORT_NAME]; // 节名
union {
ULONG PhysicalAddress; // OBJ文件中表示本节物理地址
ULONG VirtualSize; // EXE文件中表示节的实际字节数
} Misc;
ULONG VirtualAddress; // 本节的RVA
ULONG SizeOfRawData; // 本节经过文件对齐后的尺寸
ULONG PointerToRawData; // 本节原始数据在文件中的位置
ULONG PointerToRelocations; // OBJ文件中表示本节重定位信
// 息的偏移,EXE文件中无意义
ULONG PointerToLinenumbers; // 行号偏移
USHORT NumberOfRelocations; // 本节需重定位的数目
USHORT NumberOfLinenumbers; // 本节在行号表中的行号数目
ULONG Characteristics; // 节属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
代码节
的属性一般是 “可执行”、“可读”和“节中包含代码”
数据节
的属性一般是 “可读”、“可写”和“包含已初始化数据”
病毒在添加新节
时,都会将新添加的节的属性设置为可读、可写、可执行
PE文件的真正内容划分成块,称之为Section(节)
,紧跟在节表之后;
每个节是一块拥有共同属性的数据,比如代码/数据、读/写等。节的划分是基于各组数据的共同属性
节名称仅仅是个区别不同节的符号而已,类似“data”、“code”的命名只为了便于识别,惟有节的属性设置决定了节的特性和功能
典型地拥有9个预定义节,它们是.text、.bss、.rdata、.data、.rsrc、 .edata、 .idata、 .pdata和.debug
.text
.bss、.rdata、.data
.rsrc
.edata
.idata
代码节.text
Windows NT
默认的做法是将所有的可执行代码组成了一个单独的节,名为“.text”或“.code”;IAT
亦存在于.text节
之中的模块入口点之前。IAT
是一系列的跳转指令;引入函数节.idata
DLL
中引入的函数IMAGE_IMPORT_DESCRIPTOR
结构的结构数组,也叫引入表,数据目录表表项结构成员VirtualAddress
包含引入表地址API函数
地址IMAGE_IMPORT_DESCRIPTOR
的结构如下:
xxxxxxxxxx
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //IMAGE_THUNK_DATA数组的指针
};
DWORD TimeDateStamp; //文件建立时间
DWORD ForwarderChain; //一般为0
DWORD Name; //DLL名字的指针
DWORD FirstThunk; //通常也是IMAGE_THUNK_DATA数组的指针
} IMAGE_IMPORT_DESCRIPTOR;
引出函数节.edata
引出函数节的开始,是一个IMAGE_EXPORT_DIRECTORY结构
:
xxxxxxxxxx
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 一般为0
DWORD TimeDateStamp; // 文件生成时间
WORD MajorVersion; // 主版本号
WORD MinorVersion; // 次版本号
DWORD Name; // 指向DLL的名字
DWORD Base; // 基数,加上序数就是函数地址数组的索引值
DWORD NumberOfFunctions; // AddressOfFunctions数组的项数
DWORD NumberOfNames; // AddressOfNames数组的项数
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;
DLL/EXE
要引出一个函数给其他DLL/EXE
使用,有两种实现方法:
已知导出函数名,获取函数地址的一般步骤:
定位到PE header
从数据目录表读取导出表的虚拟地址
定位导出表获取名字数目(NumberOfNames
)
并行遍历AddressOfNames
和AddressOfNameOrdinals
指向的数组匹配名字,如果在AddressOfNames指向的数组中找到匹配名字,从AddressOfNameOrdinals
指向的数组中提取索引值,
AddressOfNames
的第6个元素,那就提取AddressOfNameOrdinals
数组的第6个元素作为索引值。NumberOfNames
个元素,说明当前模块没有所要的名字从AddressOfNameOrdinals数组
提取的数值作为AddressOfFunctions数组
的索引。
AddressOfFunctions
数组的第5个元素,此值就是所要函数的RVA已知函数的序数,获取函数地址的一般步骤:
PE header
虚拟地址
Base
值AddressOfFunctions
数组的索引NumberOfFunctions
做比较,大于等于后者则序数无效AddressOfFunctions
数组中的RVA重定位
API函数地址
xxxxxxxxxx
call delta ;执行后,堆栈顶端为delta在内存中的真正地址
delta: pop ebp
;这条语句将delta在内存中的真正地址存放在ebp寄存器中
……
lea eax,[ebp + (offset var1-offset delta)]
;这时eax中存放着var1在内存中的真实地址
Win32 PE病毒
和普通Win32 PE
程序一样需要调用API函数实现某些功能,但是对于Win32 PE病毒来说,它只有代码节,并不存在引入函数节相关API函数
,而应该先找出这些API函数
在相应DLL中的地址要获取API函数地址,首先需要获取KERNEL32的基地址
获取KERNEL32基地址的方法
对相应操作系统分别给出固定的Kernel32模块的基地址
Kernel32模块
的地址是固定的,甚至一些API函数的大概位置都是固定的利用程序的返回地址,在其附近搜索Kernel32模块
基地址
xxxxxxxxxx
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值
……
在得到了Kernel32的模块
地址以后,就可以在该模块中搜索所需要的API地址。
对于给定的API,搜索其地址可以直接通过Kernel32.dll
的引出表信息搜索,同样我们也可以先搜索出GetProcAddress
和LoadLibrary
两个API函数的地址,然后利用这两个API函数得到所需要的API函数地址。
搜索文件是病毒寻找目标文件的非常重要的功能
在Win32汇编
中,通常采用如下几个API
函数进行文件搜索
FindFirstFile
根据文件名查找文件
FindNextFile
FindFirstFile
函数时指定的一个文件名查找下一个文件FindClose
FindFirstFile
函数创建的一个搜索句柄xxxxxxxxxx
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
xxxxxxxxxx
FindFile Proc
①指定找到的目录为当前工作目录
②开始搜索文件(*.*)
③该目录搜索完毕?是则返回,否则继续
④找到文件还是目录?是目录则调用自身函数FindFile,否则继续
⑤是文件,如符合感染条件,则调用感染模块,否则继续
⑥搜索下一个文件(FindNextFile),转到③继续
FindFile Endp
内存映射文件
提供了一组独立的函数,是应用程序能够通过内存指针像访问内存一样对磁盘上的文件进行访问
这组内存映射文件函数将磁盘上的文件的全部或者部分映射到进程虚拟地址空间的某个位置,以后对文件内容的访问就如同在该地址区域内直接对内存访问一样简单。
对文件中数据的操作便是直接对内存进行操作,大大地提高了访问的速度,这对于计算机病毒来说,对减少资源占用是非常重要的
在计算机病毒中,通常采用如下几个步骤使用内存映射文件读写文件
①调用CreateFile函数
打开想要映射的HOST程序,返回文件句柄hFile
②调用CreateFileMapping函数
生成一个建立基于HOST文件句柄hFile
的内存映射对象,返回内存映射对象句柄hMap
③调用MapViewOfFile函数
将整个文件(一般还要加上病毒体的大小)映射到内存中。得到指向映射到内存的第一个字节的指针(pMem)
④用刚才得到的指针pMem
对整个HOST文件
进行操作,对HOST程序
进行病毒感染
⑤调用UnMapViewFile函数
解除文件映射,传入参数是pMem
⑥调用CloseHandle
来关闭内存映射文件,传入参数是hMap
⑦调用CloseHandle
来关闭HOST
文件,传入参数是hFile
CreateFileMapping
–该函数用来创建一个新的文件映射对象
MapViewOfFile
–该函数将一个文件映射对象映射到当前应用程序的地址空间
UnMapViewOfFile
–该函数在当前应用程序的内存地址空间解除对一个文件映射对象的映射
CloseHandle
–该函数用来关闭一个内核对象,其中包括文件、文件映射、进程、线程、安全和同步对象等
判断目标文件开始的两个字节是否为“MZ
”;
判断PE文件标记“PE
”;
判断感染标记,如果已被感染过则跳出继续执行HOST
程序,否则继续;
获得Directory(数据目录)的个数,每个数据目录信息占8个字节;
得到节表起始位置:Directory的偏移地址+数据目录占用的字节数=节表起始位置;
得到目前最后节表的末尾偏移(紧接其后用于写入一个新的病毒节):
开始写入节表
①写入节名(8字节);
②写入节的实际字节数(4字节);
③写入新节在内存中的开始偏移地址(4字节),同时可以计算出病毒入口位置
:
开始写入节表
写入本节(即病毒节)在文件中对齐后的大小;
写入本节在文件中的开始位置:
修改映像文件头中的节表数目
修改AddressOfEntryPoint
(即程序入口点指向病毒入口位置),同时保存旧的AddressOfEntryPoint
,以便返回HOST
继续执行。
更新SizeOfImage
(内存中整个PE映像尺寸=原SizeOfImage
+病毒节经过内存节对齐后的大小);
写入感染标记(后面例子中是放在PE头中);
写入病毒代码到新添加的节中:
–ECX
=病毒长度
–ESI
=病毒代码位置(并不一定等于病毒执行代码开始位置)
–EDI
=病毒节写入位置(后面例子是在内存映射文件中的相应位置)
将当前文件位置设为文件末尾。
CreateFile
– 打开和创建文件等
CloseHandle
– 该函数用来关闭一个内核对象,其中包括文件、文件映射、进程、线程、安全和同步对象等
SetFilePointer
– 在一个文件中设置当前的读写位置
ReadFile/ WriteFile
– 从文件中读取数据/将数据写入文件
SetEndOfFile
– 针对一个打开的文件,将当前文件位置设为文件末尾
GetFileSize
– 获取指定文件的大小
FlushFileBuffers
– 针对指定的文件句柄,刷新内部文件缓冲区
为了提高自己的生存能力,病毒不应该破坏HOST程序,病毒应该在病毒执行完毕后,立刻将控制权交给HOST程序
病毒在修改被感染文件代码开始执行位置(AddressOfEntryPoint
)时,会保存原来的值。这样,病毒在执行完病毒代码之后用一个跳转语句跳到这段代码处继续执行即可
本节结束
2019-10-03