前言:
同行例子:
这是一篇8月16日发布的文章:
部分内容:
3.5k的阅读量,224的转发量,我盲猜比我这篇文章的要高,那么有师傅就要问了,这种机翻味满满的文章应该如何盈利呢?
我在直播的时候也说了,直接挂知识星球就行了:
《从0-1开始学免杀》系列第二节
Windows内存管理
简介
本模块将详细讲解 Windows 内存基础知识。了解 Windows 如何处理内存对于构建高级恶意软件至关重要。
虚拟内存和分页
现代操作系统中的内存不会直接映射到物理内存(即 RAM)。相反,进程使用虚拟内存地址,这些地址映射到物理内存地址。这样做有几个原因,但最终目标是尽可能节省物理内存。虚拟内存可以映射到物理内存,但也可以存储在磁盘上。通过虚拟内存寻址,多个进程可以在拥有唯一虚拟内存地址的同时共享相同的物理地址。虚拟内存依赖于 内存分页 的概念,它将内存分成 4kb 的块,称为“页”。
页面状态
进程的虚拟地址空间中的页面可以处于以下三种状态之一:
1. 空闲 - 页面既未提交也未保留。该页面对进程不可访问。它可被保留、提交或同时保留和提交。尝试从空闲页面读取或写入内容会导致访问冲突异常。
2. 已保留 - 该页面已保留以备将来使用。该地址范围不能被其他分配函数使用。该页面不可访问,并且没有与之关联的物理存储。它可被提交。
3. 已提交 - 已从 RAM 总大小和磁盘中的页面文件中分配内存费用。该页面可被访问,并且访问受一个内存保护常量控制。系统仅在首次尝试读取或写入该页面时,才会初始化并将其加载到物理内存中。当进程终止时,系统将释放提交页面的存储空间。
页面保护选项
提交页面后,需要对其设置保护选项。此处可以找到内存保护常量的列表:单击此处open in new window,以下列出一些示例:
•
PAGE_NOACCESS
- 禁用对已提交页面区域的所有访问。尝试读取、写入或执行已提交区域将导致访问冲突。•
PAGE_EXECUTE_READWRITE
- 启用读取、写入和执行。强烈建议不要使用此选项,通常是 IoC,因为内存同时具有可写性和可执行性并不常见。•
PAGE_READONLY
- 启用对已提交页面区域的只读访问。尝试写入已提交区域将导致访问冲突。
内存保护
现代操作系统通常内置了内存保护功能来阻止攻击。这些功能在构建或调试恶意软件时也需要考虑。
• 数据执行保护 (DEP) - DEP 是从 Windows XP 和 Windows Server 2003 开始内置到操作系统中的系统级内存保护功能。如果页面保护选项设置为 PAGE_READONLY,DEP 将阻止代码在该内存区域中执行。
• 地址空间布局随机化 (ASLR) - ASLR 是一种内存保护技术,用于防止利用内存损坏漏洞。ASLR 随机排列进程关键数据区域(包括可执行文件的基地址以及堆栈、堆和库的位置)的地址空间位置。
x86 vs x64 内存空间
在处理 Windows 进程时,请注意进程是 x86 还是 x64。x86 进程的内存空间较小,为 4GB(0xFFFFFFFF
),而 x64 拥有更大的内存空间,为 128TB(0xFFFFFFFFFFFFFFFF
)。
内存分配示例
此示例通过一些小代码段,更好地理解人们如何通过 C 函数和 Windows API 与 Windows 内存交互。交互内存的第一步是分配内存。下面的代码段演示了几种分配内存的方式,这本质上是在运行进程中保留内存。
// 分配一个 *100* 字节的内存缓冲区
// 方法 1 - 使用 malloc()
PVOID pAddress = malloc(100);
// 方法 2 - 使用 HeapAlloc()
PVOID pAddress = HeapAlloc(GetProcessHeap(), 0, 100);
// 方法 3 - 使用 LocalAlloc()
PVOID pAddress = LocalAlloc(LPTR, 100);
内存分配函数返回 基地址,它只是一个指向已分配内存块起始部分的指针。使用上述代码段,pAddress
将成为已分配内存块的基地址。使用此指针,可以执行读取、写入和执行等操作。能执行的操作类型取决于已分配内存区域分配的保护。
下图显示了在调试器下 pAddress
的样子。
分配内存时,它可能为空或包含随机数据。一些内存分配函数提供了一个选项,可以在分配过程中将内存区域清零。
内存写入示例
完成内存分配后的下一步通常是写入缓冲区。可以有许多选项用来写入内存,但本示例使用 memcpy
来执行此操作。
PVOID pAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100);
CHAR *cString = "MalDev Academy Is The Best";
memcpy(pAddress, cString, strlen(cString));
HeapAlloc
使用 HEAP_ZERO_MEMORY
标志,这将使已分配的内存初始化为零。然后使用 memcpy
将字符串复制到已分配的内存中。memcpy
中的最后一个参数是要复制的字节数。接下来,重新检查缓冲区以验证数据是否已成功写入。
释放已分配的内存
当应用程序完成使用已分配的缓冲区时,强烈建议取消分配或释放缓冲区,以避免内存泄漏open in new window。
根据用来分配内存的函数不同,它将有对应的内存取消分配函数。例如:
• 使用
malloc
分配需要使用free
函数。• 使用
HeapAlloc
分配需要使用HeapFree
函数。• 使用
LocalAlloc
分配需要使用LocalFree
函数。
下图显示了 HeapFree
的实际操作,释放位于地址 0000023ADE449900
处的已分配内存。请注意,地址 0000023ADE449900
仍存在于进程中,但其原始内容已被随机数据覆盖。这个新数据很可能是由操作系统在进程内部执行的新分配造成的。
Windows API 简介
[简介]
Windows API 为开发人员提供了一种让其应用程序与 Windows 操作系统交互的方式。例如,如果应用程序需要在屏幕上显示某些内容、修改文件或查询注册表,所有这些操作都可以通过 Windows API 完成。Microsoft 对 Windows API 有着非常详细的文档说明,可在此处查看:此处open in new window。
[Windows 数据类型]
Windows API 定义了许多数据类型,不限于已知的数据类型(如 int、float)。数据类型已经过文档化,可以在此open in new window查看。
以下是常见数据类型列表:
•
DWORD
- 32 位无符号整数,在 32 位和 64 位系统上都用于表示从 0 到 (2^32 - 1) 的值。
DWORD dwVariable = 42;
•
size_t
- 用于表示对象的大小。在 32 位系统上是 32 位无符号整数,表示从 0 到 (2^32 - 1) 的值。而在 64 位系统上是 64 位无符号整数,表示从 0 到 (2^64 - 1) 的值。
SIZE_T sVariable = sizeof(int);
•
VOID
- 表示没有指定的数据类型。
void* pVariable = NULL; // 这与 PVOID 相同
•
PVOID
- 32 位系统上任何数据类型的 32 位或 4 字节指针。或者,64 位系统上任何数据类型的 64 位或 8 字节指针。
PVOID pVariable = &SomeData;
•
HANDLE
- 指定操作系统正在管理的特定对象(如文件、进程、线程)的值。
HANDLE hFile = CreateFile(...);
•
HMODULE
- 模块的句柄。这是模块在内存中的基址。MODULE 的一个示例可以是 DLL 或 EXE 文件。
HMODULE hModule = GetModuleHandle(...);
•
LPCSTR/PCSTR
- 指向 8 位 Windows 字符(ANSI)的常量空终止字符串的指针。“L”表示“long”,源自 16 位 Windows 编程时期,现在已经不影响数据类型,但命名约定仍然存在。“C”表示“常量”或只读变量。这两个数据类型等效于const char*
。
LPCSTR lpcString = "Hello, world!";
PCSTR pcString = "Hello, world!";
•
LPSTR/PSTR
- 与LPCSTR
和PCSTR
相同,唯一的区别是LPSTR
和PSTR
不指向常量变量,而是指向可读写的字符串。这两个数据类型等效于char*
。
LPSTR lpString = "Hello, world!";
PSTR pString = "Hello, world!";
•
LPCWSTR\PCWSTR
- 指向 16 位 Windows Unicode 字符(Unicode)的常量空终止字符串的指针。这两个数据类型等效于const wchar*
。
LPCWSTR lpwcString = L"Hello, world!";
PCWSTR pcwString = L"Hello, world!";
•
PWSTR\LPWSTR
- 与LPCWSTR
和PCWSTR
相同,唯一的区别是PWSTR
和LPWSTR
不指向常量变量,而是指向可读写的字符串。这两个数据类型等效于wchar*
。
LPWSTR lpwString = L"Hello, world!";
PWSTR pwString = L"Hello, world!";
•
wchar_t
- 与wchar
相同,用于表示宽字符。
wchar_t wChar = L'A';
wchar_t* wcString = L"Hello, world!";
•
ULONG_PTR
- 表示无符号整数,其大小与指定架构上的指针相同,这意味着在 32 位系统上,ULONG_PTR
的大小为 32 位,而在 64 位系统上,其大小为 64 位。在本课程中,ULONG_PTR
将用于处理包含指针的算术表达式(如 PVOID)。在执行任何算术运算之前,指针将被强制转换为ULONG_PTR
。此方法用于避免直接操作指针,这可能导致编译错误。
PVOID Pointer = malloc(100);
// Pointer = Pointer + 10; // 不允许
Pointer = (ULONG_PTR)Pointer + 10; // 允许
[数据类型和指针]
Windows API 允许开发者直接声明一个数据类型或数据类型的指针。这体现在数据类型名称中,以“P”开头的名称代表指向实际数据类型的指针,而不以“P”开头的名称代表实际数据类型本身。
这将在使用具有指向数据类型的参数的 Windows API 时变得有用。以下示例展示了“P”数据类型与其非指针等效类型之间的关系。
•
PHANDLE
与HANDLE*
相同。•
PSIZE_T
与SIZE_T*
相同。•
PDWORD
与DWORD*
相同。 ANSI 和 Unicode 函数
大多数 Windows API 函数都有两个版本,分别以“A”或“W”结尾。例如,有 CreateFileAopen in new window 和 CreateFileWopen in new window。以“A”结尾的函数表示“ANSI”,而以“W”结尾的函数表示 Unicode 或“Wide”。
需要注意的主要区别是,在需要时,ANSI 函数将采用 ANSI 数据类型作为参数,而 Unicode 函数将采用 Unicode 数据类型。例如,CreateFileA
的第一个参数是 LPCSTR
,它是一个指向由 8 位 Windows ANSI 字符组成的常量以空字符结尾的字符串的指针。另一方面,CreateFileW
的第一个参数是 LPCWSTR
,它是一个指向由 16 位 Unicode 字符组成的常量以空字符结尾的字符串的指针。
此外,所需的字节数会根据所使用的版本而有所不同。
char str1[] = "maldev";
// 7 字节(maldev + 空字节open in new window。
wchar str2[] = L"maldev";
// 14 字节,每个字符 2 字节(空字节也是 2 字节)
[输入和输出参数]
Windows API 具有 inopen in new window 和 outopen in new window 参数。IN
参数是传递给函数并用作输入的参数,而 OUT
参数是用于将值返回给函数调用者的参数。输出参数通常通过指针以引用方式传递。
例如,下面的代码片段显示了一个函数 HackTheWorld
,其接收一个整数指针并将值设置为 123
。这被认为是一个输出参数,因为该参数正在返回值。
BOOL HackTheWorld(OUT int* num){
// 将 num 的值设置为 123
*num =123;
// 返回布尔值
return TRUE;
}
intmain(){
int a =0;
// 'HackTheWorld' 将返回 true
// 'a' 将包含值 123
HackTheWorld(&a);
}
请记住,OUT
或 IN
关键字的使用是为了方便开发人员理解函数的预期和它对这些参数的操作。但是,值得一提的是,排除这些关键字不会影响该参数是否被视为输出或输入参数。
[Windows API 示例]
现在,Windows API 的基础知识已经讲述完毕,本节将通过 CreateFileW
函数,了解如何使用 Windows API。
如果对某个函数的功能或所需参数不确定,始终参考文档非常重要。务必阅读函数描述,并评估该函数是否能够完成所需任务。CreateFileW
文档此处open in new window提供。
下一步是查看函数的参数以及返回的数据类型。文档指出:如果函数成功,返回值是指定文件、设备、命名管道或邮件槽的开放句柄,因此 CreateFileW
返回一个 HANDLE
数据类型到所创建的指定项。
此外,请注意函数参数都是 in
参数。这意味着函数不会从参数返回任何数据,因为它们都是 in
参数。请记住,方括号中的关键字,如 in
、out
和 optional
,仅供开发人员参考,不会产生任何实际影响。
HANDLE CreateFileW(
[in] LPCWSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
[使用函数]
以下示例代码演示了 CreateFileW
的使用方法。它将在当前用户的桌面上创建一个名为 maldev.txt
的文本文件。
// 这用于存储对文件对象的句柄
// 'INVALID_HANDLE_VALUE' 用于初始化变量
HANDLE hFile = INVALID_HANDLE_VALUE;
// 所创建文件的完整路径。
// 需要使用双反斜杠来转义 C 中的单个反斜杠字符
// 确保用户名 (maldevacademy) 存在,否则请修改该用户名
LPCWSTR filePath =L"C:\\Users\\maldevacademy\\Desktop\\maldev.txt";
// 使用文件路径调用 CreateFileW
// 其他参数直接取自文档
hFile =CreateFileW(filePath, GENERIC_ALL,0,NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,NULL);
// CreateFileW 在失败时返回 INVALID_HANDLE_VALUE
// GetLastError() 是用于检索先前执行的 WinAPI 函数的错误代码的另一个 Windows API
if(hFile == INVALID_HANDLE_VALUE){
printf("[-] CreateFileW Api 函数调用失败,错误为:%d\n",GetLastError());
return-1;
}
[Windows API 调试错误]
函数在失败时,通常会返回一条简略的错误信息。例如,如果 CreateFileW
失败,它将返回 INVALID_HANDLE_VALUE
,这表示无法创建文件。若要更深入地了解文件无法创建的原因,必须使用 GetLastErroropen in new window 函数检索错误代码。
检索到代码后,需要在 Windows 系统错误代码列表open in new window 中查找它。下面翻译了一些常见的错误代码:
•
5
- ERROR_ACCESS_DENIED(访问被拒绝)•
2
- ERROR_FILE_NOT_FOUND(找不到文件)•
87
- ERROR_INVALID_PARAMETER(无效参数)
[Windows 原生 API 调试错误]
从“Windows 架构”模块中可以回忆起来,NTAPI 大多是从 ntdll.dll
导出的。与 Windows API 不同,这些函数无法通过 GetLastError
获取错误代码。相反,它们直接返回由 NTSTATUS
数据类型表示的错误代码。
NTSTATUS
用于表示系统调用或函数的状态,被定义为 32 位无符号整数值。成功的系统调用将返回 STATUS_SUCCESS
值,该值是 0
。另一方面,如果调用失败,它将返回一个非零值,要进一步调查问题的原因,必须查看 Microsoft 关于 NTSTATUS 值的文档open in new window。
下面的代码片段演示了如何对系统调用进行错误检查。
NTSTATUS STATUS = NativeSyscallExample(...);
if (STATUS != STATUS_SUCCESS){
// 以无符号整数十六进制格式打印错误
printf("[!] NativeSyscallExample 失败,状态码为:0x%0.8X\n", STATUS);
}
// NativeSyscallExample 成功
[NT_SUCCESS 宏]
同样,还可以通过此处所示的 NT_SUCCESS
宏来检查 NTAPI 的返回值 此处open in new window。如果函数执行成功,则此宏返回 TRUE
,如果函数执行失败,则返回 FALSE
。
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
以下是使用此宏的一个示例
NTSTATUS STATUS = NativeSyscallExample(...);
if (!NT_SUCCESS(STATUS)){
// 以无符号整数十六进制格式打印错误
printf("[!] NativeSyscallExample 失败,状态码:0x%0.8X \n", STATUS);
}
// NativeSyscallExample 执行成功
便携式可执行文件格式
简介
可移植可执行文件 (PE) 是 Windows 上可执行文件的格式。PE 文件扩展名示例有 .exe
、.dll
、.sys
和 .scr
。本模块讨论 PE 结构,这对构建或逆向工程恶意软件时要了解很重要。
请注意,本模块和未来模块经常将可执行文件(例:EXEs、DLLs)互换地称为“映像”。
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;// 重定位表的 file 地址
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
或 MZ
。
e_lfanew
是一个 4 字节的值,持有到 NT 头开始处的偏移量。请注意,e_lfanew
始终位于 0x3C
偏移量处。
DOS 存根
在深入 NT 标头结构之前,这里有一个错误消息,它打印以下内容:"该程序无法在 DOS 模式下运行",此情况是在程序加载在 DOS 模式open in new window(即磁盘操作系统模式)下或加载在 DOS 模式open in new window(即磁盘操作系统模式)下时触发的。值得注意的是,程序员可以在编译时更改此错误消息。这不是一个 PE 头,但知道它有好处。
NT 头 (IMAGE_NT_HEADERS)
NT 头至关重要,因为它包含两个其他映像头:FileHeader
和 OptionalHeader
,其中包含大量有关 PE 文件的信息。与 DOS 头类似,NT 头包含一个用于验证它的签名成员。通常,签名元素等于 "PE" 字符串,由 0x50
和 0x45
字节表示。但由于签名是 DWORD
数据类型的,所以签名将表示为 0x50450000
,它仍然是 "PE",只是用两个空字节填充。可以使用 DOS 头中的 e_lfanew
成员访问 NT 头。
NT 头结构根据机器体系结构而有所不同。
32 位版本:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
64 位版本:
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
唯一区别是 OptionalHeader
数据结构,IMAGE_OPTIONAL_HEADER32
和 IMAGE_OPTIONAL_HEADER64
。
文件头 (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
- PE 文件中的节区数(详见后面)。•
Characteristics
- 指定可执行文件特定属性的标志,例如它是动态链接库 (DLL) 还是控制台应用程序。•
SizeOfOptionalHeader
- 以下可选头的长度
有关文件头的其他信息,请参阅 官方文档open in new window。
可选头(IMAGE_OPTIONAL_HEADER)
可选头非常重要,虽然它被称为“可选”,但它是 PE 文件执行所必需的。之所以称为可选,是因为某些文件类型没有它。
可选头有两个版本,一个用于 32 位系统,一个用于 64 位系统。这两个版本的数据结构中成员几乎相同,主要区别是某些成员的大小。64 位版本中使用 ULONGLONG
,32 位版本中使用 DWORD
。此外,32 位版本中有一些成员在 64 位版本中没有。
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;
64 位版本:
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG 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;
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 位映像)•
MajorOperatingSystemVersion
- 所需操作系统的主要版本号(例如 11、10)•
MinorOperatingSystemVersion
- 所需操作系统的次要版本号(例如 1511、1507、1607)•
SizeOfCode
-.text
节的大小(后面讨论)•
AddressOfEntryPoint
- 指向文件入口点(通常是_main_
函数)的偏移量•
BaseOfCode
- 指向.text
节开头的偏移量•
SizeOfImage
- 映像文件的大小(以字节为单位)•
ImageBase
- 指定应用程序在执行时加载到内存中的首选地址。但是,由于 Windows 的内存保护机制(如地址空间布局随机化 (ASLR)),很少看到映像映射到其首选地址,因为 Windows PE 加载器将文件映射到不同的地址。Windows PE 加载器完成的这种随机分配会导致未来技术实现出现问题,因为某些被认为是常量的地址被更改了。然后,Windows PE 加载器将通过PE 重定位
来修复这些地址。•
DataDirectory
- 可选头中最重要的成员之一。这是 IMAGE_DATA_DIRECTORYopen in new window 数组,包含 PE 文件中的目录(将在下面讨论)。
数据目录
数据目录可以通过可选头的最后一个成员访问。这是一个 IMAGE_DATA_DIRECTORY
数据类型的数组,具有以下数据结构:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
数据目录数组的大小为 IMAGE_NUMBEROF_DIRECTORY_ENTRIES
,这是一个常数值为 16
。数组中的每个元素代表一个特定数据目录,其中包含有关 PE 节或数据表(保存特定 PE 信息)的一些数据。
可以使用特定数据目录在其在数组中的索引来访问它。
#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 运行时描述符
以下两个小节将简要介绍两个重要的数据目录:导出目录
和 导入地址表
。
导出目录
PE 的导出目录是一个数据结构,包含可执行文件中导出的函数和变量的信息。它包含导出函数和变量的地址,其他可执行文件可使用这些地址访问函数和数据。一般来说,导出目录位于导出函数的 DLL 中(例如, kernel32.dll
导出 CreateFileA
)。
导入地址表
PE 文件中有一个数据结构导入地址表
,它包含从其他可执行文件中引入的函数地址的信息。这些地址用于访问其他可执行文件中的函数和数据(例如,Application.exe
从 kernel32.dll
导入 CreateFileA
函数)。
PE 节
PE 节包含用于创建可执行程序的代码和数据。每个 PE 节都有一个唯一名称,通常包含可执行代码、数据或资源信息。PE 节的数量并非恒定,因为不同的编译器可以根据配置添加、删除或合并节。某些部分也可以稍后手动添加,因此它是动态的,IMAGE_FILE_HEADER.NumberOfSections
有助于确定该数量。
以下 PE 节是最重要的,几乎存在于每个 PE 中。
•
.text
- 包含可执行代码,即已编写的代码。•
.data
- 包含已初始化的数据,即在代码中初始化的变量。•
.rdata
- 包含只读数据。这些是用const
修饰的前缀的常量变量。•
.idata
- 包含导入表。这些是与使用代码调用的函数相关的信息表。Windows PE 加载器使用它来确定要加载到进程的 DLL 文件,以及从每个 DLL 使用的函数。•
.reloc
- 包含有关如何修复内存地址的信息,以便程序可以在没有任何错误的情况下加载到内存中。•
.rsrc
- 用于存储图标和位图等资源
每个 PE 节都有一个包含有关其有价值信息的数据结构 IMAGE_SECTION_HEADERopen in new window 。这些结构保存在 PE 文件中的 NT 头下,并相互堆叠,其中每个结构代表一个节。
回想一下,IMAGE_SECTION_HEADER 结构如下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
}Misc;
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
- 内存中节的起始位置的偏移量。
更多参考资料
如果对某些章节需要进一步的说明,强烈推荐参阅以下0xRick's Blogopen in new window上的博客文章。
• PE 概览 - https://0xrick.github.io/win-internals/pe2/open in new window
• DOS 头、DOS 存根和富头 - https://0xrick.github.io/win-internals/pe3/open in new window
• NT 头 - https://0xrick.github.io/win-internals/pe4/open in new window
• 数据目录、节头和节 - https://0xrick.github.io/win-internals/pe5/open in new window
• PE 导入(导入目录表、ILT、IAT) - https://0xrick.github.io/win-internals/pe6/open in new window
结论
首次接触 PE 头信息可能比较有挑战性。幸运的是,普通模块并不需要深入了解 PE 结构。但是,要让恶意软件执行更复杂的技术,则需要加深了解,因为有些代码需要解析 PE 文件的头信息和节。这很可能出现在中级和高级模块中。
动态链接库
介绍
.exe
和 .dll
两种文件类型都属于便携式可执行格式,但两者之间存在差异。例如,一个可以通过立即注意到的主要区别是,.exe
文件可以通过双击执行,而 .dll
文件则不能。本模块将概述两种文件类型之间的其他差异。
什么是 DLL?
DLL 是可执行函数或数据的共享库,可被多个应用程序同时使用。它们用于导出供进程使用的函数。与 EXE 文件不同,DLL 文件不能单独执行代码。相反,DLL 库需要被其他程序调用才能执行代码。如前所述,CreateFileW
从 kernel32.dll
导出,因此如果进程想要调用该函数,它首先需要将 kernel32.dll
加载到其地址空间中。
默认情况下,某些 DLL 会自动加载到每个进程中,因为这些 DLL 导出了进程正确执行所需的函数。这些 DLL 的几个示例包括 ntdll.dll
、kernel32.dll
和 kernelbase.dll
。下图显示了当前由 explorer.exe
进程加载的几个 DLL。
Explorer-DLLs
系统范围 DLL 基地址
Windows 操作系统使用系统范围 DLL 基地址在给定计算机上的所有进程的虚拟地址空间中加载一些 DLL,目的是优化内存使用率并提升系统性能。下图显示了在多个正在运行的进程中,kernel32.dll
加载到同一地址 (0x7fff9fad0000
) 的情况。
image
使用 DLL 的原因
在 Windows 中大量使用 DLL 有以下几个原因:
1. 代码模块化 - 替代包含整个功能的巨型可执行文件,代码被划分为多个独立的库,每个库关注于特定的功能。模块化让开发者在开发和调试过程中更轻松。
2. 代码重用 - DLL 由于能够被多个进程调用而促进了代码重用。
3. 高效的内存使用 - 当多个进程需要相同的 DLL 时,它们可以通过共享该 DLL 而非将其加载到进程内存中来节省内存。
在 Visual Studio 中创建 DLL 文件
要创建一个 DLL 文件,请启动 Visual Studio 并新建一个项目。当显示项目模板时,选择“动态链接库 (DLL)”选项。
接下来,选择保存项目文件的位置。保存项目后,将显示带有默认 DLL 代码的 dllmain.cpp
。
DLL 入口点
回想一下,DLL 是由应用程序(如 .exe
文件)加载的。因此,DLL 可以指定一个入口点函数,该函数将在发生特定操作时执行代码。入口点有可能在 4 个位置被调用:
•
DLL_PROCESS_ATTACH
- 进程正在加载 DLL。•
DLL_THREAD_ATTACH
- 进程正在创建一个新的线程。•
DLL_THREAD_DETACH
- 线程正常退出。•
DLL_PROCESS_DETACH
- 进程卸载 DLL。
导出函数
DLL 可以导出函数以供调用应用程序使用。要导出函数,需要使用关键词 extern
和 __declspec(dllexport)
进行定义。以下 sampleDLL.dll
文件中展示了一个示例导出的函数 HelloWorld
。
////// sampleDLL.dll //////
#include <Windows.h>
// 导出的函数
extern __declspec(dllexport)voidHelloWorld(){
MessageBoxA(NULL,"Hello, World!","DLL 消息", MB_ICONINFORMATION);
}
// DLL 的入口点
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){
switch(ul_reason_for_call){
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
在将 sampleDLL.dll
载入内存后,外部应用程序现在可以调用 HelloWorld
。
动态链接
可以使用 LoadLibrary
、GetModuleHandle
和 GetProcAddress
WinAPI 从 DLL 导入函数。这称为 动态链接open in new window。这是一种在运行时加载并链接代码(DLL)的方法,而不是使用链接器和导入地址表在编译时对其进行链接。使用动态链接有几个优点;这些优点由 Microsoft 在此处open in new window 记录。
以下部分将逐步介绍加载 DLL、检索 DLL 的句柄、检索导出的函数地址,然后从外部二进制文件调用函数。
步骤 1 - 加载 DLL
从这一步开始,我们将切换到一个 EXE 文件。这是因为我们的 EXE 文件将加载 sampleDLL.dll
,然后调用 HelloWorld
函数。因此,创建一个新的 Win32 控制台应用程序,然后按照说明调用 HelloWorld
。
在应用程序中调用诸如 MessageBoxAopen in new window 的函数时,操作系统会强制 Windows 将导出 MessageBoxA
函数的 DLL 加载到调用进程的内存地址空间,在本例中是 user32.dll
。user32.dll
的加载是由操作系统在进程启动时自动完成,而不是由代码完成。
但是,对于我们的 sampleDLL.dll
等自定义 DLL,该 DLL 将不会被加载到内存中。由于应用程序没有将 sampleDLL.dll
加载到内存中,因此需要使用 LoadLibraryopen in new window WinAPI,如下所示:
#include <windows.h>
int main() {
// 加载 DLL(Dynamic Link Library,动态链接库)
HMODULE hModule = LoadLibraryA("sampleDLL.dll"); // hModule 现在包含 sampleDLL.dll 的句柄
}
第 2 步 - 检索 DLL 的句柄
如果 sampleDLL.dll
已加载到应用程序的内存中,则可以通过 GetModuleHandleopen in new window WinAPI 函数检索其句柄,而无需使用 LoadLibrary
函数。
#include <windows.h>
intmain(){
// 尝试获取已在内存中的 DLL 的句柄
HMODULE hModule =GetModuleHandleA("sampleDLL.dll");
if(hModule ==NULL){
// 如果 DLL 未加载到内存中,使用 LoadLibrary 将其加载
hModule =LoadLibraryA("sampleDLL.dll");
}
}
步骤 3 - 获取函数地址
一旦 DLL 加载到内存中且已检索到句柄,下一步就是检索函数的地址。此操作通过以下方式完成:GetProcAddressopen in new window WinAPI,它采用导出函数的 DLL 句柄和函数名称。
#include <windows.h>
intmain(){
// 尝试获取 DLL 的句柄
HMODULE hModule =GetModuleHandleA("sampleDLL.dll");// HMODULE:句柄类型
if(hModule ==NULL){
// 如果 DLL 未加载到内存中,则使用 LoadLibrary 加载它
hModule =LoadLibraryA("sampleDLL.dll");
}
PVOID pHelloWorld =GetProcAddress(hModule,"HelloWorld");// PVOID:指针类型 // pHelloWorld 存储 HelloWorld 的函数地址
}
第4步 - 进行函数地址类型转换
一旦将HelloWorld
的地址保存在pHelloWorld
变量中,下一步就是针对此地址执行一次类型转换,转换为HelloWorld
的函数指针。必须使用此函数指针才能调用函数。
#include <windows.h>
// 构造新数据类型,该类型表示HelloWorld的函数指针
typedefvoid(WINAPI* HelloWorldFunctionPointer)();
intmain(){
// 尝试获取DLL的句柄
HMODULE hModule =GetModuleHandleA("sampleDLL.dll");
if(hModule ==NULL){
// 如果未在内存中加载DLL,则使用 LoadLibrary 进行加载
hModule =LoadLibraryA("sampleDLL.dll");
}
PVOID pHelloWorld =GetProcAddress(hModule,"HelloWorld");/// pHelloWorld 存储 HelloWorld 的函数地址
HelloWorldFunctionPointerHelloWorld=(HelloWorldFunctionPointer)pHelloWorld;
return0;
}
集成 - 调用 HelloWorld
本节将上述所有步骤集成到一个名为 call()
的函数中。该函数将主要执行以下步骤:
1. 加载
sampleDLL.dll
2. 检索
HelloWorld
函数的地址3. 将
HelloWorld
类型转换为指针4. 调用
HelloWorld
同样,这个函数是从我们的 .exe
程序中调用的,因为它加载了 DLL 并调用了 HelloWorld
函数。
#include <windows.h>
// 构建新的数据类型以表示 HelloWorld 函数指针
typedefvoid(WINAPI* HelloWorldFunctionPointer)();
voidcall(){
// 尝试获取 DLL 句柄
HMODULE hModule =GetModuleHandleA("sampleDLL.dll");
if(hModule ==NULL){
// 如果未将 DLL 加载到内存中,则使用 LoadLibrary 加载它
hModule =LoadLibraryA("sampleDLL.dll");
}
// pHelloWorld 存储 HelloWorld 函数地址
PVOID pHelloWorld =GetProcAddress(hModule,"HelloWorld");
// 将 pHelloWorld 的类型转换为 HelloWorldFunctionPointer
HelloWorldFunctionPointerHelloWorld=(HelloWorldFunctionPointer)pHelloWorld;
// 调用 HelloWorld
HelloWorld();
}
动态链接示例 - MessageBoxA
以下代码演示了动态链接的另一个简单示例,其中调用了 MessageBoxA
。此代码假设导出该函数的 DLL user32.dll
未加载到内存中。回想一下,如果 DLL 未加载到内存中,则需要使用 LoadLibrary
将该 DLL 加载到进程的地址空间中。
typedef int(WINAPI* MessageBoxAFunctionPointer)( // 创建一个新数据类型,表示 MessageBoxA 的函数指针
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
);
voidcall(){
// 检索 MessageBox 的地址,并将其保存到 'pMessageBoxA'(MessageBoxA 的函数指针)
MessageBoxAFunctionPointer pMessageBoxA =(MessageBoxAFunctionPointer)GetProcAddress(LoadLibraryA("user32.dll"),"MessageBoxA");
if(pMessageBoxA !=NULL){
// 如果不为空,则通过其函数指针调用 MessageBox
pMessageBoxA(NULL,"MessageBox's Text","MessageBox's Caption", MB_OK);
}
}
函数指针
在该课程的后续部分,函数指针数据类型将采用一种命名约定,即在 WinAPI 名称前加上“fn”,代表“Function Pointer”。例如,上述 MessageBoxAFunctionPointer
数据类型将表示为 fnMessageBoxA
。这可保持简洁并提高课程中的清晰性。
Rundll32.exe
除了使用编程方法,还有几种方式可以运行导出函数。一种常见的技术是使用二进制文件 rundll32.exeopen in new window。Rundll32.exe
是一个内置的 Windows 二进制文件,用于运行 DLL 文件的导出函数。要运行导出函数,请使用以下命令:
rundll32.exe <dll 名称>, <要运行的导出函数>
例如,User32.dll
导出函数 LockWorkStation
,该函数锁定计算机。要运行该函数,请使用以下命令:
rundll32.exe user32.dll,LockWorkStation
删除预编译头文件
当使用 Visual Studio 模板创建 DLL 文件时,DLL 模板中包含 framework.h
、pch.h
和 pch.cpp
,这些文件称为 预编译头文件open in new window。这些文件用于加快大型项目的编译速度。在本文档所讨论的情况下,你不太可能需要这些文件,因此建议按照以下步骤删除这些文件。
首先,使用 Visual Studio 的 DLL 模板创建一个新的 DLL 文件,如前面所示。
接下来,打开项目,高亮显示 framework.h
、pch.h
和 pch.cpp
,然后按 Delete 键并选择“删除”选项。
你还需要从 dllmain.cpp
中删除 #include "pch.h"
,并将其替换为 #include <Windows.h>
。
删除预编译头文件后,必须更改编译器的默认设置,以确认项目中不应使用预编译头文件。
转到 C/C++ > 预编译头文件
将“预编译头文件”选项更改为“不使用预编译头文件”,然后按“应用”。
最后,将 dllmain.cpp
文件更改为 dllmain.c
。这是必需的,因为 Maldev Academy 中提供的代码片段使用 C 而不是 C++。若要编译程序,请单击“生成”>“生成解决方案”,然后在 Release 或 Debug 文件夹下(具体取决于编译配置)创建一个 DLL。
检测机制
介绍
安全解决方案使用多种技术来检测恶意软件。了解安全解决方案用于检测或分类恶意软件的技术非常重要。
静态/特征码检测
特征码是一段字节或字符串,用于唯一标识恶意软件。还可以指定其他条件,例如变量名和导入函数。当安全解决方案扫描程序时,它会尝试将其与已知规则列表进行匹配。这些规则必须是预先构建的,并推送到安全解决方案中。YARAopen in new window 是安全厂商用于构建检测规则的工具之一。例如,如果 Shellcode 包含以 FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51
开头的字节序列,则可用于检测该 Payload 是 Msfvenom 的 x64 exec Payload。针对文件中的字符串可以使用相同的检测机制。
特征码检测很容易绕过,但可能很耗时。重要的是避免在恶意软件中硬编码可唯一标识实现的值。本课程中介绍的代码会尝试避免硬编码可能被硬编码的值,而会动态检索或计算这些值。
哈希检测
哈希检测是静态/签名检测的一个子集。这是一种非常直接的检测技术,也是安全解决方案检测恶意软件最快速、最简单的方法。此方法只需要把已知恶意软件的哈希值(例如 MD5、SHA256)保存到数据库中即可。恶意软件的文件哈希值将与安全解决方案的哈希值数据库进行比较,以查看是否有正向匹配。
规避哈希检测非常简单,但仅此可能还不够。通过更改文件中的至少 1 个字节,文件的哈希值将对任何哈希算法发生改变,从而使该文件具有可能唯一的哈希值。
启发式检测
由于签名检测方法可以通过对恶意文件进行微小更改而轻松规避,因此引入了启发式检测来发现未知、新版本和修改版本的现有恶意软件中可疑的特征。根据安全解决方案的不同,启发式模型可以包含以下一项或两项:
• 静态启发式分析 - 涉及反编译可疑程序并将代码片段与启发式数据库中已知的恶意软件进行比较。如果特定百分比的源代码与启发式数据库中的任何内容匹配,则标记该程序。
• 动态启发式分析 - 将程序放置在虚拟环境或 沙箱 中,然后由安全解决方案对其进行分析,以查找任何可疑行为。
动态启发式分析(沙箱检测)
沙箱检测通过在沙箱环境中执行文件,动态分析该文件的行为。在执行文件时,安全解决方案将查找可疑行为或被归类为恶意行为的举动。例如,分配内存不一定属于恶意行为,但按如下顺序分配内存、连接到互联网以获取 shellcode、将 shellcode 写入内存并执行 shellcode 则被视为恶意行为。
恶意软件开发人员会嵌入反沙箱技术来检测沙箱环境。如果恶意软件确认它在沙箱中执行,则它执行良性代码;否则,它执行恶意代码。
基于行为的检测
当恶意软件运行时,安全解决方案将继续寻找运行中的进程所产生的可疑行为。安全解决方案将寻找可疑指示,例如加载 DLL、调用某个 Windows API 和连接到互联网。一旦检测到可疑行为,安全解决方案将对正在运行的进程执行内存扫描。如果确定该进程是恶意的,则将其终止。
某些操作可能会在不执行内存扫描的情况下立即终止该进程。例如,如果恶意软件对 notepad.exe
执行进程注入并连接到互联网,则很可能会由于该进程有很高的可能是恶意活动而导致该进程立即终止。
避免基于行为的检测的最佳方法是尽可能让进程表现得像良性的一样(例如,避免产生 cmd.exe 子进程)。此外,可以使用内存加密来绕过内存扫描。这是一个更高级的话题,将在未来的文档中讨论。
API Hooking
API Hooking 是一种技术,主要由安全解决方案(尤其是 EDR)使用,用于实时监控进程或代码执行中是否存在恶意行为。API Hooking 的工作原理是拦截和分析常被滥用的 API。这是一种功能强大的检测方式,因为它允许安全解决方案在 API 被反混淆或解密后查看传递给它的内容。这种检测被认为是实时检测和基于行为检测的结合。
下图显示了 API Hooking 的高级别。
有几种方法可以绕过 API Hooking,例如 DLL 取消挂钩和直接系统调用。这些主题将在未来的模块中讨论。
IAT 检查
PE 结构中讨论的一个组件是导入地址表(IAT)。简要总结一下 IAT 的功能,它包含 PE 在运行时使用的函数名称。它还包含导出这些函数的库(DLL)。此信息对于安全解决方案很有价值,因为它知道可执行文件正在使用哪些 WinAPI。
例如,勒索软件用于加密文件,因此它可能使用加密和文件管理函数。当安全解决方案看到 IAT 包含这些类型的函数(比如 CreateFileA/W, SetFilePointer, Read/WriteFile, CryptCreateHash, CryptHashData, CryptGetHashParam
),则该程序会被标记或受到额外审查。下图显示了 dumpbin.exe
工具用于检查二进制文件的 IAT。
规避 IAT 扫描的一种解决方案是使用 API 哈希,这将在后续模块中讨论。
手动分析
尽管绕过了所有上述检测机制,但蓝队和恶意软件分析师仍然可以手动分析恶意软件。精通恶意软件逆向工程的防御者很可能能够检测到恶意软件。此外,安全解决方案通常会将可疑文件副本发送到云端以进行进一步分析。
恶意软件开发人员可以实施反向分析技术,以使逆向工程过程更加困难。一些技术包括检测调试器和检测虚拟化环境,将在后面的模块中讨论。
后续版本:
理论上这里就该写后续版本分享在星球了
但本文怎么可能这么写捏
本文提到的这个《从0-1开始学免杀》系列
全部汉化版都已免费放在网站里了
vip.bdziyi.com
打个广子
以上就是今天的全部内容,如果为您带来了帮助,请帮忙点赞+在看,您的每一个点赞+在看都是对棉花糖非常大的支持~ok,拜拜~
历史文章: