导语

如果你想学习PE病毒的编写,那么你首先得知道PE的格式和他的工作原理。

PE的百度百科

PE(Protable Executable),即可移植的执行体。在Windows操作系统平台下,所有的可执行文件如EXE文件、DLL文件、SYS文件、OCX文件、COM文件等均使用PE结构。

PE文件的结构

一张简简单单的图送给各位

还有一个比较详细的pdf送给各位
链接:https://pan.baidu.com/s/1SGU2AUQvzoHU9Foxx1-UYQ 提取码:ckdi

MS-DOS(DOS头)=MZ文件头+DOS Stub

DOS头的作用是定位文件PE头的开始位置,也可用于PE文件的合法性检查。而在DOS下运行时则会提示用户本文件不能在DOS下运行。在C语言的Windows.h中有预定义的数据结构IMAGE_DOS_HEADER如下

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_ress2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

MZ文件头

MZ文件头是真正意义的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;

DOS Stub

DOS Stub主要是为了兼容DOS操作系统,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode。

PE header

PEheader是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,其中包含许多PE装载器用到的重要字段。
同样的我们看看C语言中对IMAGE_NT_HEADER的预定义(我们先只研究32位下的咯)

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#endif

提示字串(signature)

这儿就是一个DWORD,内容是50h,45h,00h,00h,也就是ascii的PE空格空格

###映像文件头(FileHeader)
包含了PE文件物理分布信息,比如节数目、后续可选文件头大小、及其类型等等
定义如下

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;

###可选文件头(OptionalHeader)
定义了PE文件许多的关键信息,入内存镜像加载地址、程序入口点、节在文件和内存中的对齐粒度、程序在内存中的镜像大小、文件大小等等。定义如下 <span id="jump">

typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;

//
// NT additional fields.
//

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;

名词解释

ImageBase:PE文件在内存中安装在地址
RVA地址:Relative Virtual Address,相虚拟地址,他是相对于ImageBase的偏移位置。
对齐粒度:文件中节的对齐粒度就是文件一个节的长度,内存中节的对齐粒度就是内存中一个节的长度。
在磁盘中就按照文件的对齐粒度IMAGE_OPTIONAL_HEADER.FileAlignment对齐,在装载到内存中去运行的时候就按照内存中的节的对齐粒度IMAGE_OPTIONAL_HEADER.SectionAlignment对齐。
常见情况下内存的对齐粒度是0x1000,而文件的对齐粒度是0x200,这样做是为了与磁盘的扇区大小和内存单元的大小对齐。

可选文件头中比较重要的关键字

如图

输入表

输入表是记录PE输入函数相关信息的一张表,他记录了PE文件在运行过程中调用动态链接库的一些函数的名称和地址。输入表分为输入名称表(INT)和输入地址表(IAT)。

之前的IMAGE_OPTIONAL_HEADER那里可以看到有一个IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]的内容,他的数据结构如下,就是简单地偏移量和大小的数据分块,一共分了16块,每一块都有对应的内容。

typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

IMAGE_NUMBEROF_DIRECTORY_ENTRIES为2的data directory内装的是一个叫做IMAGE_IMPORT_DESCRIPTOR的数据结构(IID),

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;

Name:输入的DLL的名字指针,它是一个以00结尾的ASCII字符的RVA地址,该字符串包含输入的DLL名。例如:KERNEL32.DLL,或者USER32.DLL。

OriginalFirstThunk指向第一个unbound的INT(import name table,导入名称表),INT指向IMAGE_THUNK_DATA结构
FirstThunk:包含指向输入地址表(IAT)的RVA。IAT也是指向IMAGE_THUNK_DATA结构,他的格式如下。

typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

ForwarderString 指向一个转向者字符串的RVA;
Function 被输入的函数的内存地址
Ordinal 被输入的API的序数值
AddressOfData又指向一个IMAGE_IMPORT_BY_NAME的数据结构

typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Hint字段:指示本函数在其所驻留的输出表的中序号该域被PE装载器用来在DLL的输出表里快速查询。该值不是必须的,一些链接器将此值设为0;
NAME字段:这个字段比较重要。它含有输入函数的函数名,函数名是一个ASCII码字符串,并以NULL结尾。注意,这里虽然将NAME的大小定义为字节,其实他是可变的。

在之前的大图也可以看出来,通过IMAGE_DIRECTORY_ENTRY_IMPORT->多个 _IMAGE_IMPRT_DESCRIPTOR->每个指向一个IMAGE_THUNK_DATA->每个指向一个union域

问题来了,OriginalFirstThunk和FirstThunk都指向IMAGE_THUNK_DATA,这是为什么呢?

我们查看IMAGE_THUNK_DATA的数据结构可以发现,union可以取

  • DWORD ForwarderString; // PBYTE
  • DWORD Function; // PDWORD
  • DWORD Ordinal;
  • DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME

在OriginalFirstThunk所指向的IMAGE_THUNK_DATA使用了AddressOfData域,用来指向IMAGE_IMPORT_BY_NAME。他的IMAGE_THUNK_DATA的数组被称作INT(import name table,导入名称表)

而FirstThunk所指向的IMAGE_THUNK_DATA使用了Function域,用来指向函数的实际地址。他的IMAGE_THUNK_DATA的数组被称作IAT表(import address table,导入地址表)

找到这张表有两种方式,一种就是通过导入表这里找到,第二种就是通过数据目录表,倒数第三个项就是指向的 IAT 表,IMAGE_DIRECTORY_ENTRY_IAT,也是以0结束。在文件加载前,这两个表存的内容完全一致,都是存储的IMAGE_THUNK_DATA

1679890197830

在加载前,不论是INT还是IAT的IMAGE_THUNK_DATA结构都只存一个 RVA,这个 RVA 指向上图的IMAGE_IMPORT_BY_NAME 结构

在加载后,IAT 表变为存储函数的地址了,在调用导入函数的时候,就可以call这个地址了。

节表

顾名思义,是存放节表项的,相当于节的目录。
节表项包含 节名、节在文件和内存中的开始地址、长度、节属性等。

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; // 在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)
WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers; // 行号表中行号的数目
DWORD Characteristics; // 节属性如可读,可写,可执行
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

可能大家会有疑问,virtualaddress和pointertorawdata的区别是什么,其实很简单,VirtualAddress是指内存中的rva(相对虚地址),PointerToRawData是指文件中的偏移

节属性

0x00000020? 这个块包含代码。置位
0x00000040? 这个块包含已初始化的数据
0x00000080? 这个块包含未初始化的数据(如.bss 块)
0x00000200? 这个块包含注释或其它的信息。
0x00000800? 这个块的内容不应放进最终的EXE文件中。
0x02000000? 这个块可以被丢弃,因为一旦它被载入,其进程就不需要它。最通常的可丢弃块是基本重定位块(.reloc )。
0x10000000? 这个块是可共享的。
0x20000000? 这个块是可执行的。
0x40000000? 这个块是可读的。
0x80000000? 这个块是可写的。

节就是程序主体了,一般PE文件有很多节,比较常见的有
代码节
数据节
引入函数节
资源节等(如图标)
引出函数节(DLL文件中常见)
重定位节(DLL文件中常见)

PE程序执行流程

PE程序主要依赖PE程序装载器,在检查了头部信息(包括引入函数表的地址信息装入等等)之后会读取节表,根据节表来动态的将各节的内容装入内存中。这个过程中会自动的修改代码段中的jmp的地址值使它能够成功。
这点儿知识对下个PE病毒编写有好处,尽请期待。

参考:

https://blog.csdn.net/qq_39805477/article/details/121236663
https://blog.csdn.net/qq_40422314/article/details/89741040
https://www.cnblogs.com/2f28/p/9800992.html