文章总结: 本文档为免杀技术基础教程,系统讲解WindowsAPI数据类型(DWORD/SIZE_T/HANDLE等)、ANSI与Unicode函数区别、错误调试方法,深入解析PE文件格式结构包括DOS头、NT头、文件头、可选头及节区头的字段含义与代码解析方法,并介绍数据目录与导出表的基础概念,为后续免杀开发奠定底层知识基础。
综合评分: 75
文章分类: 免杀,二进制安全,安全开发,逆向分析,安全培训
2.免杀基础
原创
kernel
kernel
Relay学安全
2026年2月9日 12:07
陕西
Windows API
Windows API为开发人员提供了一种让其应用程序与Windows操作系统交互的方式。例如如果应用程序需要在屏幕上显示内容,修改文件或查询注册表,所有这些操作都可以通过Windows API完成。
Windows数据类型
DWORD
DWORD数据类型: 一个32位无符号整数,在32位和64位系统上都是如此。用于表示0到(2³² – 1)的值。这意味着最大值可以表示: 4294967295。但是要是超过这个值,就不正常了。
SIZE_T
SIZE_T数据类型: 用于表示对象的大小。在32位系统上,他是32位无符号整数,表示0 到 (2³² – 1) 的值。另一方面,在64位系统上,他是64位无符号整数,表示从 0 到 (2⁶⁴ – 1) 的值。
#include <Windows.h>#include <stdio.h>int main() {//DWORD dwsize = 4294967295; SIZE_T dwsize1 = sizeof(int); SIZE_T dwsize2 = sizeof(float); SIZE_T dwsize3 = sizeof(DWORD);}
int类型占用 4 个字节float类型占用 4 个字节DWORD类型(本质上是unsigned long)也占用 4 个字节
所以最终的值全都是4。
VOID
VOID表示没有特定数据类型。
#include <Windows.h>#include <stdio.h>int main() {void* pVariable = NULL; // 这与 PVOID 相同}
PVOID
PVOID在32位系统行,是指向任何数据类型的32位或4字节指针,在64位系统上,是指向任何数据类型的64位或8字节指针。
#include <Windows.h>#include <stdio.h>int main() {int a = 10; PVOID inta = &a;}
HANDLE
HANDLE类型是指定操作系统正在管理的特定对象,比如文件,进程,线程的值。
HANDLE hFile = CreateFile(...);
HMODULE
HMODULE类型可以表示一个模块的句柄,这是模块在内存中的基地址,这里的模块指的是EXE或DLL文件。
#include <Windows.h>#include <stdio.h>int main() { HMODULE hmodule = GetModuleHandleA("kernel32.dll");}
LPCSTR/PCSTR
LPCSTR/PCSTR指向8位Windows字符的常量,以空字符结尾的字符串的指针。“L”代表Long。源于16位Windows编程期间。如今它不影响数据类型,但命名约定仍然存在。”C” 代表 “constant” 或只读变量。这两种数据类型都等价于 const char*。
LPCSTR lpcstring = "Hello, world!";PCSTR pcstring = "Hello, world!";
wchar_t
wchar_t与wchar相同,用于表示宽字符。
wchar_t wChar = L'A';wchar_t *wstring = L"Hello, world!";
数据类型指针
Windows API允许开发者直接声明一个数据类型,或声明一个指向该数据类型的指针。
以“P”开头的数据类型代表指向实际数据类型的指针。而不以 “P” 开头的则代表实际数据类型本身。
如下例子:
PHANDLE等同于HANDLE*。
PSIZE_T等同于SIZE_T*。
PDWORD等同于DWORD*。
ANSI和Unicode函数
大多数Windows API函数都有两个版本,分别是以 “A” 结尾的版本和 以 “W”结尾的版本。例如有CreateFileA和CreateFileW.
以 “A” 结尾的的函数表示ANSI。而以 “W” 结尾的函数表示Unicode或宽字符。需要记住的主要区别是,在适用的情况下,ANSI函数接受ANSI数据类型作为参数。而 Unicode 函数接受 Unicode 数据类型。
#include <windows.h>#include <stdio.h>int main() { HANDLE hFile;
// ANSI 版本 hFile = CreateFileA( "maldev_ansi.txt", // LPCSTR - 11字节 GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if (hFile != INVALID_HANDLE_VALUE) { printf("ANSI 文件创建成功!\n"); CloseHandle(hFile); }
// Unicode 版本 hFile = CreateFileW( L"maldev_unicode.txt", // LPCWSTR - 22字节 GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if (hFile != INVALID_HANDLE_VALUE) { printf("Unicode 文件创建成功!\n"); CloseHandle(hFile); }}
输入和输出参数
Windows API具有输入和输出参数,IN参数是传递给函数的参数,用于输入。而OUT参数用于将值返回给函数的调用者。输出参数通常通过指针以引用传递。
#include <windows.h>#include <stdio.h>BOOL test(OUT int* num) { *num = 234;return TRUE;}int main() {int a = 10;test(&a);}
需要注意的是OUT或IN关键字是为了让开发者更容易理解函数以及如何处理他们的参数。省略这些关键字并不会参数被视为输出参数还是输入参数。
现在这里我们拿CreateFileW函数来做一个例子:
首先如果我们不知道该函数是做什么的,我们可以查阅官方文档。
https://learn.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-createfilew
从官方文档得知,该函数是用于创建或打开文件。
接下来查看它的语法:
首先该函数的返回值类型是HANDLE数据类型。HANDLE类型是指定操作系统正在管理的特定对象,比如文件,进程,线程的值。
此外,该函数的参数都是输入参数。这意味着该函数不会通过参数来返回任何数据,因为他们都是输入参数。需要注意的是方括号内的关键字,比如in``out``optional。纯粹是供开发者参考的,没有任何实际影响。
#include <windows.h>#include <stdio.h>int main() {//需要此变量来存储文件对象的句柄//INVALID_HANDLE_VALUE 仅用于初始化变量 HANDLE hFile = INVALID_HANDLE_VALUE;
//要创建文件的完整路径。//在C语言中需要使用双反斜杠来转义单个反斜杠字符。 LPCWSTR filePath = L"C:\\Users\\29273\\Desktop\\1.txt";
hFile = CreateFileW( filePath, GENERIC_ALL,0,NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,NULL );if (hFile == INVALID_HANDLE_VALUE) {printf("文件创建失败!!!"); }}
错误调试
当函数调用失败时,他们通常会返回一个不详细的错误。比如说CreateFileW失败,他会返回INVALID_HANDLE_VALUE。这表明文件无法创建。
为了更深入的了解文件无法创建的原因,就需要使用GetLastError函数来检索错误代码。
一旦获取了错误代码,就需要在 Windows 的系统错误代码列表中查找它。下面翻译了一些常见的错误代码:
5–ERROR_ACCESS_DENIED(拒绝访问)2–ERROR_FILE_NOT_FOUND(文件未找到)87–ERROR_INVALID_PARAMETER(参数无效)
比如这里的代码,我想要在system32目录下建一个文件,这里肯定是不行的,肯定会报错,所以这里使用GetLastError函数来捕捉。
如下图得知,错误代码为5。
在官方文档中查看该错误值:
https://learn.microsoft.com/zh-cn/windows/win32/debug/system-error-codes--0-499-
可以看到5错误值是访问被拒绝的错误。
Windows Native API调试错误
NTAPI大多都是从Ntdll.dll模块中导出的。与Windows API不同,这些函数的错误代码不能直接通过GetLastError来进行捕捉到。相反他们直接返回错误代码。该代码由NTSTATUS数据类型表示。
NTSTATUS 用于表示系统调用或函数的状态,它被定义为一个 32 位无符号整数值。成功的系统调用将返回值 STATUS_SUCCESS,即 0。另一方面,如果调用失败,它将返回一个非零值。要进一步调查问题的原因,必须查阅微软关于 NTSTATUS 值的文档。
NTSTATUS Status = NativeSyscallExample(...);if (Status != STATUS_SUCCESS) { // 以无符号整数十六进制格式打印错误 printf("[!] NativeSyscallExample Failed With Status: 0x%08X\n", Status);}// NativeSyscallExample 成功
另外一种检查NTAPI返回值的方法是使用这里展示的NT_SUCCESS宏。如果函数成功,该宏返回 TRUE,如果失败则返回 FALSE。
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
NTSTATUS Status = NativeSyscallExample(...);if (!NT_SUCCESS(Status)) { // 以无符号整数十六进制格式打印错误 printf("[!] NativeSyscallExample Failed With Status: 0x%08X\n", Status);}// NativeSyscallExample 成功
PE文件格式
PE文件格式是Windows中可执行文件的标准格式。常见的PE文件,比如 .exe``.dll``.sys``.src等等。
PE文件结构
如下图中的每个头部都被定义为一个数据结构,用于存储PE文件的相关信息。
DOS头(IMAGE_DOS_HEADER)
PE文件的第一个头部都是以两个字节: 0x4D``0x5A作为前缀。通常被称之为MZ签名。这些字节构成了DOS头签名,用于确定正在解析或检查的文件是否是有效的PE文件。DOS头的数据结构定义如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE 头部 WORD e_magic; // 魔数(签名) WORD e_cblp; // 文件最后一页的字节数 WORD e_cp; // 文件中的页数 WORD e_crlc; // 重定位项数 WORD e_cparhdr; // 头部大小(以段落计) WORD e_minalloc; // 所需最小额外段落数 WORD e_maxalloc; // 所需最大额外段落数 WORD e_ss; // 初始SS值(相对) WORD e_sp; // 初始SP值 WORD e_csum; // 校验和 WORD e_ip; // 初始IP值 WORD e_cs; // 初始CS值(相对) WORD e_lfarlc; // 重定位表文件地址 WORD e_ovno; // 覆盖号 WORD e_res[4]; // 保留字 WORD e_oemid; // OEM标识符(用于e_oeminfo) WORD e_oeminfo; // OEM信息(e_oemid特定) WORD e_res2[10]; // 保留字 LONG e_lfanew; // NT头部偏移量} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
其中结构中最重要的成员是: e_magic和e_lfanew。
e_magic的长度为2个字节,固定值为: 0x5A4D。对应的ASCII字符为 "MZ"。
e_lfanew的长度为4个字节,它存储着一个偏移量,该偏移量指向文件头中的NT头的起始位置。
需要注意的是e_lfanew成员在IMAGE_DOS_HEADER结构体中的位置是固定的,始终是在0x3C的位置。
这里可以使用PE-bear工具查看,后续都会使用该工具。
比如这里查看e_magic成员的值为0x5A4D。
查看DOS头中的最后一个成员为: 因为是固定的,所以这里将3C转换为十进制为: 60。
那么找到第60个字节即可。获取到偏移值为: 00000080,需要注意的是该值是16进制的值。
所以将00000080值转换为十进制为128。
所以这里从最顶部数128个字节就是NT头开始的地方。
那么这里通过代码来解析DOS头:
#include <windows.h>#include <stdio.h>
int main() {
HANDLE hFile;char* filepath = L"C:\\Users\\29273\\Desktop\\Payload\\x86 2.59MB 数字签名 Xshell.exe\\Xshell.exe"; hFile = CreateFileW( filepath, GENERIC_ALL,0,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL ); DWORD filesize = GetFileSize(hFile, NULL);char* buffer = (char*)malloc(filesize + 1); DWORD bytesRead; BOOL success = ReadFile( hFile, // 文件句柄 buffer, // 存储数据的缓冲区 filesize, // 要读取的字节数 &bytesRead, // 实际读取的字节数NULL // 重叠I/O结构(同步操作设为NULL) );if (success) {//成功读取文件之后 解析DOS头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { //IMAGE_DOS_SIGNATURE相当于0x5A4Dprintf("不是一个有效的PE文件");CloseHandle(hFile); }printf("e_lfanew=%d",pDosHeader->e_lfanew);
}}
NT头(IMAGE_NT_HEADERS)
NT头是PE文件的核心结构,它包含了另外两个重要的影像头,分别是文件头和可选头,这两个头部存储了大量的PE文件的信息。
与DOS头类似,NT头也包含了一个用于验证的签名成员,通常,该签名等于 "PE"字符串,对应的字节为0x50和0x45。
由于签名的数据类型是DWORD,所以是4个字节。因为签名表示为: 0x50450000。
NT头结构根据机器架构的不同而分为两个版本:
32位版本:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; // "PE"签名 (0x50450000) IMAGE_FILE_HEADER FileHeader; // 文件头 IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 32位可选头} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
64位版本:
typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; // "PE"签名 (0x50450000) IMAGE_FILE_HEADER FileHeader; // 文件头 IMAGE_OPTIONAL_HEADER64 OptionalHeader; // 64位可选头} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
文件头(IMAGE_FILE_HEADER)
文件头是NT头签名之后的重要头部结构。
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;
NumberOfSections表示节区的数量,节区是存储实际代码,数据,资源等内容的区域。该值直接影响到后续节区头表的解析。
Characteristics表示文件特性,这就是一个标志位,标志该可执行文件的属性,比如标识为是一个可执行文件,还是DLL动态链接库。
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header
例如这里的值为:2和100。
2表示的是该文件是可执行的(没有未解析的外部引用)。
100表示该PE文件是为32位机器架构设计的可执行文件。
SizeOfOptionalHeader表示可选头的大小。该值对于定位节区头表来说是很重要的。32位PE文件通常为0xE0 64位通常为0xF0。
代码解析:
#include <windows.h>#include <stdio.h>
int main() {
HANDLE hFile;char* filepath = L"C:\\Users\\29273\\Desktop\\Payload\\x86 2.59MB 数字签名 Xshell.exe\\Xshell.exe";
hFile = CreateFileW( filepath, GENERIC_ALL,0,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL ); DWORD filesize = GetFileSize(hFile, NULL);char* buffer = (char*)malloc(filesize + 1); DWORD bytesRead; PIMAGE_NT_HEADERS pNtHeader = NULL; PIMAGE_FILE_HEADER pFileHeader = NULL; BOOL success = ReadFile( hFile, // 文件句柄 buffer, // 存储数据的缓冲区 filesize, // 要读取的字节数 &bytesRead, // 实际读取的字节数NULL // 重叠I/O结构(同步操作设为NULL) );if (success) { //成功读取文件之后 解析DOS头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { //IMAGE_DOS_SIGNATURE相当于0x5A4Dprintf("不是一个有效的PE文件");CloseHandle(hFile); }printf("e_lfanew=%d\n",pDosHeader->e_lfanew);//定位到nt头 buffer缓冲区 加上 dos头中的pDosHeader->e_lfanew就可以定位到了 pNtHeader = (PIMAGE_NT_HEADERS)(buffer + pDosHeader->e_lfanew);
if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) {printf("不是一个有效的PE文件"); }//拿到文件头的地址 pFileHeader = &pNtHeader->FileHeader;
//获取到文件头的地址之后就可以输出文件头的字段了。printf("节区的数量:%x\n", pFileHeader->NumberOfSections);printf("时间戳TimeDateStamp:%x\n", pFileHeader->TimeDateStamp);printf("文件特性Characteristics:%x\n", pFileHeader->Characteristics);printf("可选头的大小:%x\n", pFileHeader->SizeOfOptionalHeader); }}
可选头(IMAGE_OPTIONAL_HEADER)
可选头有两个版本,分别是32位系统和64位系统版本。两个版本的结构几乎是一样的。主要的区别在于某些成员的大小。64位版本使用ULONGLONG类型,而32位版本则使用DWORD,还有就是32位版本中有一些64位版本不存在的成员。
可选PE头32位的字节为E0 64位则为F0。
32位版本:
typedef struct _IMAGE_OPTIONAL_HEADER32 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // 64位版本中不存在 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;
64位版本:
typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; // 64位地址空间 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; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
Magic: 描述影像文件的状态,比如32位或64位。
0x10B= 32位PE文件0x20B= 64位PE文件0x107= ROM映像
SizeOfCode表示代码段(.text节区)的大小。需要注意的是SizeOfCode是以文件对其的。
AddressOfEntryPoint表示文件入口点的偏移量,通常是main函数。这是程序执行开始时第一个被调用的代码地址。
BaseOfCode表示代码段(.text节区)起始位置的偏移量。
SizeOfImage表示影像文件在内存中的总大小。该值是在内存对其后的大小。
ImageBase指定应用程序首选的加载地址,由于ASLR(地址空间布局随机化)保护机制,实际加载的地址通常是会变化的。
DataDirectory是一个IMAGE_DATA_DIRECTORY数组,包含PE文件中的各种目录。包括导入表,导出表,资源表等等。
这里有一个东西叫做ImageBase和AddressOfEntryPoint,ImageBase指的是应用程序首选的加载地址,而AddressOfEntryPoint则是入口点的偏移量。
这意味着,ImageBase + c= 程序的入口地址。
我们这里来看一下:
如下该程序的AddressOfEntryPoint的值为: 11271 而ImageBase的值为: 140000000。 所以: 140000000(16进制) + 11271(16进制) = 140011271(程序的入口地址)
可以看到没有任何问题。
但是有一个问题,有些程序由于ASLR(地址空间布局随机化)保护机制,实际加载的地址通常是会变化的。
所以这个ImageBase是不确定的。
比如这个程序的ImageBase + AddressOfEntryPoint的值并不是如下该值: 00007FF789FF1D20。
当然我们可以在编写程序的时候来固定ImageBase基址。在链接器->高级->随机基址 这里选择否即可。
SizeOfHeader 该值是DOS头 + NT头 + 标准PE头 + 可选PE头 + 区段头 最后按照文件对其方式对其。
如下代码:
#include <windows.h>#include <stdio.h>
int main() {
HANDLE hFile;char* filepath = L"C:\\Users\\29273\\Desktop\\Payload\\x86 2.59MB 数字签名 Xshell.exe\\Xshell.exe";
hFile = CreateFileW( filepath, GENERIC_ALL,0,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL ); DWORD filesize = GetFileSize(hFile, NULL);char* buffer = (char*)malloc(filesize + 1); DWORD bytesRead; PIMAGE_NT_HEADERS pNtHeader = NULL; PIMAGE_FILE_HEADER pFileHeader = NULL; PIMAGE_OPTIONAL_HEADER pOptionHeader = NULL; BOOL success = ReadFile( hFile, // 文件句柄 buffer, // 存储数据的缓冲区 filesize, // 要读取的字节数 &bytesRead, // 实际读取的字节数NULL // 重叠I/O结构(同步操作设为NULL) );if (success) { //成功读取文件之后 解析DOS头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { //IMAGE_DOS_SIGNATURE相当于0x5A4Dprintf("不是一个有效的PE文件");CloseHandle(hFile); }printf("e_lfanew=%d\n",pDosHeader->e_lfanew);//定位到nt头 buffer缓冲区 加上 dos头中的pDosHeader->e_lfanew就可以定位到了 pNtHeader = (PIMAGE_NT_HEADERS)(buffer + pDosHeader->e_lfanew);
if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) {printf("不是一个有效的PE文件"); }//拿到文件头的地址 pFileHeader = &pNtHeader->FileHeader;
//获取到文件头的地址之后就可以输出文件头的字段了。printf("节区的数量:%x\n", pFileHeader->NumberOfSections);printf("时间戳TimeDateStamp:%x\n", pFileHeader->TimeDateStamp);printf("文件特性Characteristics:%x\n", pFileHeader->Characteristics);printf("可选头的大小:%x\n", pFileHeader->SizeOfOptionalHeader);
pOptionHeader = &pNtHeader->OptionalHeader;printf("SizeOfCode 代码段的大小:%x\n", pOptionHeader->SizeOfCode);printf("AddressOfEntryPoint 程序的入口偏移地址:%x\n",pOptionHeader->AddressOfEntryPoint);printf("ImageBase 程序的加载地址(没有ASLR的情况下):%x\n", pOptionHeader->ImageBase);printf("SizeOfImage 程序在内存中的总大小(内存对其): %x\n", pOptionHeader->SizeOfImage);printf("SizeOfHeader(DOS头 + Nt头 + 文件头 + 可选头 + 区段头): %x\n", pOptionHeader->SizeOfHeaders);printf("FileAlignment 文件对其:%x\n", pOptionHeader->FileAlignment);printf("SectionAlignment 内存对其:%x\n", pOptionHeader->SectionAlignment);
}}
节区头(IMAGE_SECTION_HEADER)
PE节区包含用于创建可执行程序的代码和数据,每个节区都有一个唯一的名称,通常包含可执行代码,数据或资源信息。PE节区的数量不是固定的,因为不同的编译器可以根据配置来添加,删除或合并节区。有些节区也可以是后期手动添加的,因为它是动态的,可以通过文件头中的NumberOfSections字段来确定有多少个节区。
每一个节区都有一个IMAGE_SECTION_HEADER数据结构,该数据结构包含了节区相关的信息,比如节区的名称,节区的在内存中的大小,在文件中的大小等等。
typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //8字节 union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; //对齐前的大小 比如对齐前是201 那么201就是对齐前的大小 这个值是可以直接干掉的 改掉之后不会影响运行 DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics;} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name表示节区的名称,比如.text``.data``.rdata。
PhysicalAddress或VirtualSize表示节区在内存中的大小。
VirtualAddress表示节区在内存中的起始偏移地址。 加上ImageBase才是内存中的真正地址。
SizeOfRawData 表示节区在文件中的大小。文件中 对齐后的大小。
PointerToRawData表示节区在文件中的偏移地址。
Characteristics表示节区的属性标志(可读 可写 可执行等等)。
常见节区
.text节区: 包含了可执行代码。.data节区: 包含了已经初始化的数据,也就是在代码中初始化的变量。.rdata节区: 包含了只读数据,都是以const前缀声明的常量变量。.idata节区: 包含了导入表,这些是代码中使用函数相关的信息表。Windows PE加载器使用它来确定将那些DLL加载到进程中,以及每个DLL使用那些函数。.reloc节区: 包含如何修复内存地址的信息,以便程序可以无错误的加载到内存中。.rsrc节区: 用于存储资源 比如图标或位图等信息。
解析代码如下:
#include <windows.h>#include <stdio.h>
int main(){ HANDLE hFile; PIMAGE_DOS_HEADER pDosHeader = NULL; PIMAGE_NT_HEADERS pNtHeader = NULL; PIMAGE_FILE_HEADER pFileHeader = NULL; PIMAGE_OPTIONAL_HEADER pOptionalHeader = NULL; PIMAGE_SECTION_HEADER pSectionHeader = NULL;char* filepath = "C:\\Users\\29273\\Desktop\\xshell\\App\\Xshell\\Xshell.exe"; hFile = CreateFileA( filepath, GENERIC_ALL,0,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL ); DWORD filesize = GetFileSize(hFile, NULL);char* buffer = (char*)malloc(filesize + 1); DWORD byteread;ReadFile( hFile, buffer, filesize, &byteread,NULL );
pDosHeader = (PIMAGE_DOS_HEADER)buffer;if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {printf("不是一个有效的PE文件!!!\n"); }//获取到NT头 pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + buffer);
//获取文件头 pFileHeader = &pNtHeader->FileHeader;
//从文件头中获取到节区的数量 DWORD NumberSections = pFileHeader->NumberOfSections;
pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
for (DWORD i = 0; i < NumberSections; i++) {// 打印节区基本信息printf("%s %x %x %x %x %x\n", pSectionHeader[i].Name, // 节区名称 pSectionHeader[i].Misc.VirtualSize, // 虚拟大小 pSectionHeader[i].VirtualAddress, // 虚拟地址(RVA) pSectionHeader[i].SizeOfRawData, // 原始数据大小 pSectionHeader[i].PointerToRawData, // 原始数据偏移 pSectionHeader[i].Characteristics); // 特性标志 } }
数据目录
数据目录是可选头中的最后一个成员,它的数据类型为IMAGE_DATA_DIRECTORY的数组,该结构如下数据结构:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
数据目录数组的大小是IMAGE_NUMBEROF_DIRECTORY_ENTRIES。这是一个值为16的常量。数组中的每一个元素代表一个特定的数据目录。
可以使用数据目录在数组中的索引来访问特定的数据目录。
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // 导出目录#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 导入目录#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // 资源目录#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // 异常目录#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // 安全目录#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // 基址重定位表#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // 调试目录#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // 体系结构特定数据#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // GP 的 RVA#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS 目录#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // 加载配置目录#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // 头文件中的绑定导入目录#define IMAGE_DIRECTORY_ENTRY_IAT 12 // 导入地址表#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // 延迟加载导入描述符#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM 运行时描述符
我们发现只有0-14,只有15项,但是签名说的是16项,为什么少了一项?因为最后一项是空的。
今天主要来看第零项,也就是这个导出目录,也可以叫做导出表。
这里我们来看一下这个结构:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //内存中的偏移地址 RVA内存偏移 //FOA FOA+FileBuffer = 导出表的地址 DWORD Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
首先这里的VirtualAddress表示的是内存中的偏移,也就是RVA内存偏移。但是我们在想一个问题。
我们前面所写的代码,都是通过申请一块内存,然后将其PE文件读入到内存中。相当于将硬盘中的PE文件读入到了内存中。
需要注意的是这里是读入,而不是加载到内存中。buffer指针指向的内存区域包含了完整的PE文件内容。按照文件对齐的方式。
所以说,如果我们想要来解析导出表,首先,我们肯定是需要将其在内存中的偏移地址转换为文件中的偏移地址。也就是RVA转换为FOA。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Relay学安全 kernel
kernel《2.免杀基础》