安徽师范大学——计算机与信息学院————作者(授课老师):周文
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 headerPE文件头的结构
xxxxxxxxxxIMAGE_NT_HEADERS STRUCT {Signature dd ?FileHeader IMAGE_FILE_HEADER <>OptionalHeader IMAGE_OPTIONAL_HEADER32 <>}IMAGE_NT_HEADERS ENDS
紧跟着“PE\0\0”的是映像文件头,是NT映像头的主要部分,它包含有PE文件的基本信息
xxxxxxxxxxtypedef 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,一般是0x010BBYTE 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; // 0x44WORD MajorSubsystemVersion; // 0x46,子系统版本号WORD MinorSubsystemVersion; // 0x48DWORD Win32VersionValue; // 0x4c,Win32版本,一般是0DWORD SizeOfImage; // 0x50,程序调入后占用内存大小(字节)DWORD SizeOfHeaders; // 0x54,文件头的长度之和DWORD CheckSum; // 0x58,校验和WORD Subsystem; // 0x5c,可执行文件的子系统WORD DllCharacteristics; // 0x5e,何时DllMain被调用,一般为0DWORD SizeOfStackReserve; // 0x60,初始化线程时保留的堆栈大小DWORD SizeOfStackCommit; // 0x64,初始化线程时提交的堆栈大小DWORD SizeOfHeapReserve; // 0x68,进程初始化时保留的堆大小DWORD SizeOfHeapCommit; // 0x6c,进程初始化时提交的堆大小DWORD LoaderFlags; // 0x70,装载标志,与调试相关DWORD NumberOfRvaAndSizes; // 0x74,数据目录的项数,一般是16IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
节通过节表实现索引
节的内容才是要真正执行的程序和相关数据修改节表NumberOfSections决定xxxxxxxxxx#define IMAGE_SIZEOF_SHORT_NAME 8typedef struct _IMAGE_SECTION_HEADER {UCHAR Name[IMAGE_SIZEOF_SHORT_NAME]; // 节名union {ULONG PhysicalAddress; // OBJ文件中表示本节物理地址ULONG VirtualSize; // EXE文件中表示节的实际字节数} Misc;ULONG VirtualAddress; // 本节的RVAULONG 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的结构如下:
xxxxxxxxxxtypedef struct _IMAGE_IMPORT_DESCRIPTOR {union {DWORD Characteristics;DWORD OriginalFirstThunk; //IMAGE_THUNK_DATA数组的指针};DWORD TimeDateStamp; //文件建立时间DWORD ForwarderChain; //一般为0DWORD Name; //DLL名字的指针DWORD FirstThunk; //通常也是IMAGE_THUNK_DATA数组的指针} IMAGE_IMPORT_DESCRIPTOR;
引出函数节.edata
引出函数节的开始,是一个IMAGE_EXPORT_DIRECTORY结构:
xxxxxxxxxxtypedef struct _IMAGE_EXPORT_DIRECTORY {DWORD Characteristics; // 一般为0DWORD TimeDateStamp; // 文件生成时间WORD MajorVersion; // 主版本号WORD MinorVersion; // 次版本号DWORD Name; // 指向DLL的名字DWORD Base; // 基数,加上序数就是函数地址数组的索引值DWORD NumberOfFunctions; // AddressOfFunctions数组的项数DWORD NumberOfNames; // AddressOfNames数组的项数DWORD AddressOfFunctions; // RVA from base of imageDWORD AddressOfNames; // RVA from base of imageDWORD 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函数地址xxxxxxxxxxcall 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模块基地址
xxxxxxxxxxmov ecx,[esp] ;将堆栈顶端的数据(返回Kernel32的地址)赋给ecxxor edx,edxgetK32Base:dec ecx ;逐字节比较验证,也可以一页一页地搜mov dx,word ptr [ecx+IMAGE_DOS_HEADER.e_lfanew] ;就是ecx+3chtest dx,0f000h ;Dos Header+stub不可能太大,超过4096bytejnz 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 函数创建的一个搜索句柄xxxxxxxxxxWIN32_FIND_DATA STRUCTdwFileAttributes 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
xxxxxxxxxxFindFile 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