一、PE文件结构 1.1 可执行文件可执行文件(executable file) 指的是可以由操作系统进行加载的=执行的文件。可执行文件格式: windows 平台: PE(Portable Executabl…
一、PE文件结构
1.1 可执行文件
可执行文件(executable file) 指的是可以由操作系统进行加载的=执行的文件。
可执行文件格式:
- windows 平台: PE(Portable Executable) 文件结构。
- Linux 平台:ELF(Executable and Linking Format) 文件结构。
用处:
- 病毒与反病毒
- 外挂与反外挂
- 加壳与脱壳
- 无源码修改功能,汉化
1.2 识别PE文件
识别是否为 PE 文件:
通过 PE 文件的特征,(PE指纹)
前两字节ascii是MZ, 3C位置的十六进制对位位置ascii是 PE。
exe文件
dll 文件
这些方框内的即为 PE 指纹。
txt 文件,很明显不是 pe 文件,
他是借助 notepad
记事本软件打开的。
二、PE文件格式
定义PE格式的主要是头文件 winnt.h
.
2.1 DOS 部分
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:一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位 MZ,可执行文件必须都是 MZ 开头。
e_lfanew:为32位可执行文件扩展的域,用来表示DOS头之后的PE头相对文件起始地址的偏移。
18个 WORD 类型,一个 LONG 类型
typedef long LONG;
18*2+4 = 64
DOS MZ头,64字节
DOS 块 不确定大小,DOS MZ头与PE头之间的部分即为 DOS块。可随意修改,不影响。
2.2 PE 文件头
前四字节为PE文件头标志,可根据 DOS头的 e_lfanew 得到。
(一个64位,一个32位)
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
DWORD Signature;
4字节,小端存储, 00004550 ,代表PE文件头标志
-
IMAGE_FILE_HEADER FileHeader;
20字节,代表PE文件表头typedef struct _IMAGE_FILE_HEADER { WORDMachine; WORDNumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORDSizeOfOptionalHeader; WORDCharacteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
2+2+4+4+4+2+2=20
-
WORDMachine;
2字节,该文件的运行平台,是x86、x64还是I64等等,可以是下面值里的某一个。#define IMAGE_FILE_MACHINE_UNKNOWN 0 #define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386. #define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian #define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian #define IMAGE_FILE_MACHINE_R100000x0168 // MIPS little-endian #define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2 #define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP #define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian #define IMAGE_FILE_MACHINE_SH3DSP0x01a3 #define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian #define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian #define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5 #define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian #define IMAGE_FILE_MACHINE_THUMB 0x01c2 #define IMAGE_FILE_MACHINE_AM33 0x01d3 #define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian #define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1 #define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64 #define IMAGE_FILE_MACHINE_MIPS160x0266 // MIPS #define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64 #define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS #define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS #define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64 #define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon #define IMAGE_FILE_MACHINE_CEF 0x0CEF #define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code #define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8) #define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian #define IMAGE_FILE_MACHINE_CEE 0xC0EE
-
WORDNumberOfSections;
2字节,该PE文件中有多少个节,也就是节表中的项数。即有多少区段表。 -
DWORD TimeDateStamp;
4字节,PE文件的创建时间,一般有连接器填写。比如下图
3FCCF133
转换成十进制就是1070395699
,也就是2003-12-03 04:08:19
。注意此时间不会随着更改程序某些字节的变化而变化。 -
DWORD PointerToSymbolTable;
4 字节,COFF文件符号表在文件中的偏移,现在基本没用了。 -
DWORD NumberOfSymbols;
4 字节,符号表的数量。如果有COFF 符号表,它代表其中的符号数目,COFF符号是一个大小固定的结构,如果想找到COFF 符号表的结束位置,则需要这个变量。 -
WORDSizeOfOptionalHeader;
2字节,紧随其后的PE可选头的大小。下图为 00E0 即为32字节。 -
WORDCharacteristics;
2字节,可执行文件的属性,,有选择的通过几个值可以运算得到。( 这些标志的有效值是定义于 winnt.h 内的 IMAGEFILE** 的值,具体含义见下表。普通的EXE文件这个字段的值一般是 0100h,DLL文件这个字段的值一般是 210Eh。)多种属性可以通过 “或运算” 使得同时拥有!#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file. #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references). #define IMAGE_FILE_LINE_NUMS_STRIPPED0x0004 // Line nunbers stripped from file. #define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file. #define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set #define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses #define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed. #define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine. #define IMAGE_FILE_DEBUG_STRIPPED0x0200 // Debugging info stripped from file in .DBG file #define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file. #define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file. #define IMAGE_FILE_SYSTEM0x1000 // System File. #define IMAGE_FILE_DLL 0x2000 // File is a DLL. #define IMAGE_FILE_UP_SYSTEM_ONLY0x4000 // File should only be run on a UP machine #define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
可以看出,PE文件头定义了PE文件的一些基本信息和属性,这些属性会在PE加载器加载时用到,如果加载器发现PE文件头中定义的一些属性不满足当前的运行环境,将会终止加载该PE。
PE文件表头,标准 PE 头,20 字节。
PE文件表头可选部分,PE扩展PE头
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORDMagic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
BYTEMajorLinkerVersion;// 链接程序的主版本号
BYTEMinorLinkerVersion;// 链接程序的次版本号
DWORD SizeOfCode; // 代码段的长度,如果有多个代码段,则是代码段长度的总和。
DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
DWORD AddressOfEntryPoint; // 程序执行入口 RVA
DWORD BaseOfCode;// 代码的区块的起始RVA
DWORD BaseOfData;// 数据的区块的起始RVA
//
// NT additional fields.
//
DWORD ImageBase; // 程序的首选装载地址,数据机制
DWORD SectionAlignment; // 内存中的区块的对齐大小,块对齐
DWORD FileAlignment; // 文件中的区块的对齐大小,文件块对齐
WORDMajorOperatingSystemVersion;// 要求操作系统最低版本号的主版本号
WORDMinorOperatingSystemVersion;// 要求操作系统最低版本号的副版本号
WORDMajorImageVersion; // 可运行于操作系统的主版本号
WORDMinorImageVersion; // 可运行于操作系统的次版本号
WORDMajorSubsystemVersion; // 要求最低子系统版本的主版本号
WORDMinorSubsystemVersion;// 要求最低子系统版本的次版本号
DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
DWORD SizeOfImage; // 映像装入内存后的总尺寸
DWORD SizeOfHeaders;// 所有头 + 区块表的尺寸大小
DWORD CheckSum; // 映像的校检和
WORDSubsystem; // 可执行文件期望的子系统
WORDDllCharacteristics;// DllMain()函数何时被调用,默认为 0
DWORD SizeOfStackReserve;// 初始化时的栈大小
DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
DWORD LoaderFlags; // 与调试有关,默认为 0
DWORD NumberOfRvaAndSizes;//下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; // 数据目录表
-
WORDMagic;
, 表示可选头的类型。#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b // 32位PE可选头 #define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头 #define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
-
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的数据块的IMAGE_DATA_DIRECTORY结构的定义很简单,它仅仅指出了某种数据块的位置和长度。
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
VirtualAddress:是一个RVA。
Size:是一个大小。数据目录列表的含义如下:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory #define IMAGE_DIRECTORY_ENTRY_RESOURCE2 // Resource Directory #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory #define IMAGE_DIRECTORY_ENTRY_SECURITY4 // Security Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory // IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE7 // Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP #define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG10 // Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT12 // Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
在PE文件中寻找特定的数据时就是从这些IMAGE_DATA_DIRECTORY结构开始的,比如要存取资源,那么必须从第3个IMAGE_DATA_DIRECTORY结构(索引为2)中得到资源数据块的大小和位置;同理,如果要查看PE文件导入了哪些DLL文件的哪些API函数,那就必须首先从第2个IMAGE_DATA_DIRECTORY结构得到导入表的位置和大小。
32 为 224 字节,可扩展, 一行 16 字节,14 行
2.3 节表
参考下边 三、PE两种状态
PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,
typedef struct _IMAGE_SECTION_HEADER {BYTEName[IMAGE_SIZEOF_SHORT_NAME]; // 8个字节的节区名称union {DWORD PhysicalAddress;DWORD VirtualSize; //节区的尺寸} Misc;DWORD VirtualAddress; // 节区的 RVA 地址DWORD SizeOfRawData;// 在文件中对齐后的尺寸DWORD PointerToRawData; // 在文件中的偏移量DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移DWORD PointerToLinenumbers;// 行号表的偏移(供调试使用地)WORDNumberOfRelocations;// 在OBJ文件中使用,重定位项数目WORDNumberOfLinenumbers;// 行号表中行号的数目DWORD Characteristics;// 节属性如可读,可写,可执行等} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
顺序排列的一系列节表数量和节的数量相应数据。
分别都为 40 字节。
2.4 节数据
从这往下的就是节数据了。
三、PE两种状态
在执行一个PE文件的时候,windows 并不在一开始就将整个文件读入内存的,而是采用与内存映射文件类似的机制。也就是说,windows 装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系。当且仅当真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。
但是要注意的是,系统装载可执行文件的方法又不完全等同于内存映射文件。当使用内存映射文件的时候,系统对“原著”相当忠实,如果将磁盘文件和内存映像比较的话,可以发现不管是数据本身还是数据之间的相对位置的都是完全相同的。而我们知道,在装载可执行文件的时候,有些数据在装入前会被预处理,如重定位等,正因此,装入以后,数据之间的相对位置可能发生微妙的变化.
Windows 装载器在装载DOS部分、PE文件头部分和节表(区块表)部分是不进行任何特殊处理的,而在装载节(区块)的时候则会自动按节(区块)的属性做不同的处理。
①内存页的属性:
对于磁盘映射文件来说,所有的页都是按照磁盘映射文件函数指定的属性设置的。但是在装载可执行文件时,与节对应的内存页属性要按照节的属性来设置。所以,在同属于一个模块的内存页中,从不同节映射过来的的内存页的属性是不同的。
②节的偏移地址:
节的起始地址在磁盘文件中是按照 IMAGE_OPTIONAL_HEADER32 结构的 FileAlignment 字段的值进行对齐的,而当被加载到内存中时是按照同一结构中的 SectionAlignment 字段的值对其的,两者的值可能不同,所以一个节被装入内存后相对于文件头的偏移和在磁盘文件中的偏移可能是不同的。
注意,节事实上就是相同属性数据的组合!当节被装入到内存中的时候,相同一个节所对应的内存页都将被赋予相同的页属性, 事实上,Windows 系统对内存属性的设置是以页为单位进行的,所以节在内存中的对齐单位必须至少是一个页的大小。(对于32位操作系统来说,这个值一般是4KB==1000H; 对于64位操作系统这个值一般是8KB==2000H)。节在磁盘中就没有最小4K的限制,为了减少磁盘文件的大小,文件对齐的单位一般要小于内存对齐的单位(FileAlignment的值一般为200h,一个扇区),这样,在磁盘中就不必为每个节最后的零头数据补足4KB的大小了。
③节的尺寸:
对节的尺寸的处理主要分为两个方面:
第一个方面,正如刚刚我们所讲的,由于磁盘映像和内存映像中节对齐存储单位的不同而导致了长度扩展不同(填充的0数量不同嘛~);
第二个方面,是对于包含未初始化数据的节的处理问题。既然是未初始化,那么没有必要为其在磁盘中浪费空间资源,但在内存中不同,因为程序一运行,之前未初始化的数据便有可能要被赋值初始化,那么就必须为他们留下空间。
④不进行映射的节:
有些节并不需要被映射到内存中,例如.reloc节,重定位数据对于文件的执行代码来说是透明的,无作用的,它只是提供Windows 装载器使用,执行代码根本不会去访问到它们,所以没有必要将他们映射到物理内存中。.
在硬盘中文件对齐。
在内存中 内存对齐
四、DOS头
DOS MZ头 64 字节
前两字节(PE文件表示)和后四字节(通过其找到PE文件头位置)可以必须要,其他可以都为0.
其余以0填充后任然可以运行。
DOS块,MZ头与PE文件头之间部分,是由连接器自动填写的,可随意修改。
全部以 0 填充,程序无影响。
五、PE头
5.1 PE标识
PE标识不能破坏,操作系统在启动一个程序的时候会检测这个标识。
5.2 标准PE头
小端存储,前两字节为 用 32 位 winhex打开 014c
用 64位 winhex 打开为 8664,即为 x64程序。
第二个属性值为 0070,代表有 7 个节表。
第三个属性值,4个字节为编译程序时产生的时间戳。修改五任何影响,这里的时间并不是我们修改导致程序显示的时间,
第六个属性值,扩展PE头,00f0 ,即为64位程序。
最后一个属性值,2字节,代表文件属性,
2200,拆分 0000 0000 0010 0010
对照下图,第一位和第五位有值,可执行,应用程序可处理大于 2GB的地址。
5.3 扩展PE头
32位和64位略有不同。
六、RVA和FOA的转换
RVA : 相对虚拟地址(Relative Virtual Address),PE 文件中的各种数据结构中涉及地址的字段大部分都是以 RVA 表示的。
VA 是当PE 文件被装载到内存中后,某个数据位置相对于文件头的偏移量。 即
RVA = 内存地址 - ImageBase
FOA : 文件偏移值
RVA 转换位 FOA:
-
判断 RVA 是否位于 PE 头,如果是 FOA=RVA,(在内存中和文件中PE头(DOS头+PE头+节表)是相同的,节数据不同,因为文件对其和内存大小可能不相等)
-
判断 RVA 位于哪个节
RVA >= 节.VirtualAddress
RVA <= 节.VirtualAddress + 当前内存对齐后的大小
差值 = RVA - 节.VirtualAddress
-
FOA = 节.PointerToRawData + 差值
七、空白区添加代码
- 构造要写入的代码
- 在PE的空白区构造一段代码
- 修改入口地址位新增代码
- 新增代码执行后,跳回入口地址
这个就是一个很简单的只有弹框的程序
#include <windows.h>
int main()
{
MessageBox(0,0,0,0);
return 0;
}
反汇编:
MessageBox(0,0,0,0);009E1778 8B F4mov esi,esp 009E177A 6A 00push0 009E177C 6A 00push0 009E177E 6A 00push0 009E1780 6A 00push0 009E1782 FF 15 98 B0 9E 00calldword ptr [__imp__MessageBoxW@16 (09EB098h)] 009E1788 3B F4cmp esi,esp 009E178A E8 A6 FA FF FF call__RTC_CheckEsp (09E1235h)
上边的要依赖导入表,所以改为下边的
机器码
6A 00 6A 00 6A 00 6A 00 E8 00 00 00 00
E8后边的对应的00 00 00 00
是算出来的
要跳转的地址 - 当前地址 - 5
在看下 jmp 指令,因为执行完添加的代码之后要调回去执行原来的代码,
E9 后边的跟计算 E8 的一样
机器码:
6A 00 6A 00 6A 00 6A 00 E8 00 00 00 00 E9 00 00 00 00
如果上面那段是保护程序的代码,那就是壳,如果破坏,就是病毒。
可以把上段代码插入到空白区,也就是 0 区域。
找到 messagebox 函数
ctrl+n,
记下地址 75 D2 19 30
现在构造的代码只能在自己机器上执行,先插入代码
计算
文件偏移地址转换为内存地址
要跳转的地址 - E8指令当前的地址 - 575D21930 - 4AD27D38 -5 = 2AFF9BF36A 00 6A 00 6A 00 6A 00 E8 F3 9B FF 2A E9 00 00 00 00
跳到程序入口处 4AD05046
4AD05046 - 4AD27D3E -5 = FFFDD3036A 00 6A 00 6A 00 6A 00 E8 F3 9B FF 2A E9 03 3D DF FF
我们还需要修改程序入口处,是入口处变为咱们添加的代码地址,然后执行完之后在跳回原本的程序入口处,
我们添加代码的地址 026930
找到程序原本入口00005046
,
修改入口点,之后保存,
测试,成功弹窗
八、扩大节
在空白区加入代码只限少数代码,如果要添加的代码很多,显然这种方法就不显示了。
所以我们可以适当的扩大一个节以便添加更多代码。
扩大最后一个节
步骤:
-
分配一块新的空间,大小为 S
-
将最后一个节的 SizeOfRawData 和 VirtualSize 改成 N
N = (SizeOfRawData 或者 VirtualSize 内存对齐后的值) + S
-
修改 SizeOfImage 的值
先看下本来的节查看
在文件最后添加 1000H 个字节,转成十进制就是 4096 个
利用 UltraEdit
,右键十六进制插入。
然后去修改节表属性信息,
最后修改镜像大小
原来是 00500700
改为 00600700
改完之后程序运行不影响,但扩大了节。
修改后的
九、新增节
如果添加代码时,空白区不够,我们可以无限的扩大最后一个节,我们还可以新增节,在新增节里放我们的数据。
新增节步骤:
- 判断是否有足够的空间,可以添加一个节表。
- 在节表中新增一个成员。
- 修改 PE 头中节的数量
- 修改 sizeOfImage 的大小
- 在原有数据的最后,新增一个节的数据(内存对其的整数倍)
- 修正新增节表的属性。
还可直接使用 PEtools 添加
十、导出表
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;// 一般为0
DWORD TimeDateStamp; // 导出表生成的时间戳,由连接器生成。
WORDMajorVersion; // 主版本号
WORDMinorVersion; // 副版本号
DWORD Name; // 模块的名字。
DWORD Base; // 序号的基数,按序号导出函数的序号值从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;
一个可执行文件是由多个 PE 文件组成的。
还有其他 DLL pe 文件。
通常 dll 既有导入表,也有导出表。Exe 通常只有 导入表。
exe程序中通常会使用动态链接库dll中的函数;
dll相当于一个独立的模块,dll中的代码并不会编译到exe程序中;
这就产生了一个问题:exe怎么知道dll中的代码在什么位置;
这就需要dll提供一个清单,这个清单中能清晰说明有多少个函数、它们的名字、地址;
导出表就是这样的一个清单;
作用:记录了导出符号的地址,名称,与序号
(提示:exe文件中很少有导出表的,大多数dll都有导出表,某些存放资源文件的dll就没有导出表)
导出表在扩展PE头的最后一个成员数组的第一个结构体, 8个数组,1个数组 8字节
十一、导入表
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;// 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
在编程中常常用到“导入函数”(Import functions),导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,在调用者程序中只保留一些函数信息,包括函数名及其驻留的DLL名等。
于磁盘上的PE 文件来说,它无法得知这些输入函数在内存中的地址,只有当PE 文件被装入内存后,Windows 加载器才将相关DLL 装入,并将调用输入函数的指令和函数实际所处的地址联系起来。这就是“动态链接”的概念。动态链接是通过PE 文件中定义的“导入表”来完成的,导入表中保存的正是函数名和其驻留的DLL 名等。
十二、重定位表
既然有VA这么简单的表示方式为什么还要有前面的RVA呢?因为虽然PE文件为自己指定加载的基地址,但是windows有茫茫多的DLL,而且每个软件也有自己的DLL,如果指定的地址已经被别的DLL占了怎么办?如果PE文件无法加载到预期的地址,那么系统会帮他重新选择一个合适的基地址将他加载到此处,这时原有的VA就全部失效了,NT头保存了PE文件加载所需的信息,在不知道PE会加载到哪个基地址之前,VA是无效的,所以在PE文件头中大部分是使用RVA来表示地址的,而在代码中是用VA表示全局变量和函数地址的。那又有人要问了,既然加载基址变了以后VA都失效了,那存在于代码中的那些VA怎么办呢?答案是:重定位。系统有自己的办法修正这些值,到后续重定位表的文章中会详细描述。既然有重定位,为什么NT头不能依靠重定位采用VA表示地址呢(十万个为什么)?因为不是所有的PE都有重定位,早期的EXE就是没有重定位的。
步骤:
1.编译的时候由编译器识别出哪些项使用了模块内的直接VA,比如push一个全局变量、函数地址,这些指令的操作数在模块加载的时候就需要被重定位。
2.链接器生成PE文件的时候将编译器识别的重定位的项纪录在一张表里,这张表就是重定位表,保存在DataDirectory中,序号是 IMAGE_DIRECTORY_ENTRY_BASERELOC。
3.PE文件加载时,PE 加载器分析重定位表,将其中每一项按照现在的模块基址进行重定位。
以上三步,前两部涉及到了编译和链接的知识,跟本文的关系不大,我们直接看第三步,这一步符合本系列的特征。
在查看重定位表的定义前,我们先了解一下他的存储方式,有助于后面的理解。按照常规思路,每个重定位项应该是一个DWORD,里面保存需要重定位的RVA,这样只需要简单操作便能找到需要重定位的项。然而,Windows并没有这样设计,原因是这样存放太占用空间了,试想一下,加入一个文件有n个重定位项,那么就需要占用4*n个字节。所以Windows采用了分组的方式,按照重定位项所在的页面分组,每组保存一个页面其实地址的RVA,页内的每项重定位项使用一个WORD保存重定位项在页内的偏移,这样就大大缩小了重定位表的大小。
有了上面的概念,我们现在可以来看一下基址重定位表的定义了:
typedef struct _IMAGE_BASE_RELOCATION {DWORD VirtualAddress;DWORD SizeOfBlock;// WORDTypeOffset[1];} IMAGE_BASE_RELOCATION;typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
VirtualAddress:页起始地址RVA。
SizeOfBlock:表示该分组保存了几项重定位项。
TypeOffset:这个域有两个含义,大家都知道,页内偏移用12位就可以表示,剩下的高4位用来表示重定位的类型。而事实上,Windows只用了一种类型IMAGE_REL_BASED_HIGHLOW 数值是 3。
此内容转载 http://blog.csdn.net/evileagle/article/details/12886949
参考文章:
https://www.bilibili.com/video/BV18r4y1K7sa
https://blog.csdn.net/adam001521/article/details/84658708
https://blog.csdn.net/evileagle/article/details/12886949
https://blog.csdn.net/chenlycly/article/details/53378196
此文章转载于: https://www.freebuf.com/articles/network/265889.html
- 本文作者: ym
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/354
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!