病毒一览"/>
PE文件病毒一览
PE文件病毒
参考1
参考2
参考3
参考4
参考书籍《加密与解密》
PE文件简介
简介
PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)
PE文件是指32位可执行文件,也称为PE32。64位的可执行文件称为PE+或PE32+,是PE(PE32)的一种扩展形式(请注意不是PE64)。
PE文件使用的是一个平面地址空间,所有代码和数据都合并在一起,文件内容被分割为不同区块,区块包含代码和数据,区块按页对齐,没有大小限制,每个块都有自己在内存的属性,如是否包含代码,是否可读可写
认识到PE文件不是作为单一内存映射文件被载入内存是很重要的。Windows 加载器(又称PE 装载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。磁盘文件一旦被载入内存,磁盘上的数据结构布局和内存中的数据结构布局就是一致的。这样,如果在磁盘的数据结构中寻找一些内容, 那么几乎都能在被载入的内存映射文件中找到相同的信息,但数据之间的相对位置可能会改变,某项的偏移地址可能区别于原始的偏移位置。不管怎样,对所有表现出来的信息,都允许进行从磁盘文件偏移到内存偏移的转换。
结构总览
一些基本概念
-
基地址
当PE文件通过Windows加载器载入内存后,内存中的版本称为模块( Module )。映射文件的起始地址称为模块句柄( hModule ),可以通过模块句柄访问内存中的其他数据结构。这个初始内存地址也称为基地址( ImageBase )。
内存中的模块代表进程将这个可执行文件所需要的代码、数据、资源、输入表、输出表及其他有用的数据结构所使用的内存都放在一个 连续的内存块中,程序员只要知道装载程序文件映像到内存后的基地址即可。PE文件的剩余部分可以被读入,但可能无法被映射。例如,在将调试信息放到文件尾部时,PE的一个字段会告诉系统把文件映射到内存时需要使用多少内存,不能被映射的数据将被放置在文件的尾部。
基地址的值是由PE文件本身设定的。按照默认设置,用Visual C++建立的EXE文件的基地址
是40000h、DLL 文件的基地址是000000h。可以在创建应用程序的EXE文件时改变这个地址,方
法是在链接应用时使用链接程序的/BASE选项,或者在链接后通过REBASE应用程序进行设置。 -
虚拟地址
PE文件被系统加载器映射到内存中,每个都有自己虚拟空间,它的内存地址称为虚拟地址(VA)
-
相对虚拟地址
为了避免在PE文件中出现绝对内存地址引入了相对虚拟地址( Relative Virtual Address, RVA )
的概念。RVA只是内存中的一个简单的、相对于PE文件载入地址的偏移位置,它是一个“相对”地
址(或称偏移量)。
将一个RVA转换成真实的地址只是简单地翻转这个过程,即用实际的载入地址加RVA,得到
实际的内存地址。它们之间的关系如下:
虚拟地址(VA) =基地址( ImageBase) +相对虚拟地址( RVA) -
文件偏移地址
当PE文件储存在磁盘中时,某个数据的位置相对于文件头的偏移量称为文件偏移地址( File
Offset )或物理地址( RAW Offset )。文件偏移地址从PE文件的第1个字节开始计数,起始值为0。
用十六进制工具(例如Hex Workshop、WinHex 等)打开文件时所显示的地址就是文件偏移地址。
基本结构
基本结构
结构详解
MS-DOS头
64字节,是用来兼容MS-DOS操作系统的,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode.还有一个目的,就是指明NT头在文件中的位置
定义:
IMAGE_DOS_HEADER {WORD e_magic; // +0000h - EXE标志,“MZ”WORD e_cblp; // +0002h - 最后(部分)页中的字节数WORD e_cp; // +0004h - 文件中的全部和部分页数WORD e_crlc; // +0006h - 重定位表中的指针数WORD e_cparhdr; // +0008h - 头部尺寸,以段落为单位WORD e_minalloc; // +000ah - 所需的最小附加段WORD e_maxalloc; // +000ch - 所需的最大附加段WORD e_ss; // +000eh - 初始的SS值(相对偏移量)WORD e_sp; // +0010h - 初始的SP值WORD e_csum; // +0012h - 补码校验值WORD e_ip; // +0014h - 初始的IP值,代码入口WORD e_cs; // +0016h - 初始的CS值WORD e_lfarlc; // +0018h - 重定位表的字节偏移量WORD e_ovno; // +001ah - 覆盖号WORD e_res[4]; // +001ch - 保留字00WORD e_oemid; // +0024h - OEM标识符WORD e_oeminfo; // +0026h - OEM信息WORD e_res2[10]; // +0028h - 保留字LONG e_lfanew; // +003ch - PE头相对于文件的偏移地址}
需要关注的两个域
-
e_magic
一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位‘MZ’,可执行文件必须都是’MZ’开头
-
e_Ifanew 最后4字节
RVA,为32位可执行文件扩展的域,用来表示DOS头之后的NT头相对文件起始地址的偏移
DOS头和NT头之间是DOS存根,没有它程序也能正常执行
PE文件头
包含windows PE文件的主要信息,其中包括一个‘PE’字样的签名,PE文件头(IMAGE_FILE_HEADER)和PE可选头(IMAGE_OPTIONAL_HEADER32)
PE装载器将从IMAGE DOS _HEADER结构的e. _lfanew字段里找到PE Header的起始偏移量,用其加上基址,得到PE文件头的指针。
定义:
IMAGE_NT_HEADERS {DWORD Signature; // +0000h - PE文件标识,“PE00”IMAGE_FILE_HEADER FileHeader; // +0004h - PE标准头IMAGE_OPTIONAL_HEADER32 OptionalHeader; // +0018h - PE扩展头
}
需要关注的
-
Signature
类似e_magic,其高16位是0,低16是0x4550,字符是PE00
IMAGE_FILE_HEADER
20字节
PE文件头:记录了PE文件的全局属性,如运行平台,PE文件类型,文件中存在的节总数等
IMAGE_FILE_HEADER {WORD Machine; // +0004h - 运行平台WORD NumberOfSections; // +0006h - PE中节的数量,当定义的节段数与实际不符时,将发生运行错误DWORD TimeDateStamp; // +0008h - 文件创建日期和时间DWORD PointerToSymbolTable; // +000ch - 指向符号表DWORD NumberOfSymbols; // +0010h - 符号表中的符号数量WORD SizeOfOptionalHeader; // +0014h - 扩展头结构的长度WORD Characteristics; // +0016h - 文件属性,是否是可运行状态等}
重要成员(设置不正确会导致文件无法正常运行)
-
Machine
每个CPU都有唯一的Machine码
-
NumberOfEsctions
NumberOfEsctions指文件中存在的节段(又称节区)数量,也就是节表中的项数。该值一定要大于0,且当定义的节段数与实际不符时,将发生运行错误。
-
SizeOfOptionalHeader
扩展头结构的长度
-
Characteristics
可执行文件属性
DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1,即DLL中不删除重定位信息,EXE文件中删除重定位信息
IMAGE_OPTIONAL_HEADER32
PE可选头:文件执行时的入口地址、文件被操作系统装入内存后的默认基地址,以及节在磁盘和内存中的对齐单位等信息均可以在此结构中找到。对该结构中的某些数值的随意改动可能会造成PE文件的加载或运行失败
IMAGE_OPTIONAL_HEADER {WORD Magic; // +0018h - 魔术字107h = ROM Image,10bh = exe ImageBYTE MajorLinkerVersion; // +001ah - 链接器版本号BYTE MinorLinkerVersion; // +001bh - DWORD SizeOfCode; // +001ch - 所有含代码的节的总大小DWORD SizeOfInitializedData; // +0020h - 所有含已初始化数据的节的总大小DWORD SizeOfUninitializedData; // +0024h - 所有含未初始化数据的节的大小DWORD AddressOfEntryPoint; // +0028h - 程序执行入口RVADWORD BaseOfCode; // +002ch - 代码的节的起始RVADWORD BaseOfData; // +0030h - 数据的节的起始RVADWORD ImageBase; // +0034h - 程序的建议装载地址DWORD SectionAlignment; // +0038h - 内存中的节的对齐粒度DWORD FileAlignment; // +003ch - 文件中的节的对齐粒度WORD MajorOperatingSystemVersion; // +0040h - 操作系统版本号WORD MinorOperatingSystemVersion; // +0042h - WORD MajorImageVersion; // +0044h - 该PE的版本号WORD MinorImageVersion; // +0046h - 链接器版本号WORD MajorSubsystemVersion; // +0048h - 所需子系统的版本号WORD MinorSubsystemVersion; // +004ah - DWORD Win32VersionValue; // +004ch - 未用DWORD SizeOfImage; // +0050h - 内存中的整个PE映象尺寸DWORD SizeOfHeaders; // +0054h - 所有头+节表的大小DWORD CheckSum; // +0058h - 校验和WORD Subsystem; // +005ch - 文件的子系统WORD DllCharacteristics; // +005eh - DLL文件特性DWORD SizeOfStackReserve; // +0060h - 初始化时的栈大小DWORD SizeOfStackCommit; // +0064h - 初始化时实际提交的栈大小DWORD SizeOfHeapReserve; // +0068h - 初始化时保留的堆大小DWORD SizeOfHeapCommit; // +006ch - 初始化时实际提交的堆大小DWORD LoaderFlags; // +0070h - 与调试有关DWORD NumberOfRvaAndSizes; // +0074h - 下面的数据目录结构的项目数量IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 0078h - 数据目录
}
重要成员(程序运行所必须的,设置错误将导致无法正常运行)
-
Magic
可选头的类型
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b // 32位PE可选头 #defineIMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头 #defineIMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
-
AddressOfEntryPoint
程序入口的RVA,对于exe可以理解为WinMain的RVA。对于DLL可以理解为DllMain的RVA,对于驱动程序,可以理解为DriverEntry的RVA
-
ImageBase
映象(加载到内存中的PE文件)的基地址
EXE文件总是按照这个地址装入
DLL文件则包含了重定位信息,地址可变(因为个DLL文件全部使用宿主EXE文件的地址空间,优先装入的地址可能被其他DLL文件占用)
一般来说,使用开发工具(VB/VC++/Delphi)创建好EXE文件后,其ImageBase值为00400000,DLL文件的ImageBase值为10000000(当然也可以指定其他值)。
执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint
-
SectionAlignment,FileAlignment
FileAlignment指定了节段在磁盘文件中的最小单位,而SectionAlignment则指定了节区在内存中的最小单位(SectionAlignment必须大于或者等于FileAlignment)
-
SizeOfImage
当PE文件加载到内存时,SizeOfImage指定了PE Image在虚拟内存中所占用的空间大小,一般文件大小与加载到内存中的大小是不同的
-
SizeOfHeader
所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的,必须是FileAlignment的整数倍
-
Subsystem
用来区分系统驱动文件(.sys)与普通可执行文件(.exe,*.dll)
-
IMAGE_OPTIONAL_DIRECTORY
定义了PE文件中出现的所有不同类型的数据的目录信息
IMAGE_DATA_DIRECTORY {DWORD VirtualAddress; // +0000h - 数据的起始RVADWORD Size; // +0004h - 数据块的长度 }
数组中的每一项对应一个特定的数据结构
重点关注导入和导出表
-
NumberOfRvaAndSizes
数据目录的项数,即上面这个数组的项数
-
DllCharacteristics
DLL的文件属性,只对DLL文件有效,可以是下面定义中某些的组合
是PE文件后续节的描述,windows根据节表的描述加载每个节,每个节表项记录了PE中与某个特定的节有关的信息,如节的属性、节的大小、在文件和内存中的起始位置等
IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // +0000h - 8个字节节名
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; // +0008h - 节区的尺寸
DWORD VirtualAddress; // +000ch - 节区的RVA地址
DWORD SizeOfRawData; // +0010h - 在文件中对齐后的尺寸
DWORD PointerToRawData; // +0014h - 在文件中的偏移
DWORD PointerToRelocations; // +0018h - 在OBJ文件中使用
DWORD PointerToLinenumbers; // +001ch - 行号表的位置(供调试用)
WORD NumberOfRelocations; // +0020h - 在OBJ文件中使用
WORD NumberOfLinenumbers; // +0022h - 行号表中行号的数量
DWORD Characteristics; // +0024h - 节的属性
}
#### 节表节表是PE文件后续节的描述由一系列的*IMAGE_SECTION_HEADER*结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的*IMAGE_SECTION_HEADER*结构作为结束,所以节表中*IMAGE_SECTION_HEADER*结构数量等于节的数量加一。节表总是被存放在紧接在PE文件头的地方节段头定义:```c
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];名字
union {DWORD PhysicalAddress;DWORD VirtualSize;
} Misc;尺寸
DWORD VirtualAddress; RVA
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
重要关注
-
Name
区块名。这是一个由8个ASCII码组成,用来定义区块的名称的数组。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.”实际上是不是必须的。值得我们注意的是,如果区块名达到8 个字节,后面就没有0字符了。前边带有一个“ ” 的 区 块 名 字 会 从 连 接 器 那 里 得 到 特 殊 的 待 遇 , 前 边 带 有 “ ” 的区块名字会从连接器那里得到特殊的待遇,前边带有“ ”的区块名字会从连接器那里得到特殊的待遇,前边带有“”的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$”后边的字符的字母顺序进行合并的。每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data”或者说将包含数据的区块命名为“.Code”都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。 -
VirtualSize
对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。
-
VirtualAddress
该区块装载到内存中的RVA地址。这个地址是按照内存页来对齐的,因此它的数值总是SectionAlignment的值的整数倍。
-
PointerToRawData
指出节在磁盘文件中所处的位置。这个数值是从文件头开始算起的偏移量。
-
SizeOfRawData
该区块在磁盘中所占的大小,这个数值等于VirtualSize字段的值按照FileAlignment的值对齐以后的大小。
-
Characteristics
该区块的属性。该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。
装载过程:
依靠PointerToRawData,SizeOfRawData,VirtualAddress,VirtualSize这4个字段的值,装载器就可以从PE文件中找出某个节(从 PointerToRawData 偏移开始的 SizeOfRawData 字节)的数据,并将它映射到内存中去(映射到从模块基地址偏移 VirtualAddress 的地方,并占用以VirtualSize的值按照页的尺寸对齐后的空间大小)
节块
PE文件有不同的节段:code(代码),data(数据),resource(资源)
用LordPE打开如下
在这里插入图片描述
每个是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义
当编程从PE 文件中读取需要的内容时,如输入表、输出表,不能以区块名字作为参考,正确的方法是按照数据目录表中的字段来进行定位
-
块的偏移地址
按照IMAGE_OPTIONAL_HEADER32中的FileAlignment字段的值进行对齐的,而当被加载到内存中时是按照同一结构中的SectionAlignment字段的值设置对齐的,两者的值可能不同
-
常见区块名称
可以自己命名
如果两个区块有相似,一致的属性,那么在链接的时候能被合并成一个单一区块
-
区块对齐
无论是在内存中存放还是在磁盘中存放,区块是要对齐的,但它们一般的对齐值是不同的
PE 文件头里边的FileAligment 定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙
PE 文件头里边的SectionAligment 定义了内存中区块的对齐值。PE 文件被映射到内存中时,区块总是至少从一个页边界开始
-
RVA和文件偏移的转换
RVA 是当PE 文件被装载到内存中后,某个数据位置相对于文件头的偏移量。将 RVA 的值加上文件被装载的基地址,就可以找到数据在内存中的实际地址
任何的 RVA 必须经过到文件偏移的换算,才能用来定位并访问文件中的数据
换算方法
步骤一:循环扫描区块表得出每个区块在内存中的起始 RVA(根据IMAGE_SECTION_HEADER 中的VirtualAddress 字段),并根据区块的大小(根据IMAGE_SECTION_HEADER 中的SizeOfRawData 字段)算出区块的结束 RVA(两者相加即可),最后判断目标 RVA 是否落在该区块内。
步骤二:通过步骤一定位了目标 RVA 处于具体的某个区块中后,那么用目标 RVA 减去该区块的起始 RVA ,这样就能得到目标 RVA 相对于起始地址的偏移量 RVA2.
步骤三:在区块表中获取该区块在文件中所处的偏移地址(根据IMAGE_SECTION_HEADER 中的PointerToRawData 字段), 将这个偏移值加上步骤二得到的 RVA2 值,就得到了真正的文件偏移地址。
PE文件加载过程
一些概念
- 虚拟内存地址(Virtual Address, VA)PE文件中的指令被装入内存后的地址。
- 相对虚拟内存地址(Reverse Virtual Address, RVA相对虚拟地址是内存地址相对于映射基址的偏移量。
- 文件偏移地址(File Offset Address, FOA)数据在PE文件中的地址叫文件偏移地址,这是文件在磁盘上存放时相对于文件开头的偏移。
- 装载基址(Image base)PE装入内存时的基地址。默认情况下,EXE文件在内存中的基地址时0x00400000, DLL文件是0x10000000。这些位置可以通过修改编译选项更改。
- 虚拟内存地址、映射基址、相对虚拟内存地址的关系:
VA = Image Base + RVA
PE头内部信息大多是RVA形式存在。原因在于(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他的PE文件(DLL)。此时必须通过重定向(Relocation)将其加载到其他空白的位置,若PE头信息使用的是VA,则无法正常访问。因此使用RVA来重定向信息,即使发生了重定向,只要相对于基准位置的相对位置没有变化,就能正常访问到指定信息,不会出现任何问题。
- 文件偏移是相对于文件开始处0字节的偏移,相对虚拟地址则是相对于装载基址0x00400000处的偏移。(1)PE文件中的数据按照磁盘数据标准存放,以0x200字节为基本单位进行组织,PE数据节的大小永远是0x200的整数倍。(2)当代码装入内存后,将按照内存数据标准存放,并以0x1000字节为基本单位进行组织,内存中的节总是0x1000的整数倍。
- 内存中数据节相对于装载基址的偏移量和文件中数据节的偏移量的差异称为节偏移。
文件偏移地址 = 虚拟内存地址(VA) - 装载基址(Image Base) - 节偏移 = RVA - 节偏移
具体过程:
- PE装载器首先检查DOS header里的PE header的偏移量。如果找到,则直接跳转到PE header的位置
- 当PE装载器跳转到PE header后,第二步要做的就是检查PE header是否有效。如果该PE header有效,就跳转到PE header的尾部
- 紧跟PE header尾部的是节表。PE装载器执行完第二步后开始读取节表中的节段信息,并采用文件映射(Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系)的方法将这些节段映射到内存,同时附上节表里指定节段的读写属性
- PE文件映射入内存后,PE装载器将继续处理PE文件中类似 import table (输入表)的逻辑部分
PE文件被加载到内存中后,它就被称作映像(image),由于内存中是按照页对齐的,所以占用的空间会比硬盘中大一些。
结构图
导入表
输入表就相当于 EXE文件与 DLL文件沟通的钥匙,所有的导入函数信息都会写入输入表中,在PE 文件映射到内存后,Windows 将相应的 DLL文件装入,EXE 文件通过“输入表”找到相应的 DLL 中的导入函数,从而完成程序的正常运行,这一动态连接的过程都是由“输入表”参与的。
-
输入函数
输入函数,表示被程序调用但是它的代码不在程序代码中的,而在DLL中的函数。对于这些函数,磁盘上的可执行文件只是保留相关的函数信息,如函数名,DLL文件名等。在程序运行前,程序是没有保存这些函数在内存中的地址。当程序运行起来时,windows加载器会把相关的DLL装入内存,并且将输入函数的指令与函数真在内存中正的地址联系起来。输入表(导入表)就是用来保存这些函数的信息的。
-
输出表的位置
我们知道,在IMAGE_OPTIONAL_HEADER 中的 DataDirectory数组保存了输入表的RVA跟大小。通过RVA可以在OllyDbg中加载程序通过ImageBase+RVA 找到输入表,或者通过RVA计算出文件偏移地址,查看磁盘中的可执行文件,通过文件偏移地址找到输入表。
-
IAT
在PE文件内有一组数据结构,它们分别对应于被输入的DLL。每一个这样的结构都给出了被输入的DLL的名称并指向一组函数指针。这组函数指针称为输入地址表( Import Address Table, IAT )。每一个被引入的API在IAT里都有保留的位置,在那里它将被Windows加载器写入输入函数的地址。最后一点特别重要: 一旦模块被载入,IAT 中将包含所要调用输入函数的地址。
-
导入表的结构
输入表是以一个IMAGE_IMPORT_DESCRIPTOR(简称IID) 的数组开始。每个被 PE文件链接进来的 DLL文件都分别对应一个 IID数组结构。在这个 IID数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以一个全为NULL(0) 的 IID 作为结束的标志。
IID结构如下
typedef struct_IMAGE_IMPORT_DESCRIPTOR { _ANONYMOUS_UNION union{ //00h DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; //04h DWORD ForwarderChain; //08h DWORD Name; //0Ch DWORD FirstThunk; //10h }IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
重点关注
-
OriginalFirstThunk
包含指向输入表名称(INT)的RVA。一个IMAGE_THUNK_DATA数组叫做输入名称表Import Name Table(INT),用来保存函数,数组中的每个IMAGE THUNK DATA结构都指IMAGE_IMPORT_ BY_NAME结构,数组以一个内容为0的IMAGE _THUNK _DATA结构结束。
-
Name
DLL名字的指针, 指向一个用NULL作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称,如:KERNEL32.DLL
-
FirstThunk
它也指向IMAGE_THUNK_DATA数组叫做输入地址表Import Address Table(IAT)
OriginalFirstThunk与FirstThunk 相似,它们分别指向两个本质上相同的数组IMAGE THUNK
DATA结构。这些数组有好几种叫法,最常见的是输人名称表( Import Name Table, INT )和输人地
址表( Import Address Table, IAT)。IMAGE_THUNK_DATA
typedef struct_IMAGE_THUNK_DATA32 { union { DWORD ForwarderString;//转向者字符串的RVA DWORD Function;//被输入的函数的内存地址 DWORD Ordinal;//被输入的API的序数值
DWORD AddressOfData;//指向 IMAGE_IMPORT_BY_NAME
} u1;
}IMAGE_THUNK_DATA32,*PIMAGE_THUNK_DATA32;当IMAGE_THUNK_DATA 的值最高位为1时,表示函数是以序号方式输入,这时低31为被当作函数序号。当最高位是0时,表示函数是以字符串类型的函数名方式输入的,这时,IMAGE_THUNK_DATA 的值为指向IMAGE_IMPORT_BY_NAME 的结构的RVA。**IMAGE_IMPORT_BY_NAME**
-
typedef struct_IMAGE_IMPORT_BY_NAME {
WORD Hint; // 表示这个函数在其所驻留DLL的输出表的序号,不是必须的BYTE Name[1]; //函数名,是一个ASCII字符串以0结尾,大小不固定} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
两个 IMAGE_THUNK_DATA 数组指向IMAGE_THUNK_DATA 的原因
第1个数组(由OriginalFirstThunk所指向)是单独的一项,不可改写,称为INT,有时也称为提示名表( Hint- -nameTable)。第2个数组(由FirstThunk所指向)是由PE装载器重写的。PE装载器先搜索OriginalFirst。当程序加载时,IAT 会被PE加载器重写,Table)。第2个数组(由FirstThunk所指向)是由PE装载器重写的。PE装载器先搜索OriginalFirstThunk,如果找到,加载程序就迭代搜索数组中的每个指针,找出每个IMAGE IMPORT_ BY NAME结构所指向的输人函数的地址。然后,加载器用函数真正的人口地址来替代由FirstThunk 指向的IMAGE THUNK _DATA数组里元素的值。“Jmp dword ptr xxxxxx]”语句中的“xxxxd”是指FirstThunk数组中的-一个人口,因此称为输人地址表( Import Address Table, IAT)。所以,当PE文件装载内存后准备执行时,图11.13 已转换成如图11.14所示的状态,所有函数人口地址排列在- -起。此时,输人表中的其他部分就不重要了,程序依靠IAT提供的函数地址就可以正常运行。。PE加载器先搜索INT,PE加载器迭代搜索INT数组中的每个指针,找出 INT所指向的IMAGE_IMPORT_BY_NAME结构中的函数在内存中的真正的地址,并把它替代原来IAT中的值。当完成后,INT就没有用了,程序只需要IAT就可以正常运行了。
导出表
用来描述模块中的导出函数的结构,如果一个模块导出了函数,那么这个函数会被记录在导出表中,这样通过GetProcAddress函数就能动态获取到函数的地址。
当一个DLL函数能被EXE或另一个DLL文件使用时,就相当于被输出了。
函数导出的方式有两种,一种是按名字导出,一种是按序号导出。
定义
typedef struct _IMAGE_EXPORT_DIRECTORY {DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVAfrom base of image DWORD AddressOfNames; // RVA frombase of image
DWORD AddressOfNameOrdinals; // RVAfrom base of image 数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号
}IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
获取导出函数地址时,先在AddressOfNames中找到对应的名字,比如Func2,他在AddressOfNames中是第二项,然后从AddressOfNameOrdinals中取出第二项的值,这里是2,表示函数入口保存在AddressOfFunctions这个数组中下标为2的项里,即第三项,取出其中的值,加上模块基地址便是导出函数的地址。如果函数是以序号导出的,那么查找的时候直接用序号减去Base,得到的值就是函数在AddressOfFunctions中的下标。
-
从序号查找函数入口地址
1.定位到PE 文件头
2.从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA
3.从导出表的 Base 字段得到起始序号
4.将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引
5.检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的
6.用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址
-
从函数名称查找入口地址
1.最初的步骤是一样的,那就是首先得到导出表的地址
2.从导出表的 NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环
3.从 AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数
4.如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x
5.最后,以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址
重定位
当链接器生成一个PE文件时,会假设这个文件在执行时被装载到默认的基地址处,并把code
和data的相关地址都写入PE文件。如果载人时将默认的值作为基地址载人,则不需要重定位。但
是,如果PE文件被装载到虚拟内存的另一个地址中,链接器登记的那个地址就是错误的,这时就
需要用重定位表来调整。在PE文件中,重定位表往往单独作为一块,用“.reloc" 表示。
基址重定位概念
PE格式不参考外部DLL或模块中的其他区块,而是把文件中所有可能需要修改的地址放在一个数组里。如果PE文件不在首选的地址载人,那么文件中的每一个定位都需要被修正。对加载器来说,它不需要知道关于地址使用的任何细节,只要知道有一系列的数据需要以某种一致的方式来修正就可以了。
对EXE文件来说,每个文件总是使用独立的虚拟地址空间,所以EXE总是能够按照这个地址载入,这意味着EXE文件不再需要重定位信息。对DLL来说,因为多个DLL文件使用宿主EXE文件的地址空间,不能保证载人地址没有被其他DLL使用,所以DLL文件中必须包含重定位信息,除非用一个/FIXED开关来忽略它们。在Visual Studio .NET中,链接器会为Debug和Release模式的EXE文件省略基址重定位,因此,在不同系统中跟踪同一个DLL文件时,其虚拟地址是不同的
重定位表的结构
基址重定位表( Base Relocation Table )位于-个.reloc区块内,找到它们的正确方式是通过数据目录表IMAGE_DIRECTORY_ ENTRY _BASERELOC
条目查找。基址重定位数据采用类似按页分割的方法组织,是由许多重定位块串接成的,每个块中存放4KB ( 1000h)的重定位信息,每个重定位数据块的大小必须以DWORD( 4字节)对齐。它们以一一个IMAGE_ BASE RELOCATION结构开始,格式如下。
struct_IMAGE_BASE_RELOCATION
{DWORD VirtualAddress; //重定位数据开始的RVA地址DWORD SizeOfBlock; //重定位块的长度WORD TypeOffset; //重定位项位数组
}IMAGE_BASE_RELOCATION;
- VirtualAddress:这组重定位数据的开始RVA地址。各重定位项的地址加这个值才是该重定
位项的完整RVA地址。 - SizeOfBlock:当前重定位结构的大小。因为VirtualAddress 和SizeOfBlock的大小都是固定的
4字节,所以这个值减8就是TypeOffset数组的大小 - Typeffset:一个数组。数组每项大小为2字节,共16位。这16位分为高4位和低12位。
高4位代表重定位类型;低12位是重定位地址,它与VirtualAddress相加就是指向PE映像
中需要修改的地址数据的指针。
常见重定位类型
重定位表结构图
PE病毒分析
参考链接
特点
该病毒是将可执行文件的代码中程序入口地址改为病毒的程序入口,这样就会导致用户在运行的时候执行病毒文件
PE文件判断
- 判断是否是PE文件
-
检验文件头部第一个字的值是否等于MZ,如果是,则DOS头有效。
-
用DOS头的字段e_lfanew来定位PE头。
-
比较PE透的第一个双字的值是否等于45500000H(PE\0\0)。
-
感染的一般方法
- 判断目标文件开始的两个字节是否为“MZ”;
- 判断PE文件标记“PE”;
- 判断感染标记,如果已被感染过则跳过这个文件,否则继续;
- 获得Directory(数据目录)的个数,每个数据目录信息占8个字节;
- 得到节表起始位置:Directory的地址+数据目录占用的字节数=节表起始位置;
- 得到目前最后节表的末尾偏移(紧接其后用于写入一个新的病毒节):
节表起始位置+节的个数×(每个节表占用的字节数28H)=目前最后节表的末尾偏移 - 开始写入新的节表项
- 写入节名(8字节);
- 写入节的实际字节数(4字节);
- 写入新节在内存中的开始偏移地址(4字节),同时可以计算出病毒入口位置:
上节在内存中的开始偏移地址+(上节大小/节对齐+1)×节对齐 - 写入新节(即病毒节)在文件中对齐后的大小;
- 写入新节在文件中的开始位置:
上节在文件中的开始位置+上节对齐后的大小
- 修改映像文件头中的节表数目
- 修改AddressOfEntryPoint(即程序入口点指向病毒入口位置),同时保存旧的AddressOfEntryPoint,以便返回HOST继续执行。
- 更新SizeOfImage(内存中整个PE映像尺寸=原SizeOfImage+病毒节经过内存节对齐后的大小);
- 写入感染标记(可以放在PE头中);
- 写入病毒代码到新节指向的文件偏移中。
更多推荐
PE文件病毒一览
发布评论