PE感染型病毒的研究与修复
仅为样本实例
修复PE文件结构¶
样本示例¶

修复DOS头¶
DOS头结构¶
DOS头 的作用是兼容 MS-DOS 操作系统中的可执行文件,对于 32位PE文件来说,DOS 所起的作用就是显示一行文字,提示用户:我需要在32位windows上才可以运行。我认为这是个善意的玩笑,因为他并不像显示的那样不能运行,其实已经运行了,只是在 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头之后的 NT头相对文件起始地址的偏移。

修复MZ标识和e_lfanew指向的NT头标识
修复NT头¶
NT头结构¶
顺着 DOS 头中的 e_lfanew,我们很容易可以找到 NT头,这个才是 32位PE文件中最有用的头,定义如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature¶
类似于 DOS头中的 e_magic,其高16位是0,低16是0x4550,用字符表示是 'PE‘ 。
- IMAGE_FILE_HEADER
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
每个域的具体含义如下:
Machine:该文件的运行平台,是 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_R10000 0x0168 // 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_SH3DSP 0x01a3
#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_MIPS16 0x0266 // 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
NumberOfSections: 该PE文件中有多少个节,也就是节表中的项数。
TimeDateStamp: PE文件的创建时间,一般有连接器填写。
PointerToSymbolTable: COFF文件符号表在文件中的偏移。
NumberOfSymbols: 符号表的数量。
SizeOfOptionalHeader: 紧随其后的可选头的大小。
Characteristics: 可执行文件的属性,可以是下面这些值按位相或。
#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_STRIPPED 0x0004 // 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_STRIPPED 0x0200 // 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_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 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 可选头,别看他名字叫可选头,其实一点都不能少,不过,它在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。为了简单起见,我们只看32位。
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
PE 可选头字段¶
- Magic:表示可选头的类型。
#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
- MajorLinkerVersion 和 MinorLinkerVersion:链接器的版本号。
- SizeOfCode:代码段的长度,如果有多个代码段,则是代码段长度的总和。
- SizeOfInitializedData:初始化的数据长度。
- SizeOfUninitializedData:未初始化的数据长度。
- AddressOfEntryPoint:程序入口的 RVA,对于exe这个地址可以理解为WinMain的RVA。对于DLL,这个地址可以理解为DllMain的RVA,如果是驱动程序,可以理解为DriverEntry的RVA。当然,实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成,当然,这些不是本文的重点。
- BaseOfCode:代码段起始地址的RVA。
- BaseOfData:数据段起始地址的RVA。
- ImageBase:映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。
- SectionAlignment:节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。
- FileAlignment:节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。
- MajorOperatingSystemVersion、MinorOperatingSystemVersion:所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。
- MajorImageVersion、MinorImageVersion:映象的版本号,这个是开发者自己指定的,由连接器填写。
- MajorSubsystemVersion、MinorSubsystemVersion:所需子系统版本号。
- Win32VersionValue:保留,必须为0。
- SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。
- SizeOfHeaders:所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。
- CheckSum:映象文件的校验和。
-
Subsystem:运行该PE文件所需的子系统,可以是下面定义中的某一个:
-
SizeOfStackReserve:运行时为每个线程栈保留内存的大小。
- SizeOfStackCommit:运行时每个线程栈初始占用内存大小。
- SizeOfHeapReserve:运行时为进程堆保留内存大小。
- SizeOfHeapCommit:运行时进程堆初始占用内存大小。
- LoaderFlags:保留,必须为0。
- NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数。
- DataDirectory:数据目录,这是一个数组,数组的项定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
- VirtualAddress:是一个RVA。
- Size:是一个大小。
这两个数有什么用呢 ?一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域。**那他定义的是什么东西的区域呢?**前面说了,DataDirectory 是个数组,数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // 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_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
修复NumberOfSections¶

可以看到是3个节

修改NumberOfSections为正确的节数
修复SizeOfOptionalHeader¶
32位的PE文件默认值为0xE0,64位PE文件默认值为0xF0 ,该值可变,由于该文件为32位,所以修改为0xE0

修复AddressOfEntryPoint¶

修改AddressOfEntryPoint为0x1000
修复ImageBase¶

修改ImageBase为0x00400000
修复SectionAlinment¶

修复FileAlignment¶

修复FileAlignment为0x00000200
示例运行¶

PE文件病毒¶
节表结构¶
PE节表位于PE头的下面,PE节表中记录了各个节表的起始位置、大小,以及在内存中偏移位置和属性。
其结构体如下:
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//*[1]:节名
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //*[2]:对其前节大小,内存中节大小
} Misc;//联合体
DWORD VirtualAddress; //*[3]:内存中偏移
DWORD SizeOfRawData; //*[4]:文件对其后节大小
DWORD PointerToRawData; //*[5]:文件中节位置
DWORD PointerToRelocations;//
DWORD PointerToLinenumbers;//
WORD NumberOfRelocations; //
WORD NumberOfLinenumbers; //
DWORD Characteristics; //*[6]:节属性的标志
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

节表数量¶
节表数量不在节表中,而是在上一篇文章中已经介绍过,在标准PE头中。标准PE头中有一个成员是NumberOfSections他是节表的数量。

节表名字¶
节表名字是各个节表的第一个成员,首先我们根据PE头大小来确定节表所在位置即DOS头+垃圾数据+NT头,下面在010Editor演示具体数据。
节表中第一个数据是节名,节名一共占用的数据大小是8个字节, 演示中第一个节名是.text。

节表大小¶
节表中真实的大小在VirtualAddress中,也就是节名后的4个字节。他所代表的意思是对其前的大小也是在内存中的大小。
而在他后4四节之后的4字节数据,代表的是对其后的大小也是文件中的大小SizeOfRawData。

节位置¶
节位置都是在节大小之后,占用大小也是为4字节。VirualAddress后的4字节代表的是内存中节偏移,SizeOfRawData后的4字节代表的是文件中节位置。

在内存中锁定具体的节位置需要内存地址+节偏移得到的地址才是真实的内存节所在的位置,而在文件中的节位置则可以根据地址直接索引到,不需要+文件地址因为文件地址开始就是0。

内存地址在可选PE头的ImageBase中,所以我们需要先获取到ImageBase的值这里是0x00400000,所以加上偏移位置就是0x00401000。


一般程序在没有被修改的情况下,默认.text段就是程序用来放代码的地方。所以我们也可以用OD直接载入程序,然后跳到.text段的位置可以看到程序的汇编代码。
(小知识:OD载入程序就是模拟了程序拉伸的过程,程序在内存中的样子。)

节表属性¶
节表中的最后一个数据也是最重要的,他代表了这个节是否可读、可写、可执行或者是否包含可执行代码、初始化、未初始化的数据。所以一般我们判断一个段是否是代码段就是根据这个属性的值来判断的,因为节表名是可以改的,比如我把随便一个节表名改成.text、.code。那你就觉得他是代码段了吗?
代码段的属性一般是0x60000020,其中这4字节的数据,他每一位都对应下面表格中的数据。下面我们把0x60000020来拆分一下,首先最后2位对应下面的包含可执行代码的数据,然后最高位的6对应下面的(该块可执行)+(该块可读)的值。所以代码段的属性就是:(包括可执行代码)、(可执行)、(可读)。
--> 标志(属性块) 常用特征值对照表:<--
[值:00000020h](*包含可执行代码)
[值:00000040h](*该块包含已初始化的数据)
[值:00000080h](*该块包含未初始化的数据)
[值:00000200h][Section contains comments or some other type of information.]
[值:00000800h][Section contents will not become part of image.]
[值:00001000h][Section contents comdat.]
[值:00004000h][Reset speculative exceptions handling bits in the TLB entries for this section.]
[值:00008000h][Section content can be accessed relative to GP.]
[值:00500000h][Default alignment if no others are specified.]
[值:01000000h][Section contains extended relocations.]
[值:02000000h][Section can be discarded.]
[值:04000000h][Section is not cachable.]
[值:08000000h][Section is not pageable.]
[值:10000000h](*该块为共享块).
[值:20000000h](*该块可执行)
[值:40000000h](*该块可读)
[值:80000000h](*该块可写)
采用方法:在原有代码的空隙中注入代码

- 节一:可执行 可读 包含代码 (代码节)
- 节二:可读 包含未初始化的数据
- 节三:可执行 可读 可写 包含未初始化的数据
找FOA¶

FOA位置为0x00000400

分析代码¶
模拟输出hello world的程序
查看反汇编¶

分析执行逻辑¶

6A 00
68 00304000 (00403000, RVA=3000, FOA=800)
68 0F304000 (0040300F, RVA=300F, FOA=80F)
6A 00
字符串位置为0x800到0x80F

编写注入代码¶
写入字符串(参数)(在FOA=900处写入)

'病毒实验2' hexcode:E79785E6AF92E5AE9EE9AA8C32
'杨一帆 2022122117' hexcode:E69DA8E4B880E5B8862032303232313232313137

写入函数(在FOA=500处写入)
6A 00
68 00314000 (字符串地址00403100, 位于节3(FOA/RVA)=(800-1000/3000-4000), RVA=3100, FOA=900, 偏移x=100)
68 10314000 (字符串地址00403110, 位于节3(800-1000/3000-4000), RVA=3110 FOA=910, 偏移x=110)
6A 00
# comment
(此条指令位置:
位于节1(400-600/1000-2000), FOA=512, RVA=1112, 偏移x=112)
(jmp指令位置0040101A:
位于节1(400-600/1000-2000), FOA=41A, RVA=101A, 偏移x=1A)
两者FOA相差512 - 41A + 1=F9,所以下一条指令为
Call - F9
E8 -F9000000
E8 FFFFFF07
E8 07FFFFFF (FFFFFF07=-000000F9) (往回跳转调用函数) E9 E8FEFFFF (jmp 00401000跳转回原入口)
将6A 00 68 00 31 40 00 68 10 31 40 00 6A 00 E8 07 FF FF FF E9 E8 FE FF FF写入到0x500处

更改程序入口¶
- 原代码:位于节1(400-600/1000-2000), FOA=400, RVA=1000, 偏移x=0
- 自己的代码:位于节1(400-600/1000-2000), FOA=500, RVA=1100, 偏移x=100
将AddressOfEntryPoint(程序入口点)从1000改成1100。

示例输出¶

打造最小的PE文件¶

是否PE文件?¶
是PE文件,因为如下图所示,文件由”4D5A”开头,0x3C位置DWORD字段(0x00000030)指向的位置的字段为”50450000”。

有多少个节?¶
没有节,因为如下图所示,PE_HEADER(PE文件头)偏移0x02位置的WORD字段(NumberOfSections, 节的数目)为0。
另外,PE_HEADER(PE文件头)偏移0x10位置的WORD字段(SizeOfOptionalHeader, 可选映像头的大小)为0x0070。

EOP位置¶
该文件程序入口点0x00000088
如下图所示,OPTIONAL_PE_HEADER(可选映像头)偏移0x10位置的DWORD字段(AddressOfEntryPoint, 程序入口点)为0x00000088。

内存对齐和文件对齐¶
内存对齐粒度0x00000004,文件对齐粒度0x00000004
如下图所示,OPTIONAL_PE_HEADER(可选映像头)偏移0x1c位置的DWORD字段(ImageBase, 内存镜像基址)为0x00400000。
OPTIONAL_PE_HEADER(可选映像头)偏移0x20位置的DWORD字段(SectionAlignment, 内存对齐)为0x00000004。
OPTIONAL_PE_HEADER(可选映像头)偏移0x24位置的DWORD字段(FillAlignment, 文件对齐)为0x00000004。

映像总尺寸和头部大小¶
该文件装入内存后映像总尺寸?0x0000020D头部有多大?0x000000B8
如下图所示,OPTIONAL_PE_HEADER(可选映像头)偏移0x38位置的DWORD字段(SizeOfImage, 文件装入内存后映像总尺寸)为0x0000020D。
OPTIONAL_PE_HEADER(可选映像头)偏移0x3c位置的DWORD字段(SizeOfHeaders, 头部大小)为0x000000B8。

引入函数表和IAT表¶
OPTIONAL_PE_HEADER(可选映像头)偏移0x60位置开始的字段(DataDirectory[16])是一个16个元素数组,每个元素为一个结构体(_IMAGE_DATA_DIRECTORY),每个结构体包含2的DWORD字段:VirtualAddress和Size。这个PE文件只有2个元素。
第0个数组元素DataDirectory[0]为导出表
第1个数组元素DataDirectory[1]为导入表,VirtualAddress(第一张导入表的地址)为0x000000FC

*0x000000FC*指向的第一张导入表偏移0x0c位置的DWORD字段(Name, [地址:指向模块名称字符串])为0x0000004A
偏移0x10位置的PIMAGE_THUNK_DATA(大小为4字节)字段(FirstThunk[地址:指向IAT表])为0x000000B4
- 第一张导入表:0x0000004A
- IAT表:0x000000B4

位于第一张导入表之后的第二张导入表偏移0x0c位置的DWORD字段(Name, [地址:指向模块名称字符串])为0x00000110 偏移0x10位置的PIMAGE_THUNK_DATA(大小为4字节)字段(FirstThunk[地址:指向IAT表])为0x000000EC

从哪些DLL引入了哪些API函数?¶
*0x0000004A*指向的字符串为”kernel32.dll”(以0x00结尾),说明第一张导入表的模块名称为kernel32.dll

*0x000000B4*指向一组地址,分别为”0x000000DE” (以0x00000000结尾),每个地址代表一个函数。
*0x000000DE*指向位置的前两个字节是序号0x0000,之后是一个字符串”ExitProcess” (以0x00结尾),说明第一张导入表的第一个函数的序号为0x0000,名称为ExitProcess。

*0x00000110*指向的字符串为”user32.dll”(以0x00结尾),说明第二张导入表的模块名称为user32.dll

*0x000000EC*指向一组地址,分别为”0x000000BA” (以0x00000000结尾),每个地址代表一个函数。
*0x000000BA*指向位置的前两个字节是序号0x0000,之后是一个字符串”MessageBoxA” (以0x00结尾),说明第二张导入表的第一个函数的序号为0x0000,名称为MessageBoxA。

所以,由上述分析可知,该PE文件从kernel32.dll引入了ExitProcess,从user32.dll引入了MessageBoxA
测试是否运行¶

在64位PC上貌似无法运行
在功能不变的情况下,使程序最小¶
将最后一个用不到的函数删除,然后将文件尾部的0全部删除,就可以得到最小的PE文件

PE和DLL的区别和共同点¶
DLL文件结构¶
动态链接库(DLL)文件是一种特殊的可执行文件,它的格式与普通的EXE文件非常相似。DLL文件通常包含了程序运行时所需的代码和资源,但并不具备执行的入口点。它们被设计为由其他程序在运行时动态加载。
DLL文件结构主要包括以下几个部分:
- DOS头 :这是所有Windows可执行文件共有的部分,包含了文件的魔数(MZ标识)、DOS可执行代码和一个指向PE头的偏移量。虽然对于现代系统来说这部分已经没有实际的执行意义,但系统仍会检查它是否存在,以确定文件是否为可执行文件。
- PE头(NT头) :这是PE(Portable Executable)格式的核心,包含了文件的元数据,如文件签名、机器架构、时间戳、符号表入口等重要信息。PE头是DLL文件的核心部分,它定义了文件的结构和如何被加载到内存中。
- 节表 :节表定义了文件中的各个数据段,如代码段(.text)、数据段(.data)、资源段(.rsrc)等。每个节都有自己的属性,如是否可读、可写、可执行等,这些属性决定了如何在内存中映射这些段。
- 数据段 :包含了实际的代码、数据和其他资源。数据段通常被分为多个节,每个节都有特定的用途。
文件结构不同点和相同点¶
相同点:
- 两者都是Windows可执行文件格式,采用PE文件格式。
- 两者都包含有Image DOS Header、Image NT Header、Section Table等头部信息。
- 两者都可能包含有多个sections,每个section包含有可执行代码、数据等信息。
- 两者都支持导出函数和导入函数。

可以看到都有DOS头、NT头,节表,节、导入导出表等结构
不同点:
- EXE文件是应用程序文件,而DLL文件是共享库文件。EXE文件是独立运行的程序,而DLL文件是被其他程序调用的库。
- EXE文件一般只包含一个入口点,即程序的入口函数,而DLL文件可以包含多个入口点,即多个导出函数。
- EXE文件在编译时会链接所有的库函数,而DLL文件则是在运行时动态链接库函数。
- EXE文件可以被其他程序调用,但不能调用其他程序,而DLL文件既可以被其他程序调用,也可以调用其他程序的函数。
- EXE文件的代码和数据是被加载到内存中的,而DLL文件的代码和数据是在需要时才被加载到内存中。
- EXE文件不能导入其他的DLL函数,只能导入系统DLL函数,而DLL文件可以导入其他DLL文件的函数。
功能不同点和相同点¶
相同点:
- 都是PE文件格式,有相似的文件头和数据结构;
- 都包含可执行的代码和数据;
- 都可以被链接到其他程序或模块中。
不同点:
- EXE文件是可执行文件,而DLL文件是动态链接库;
- EXE文件一般独立运行,而DLL文件一般被其他程序调用;
- EXE文件中代码和数据一般是完全独立的,而DLL文件中的代码和数据可以被多个程序共享;
- EXE文件一般包含主程序的入口点代码,而DLL文件一般包含导出函数供其他程序调用;
- EXE文件的加载速度比DLL文件慢,因为需要加载全部代码和数据,而DLL文件只需要加载部分代码和数据。