模块 11 - Windows 进程
恶意软件开发课程 - Windows 进程
模块 11 - Windows 进程#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
Windows 进程#
什么是 Windows 进程?#
Windows 进程是在 Windows 机器上运行的程序或应用程序。进程可以由用户或系统本身启动。进程消耗内存、磁盘空间和处理器时间等资源来完成任务。
💡 初学者提示:理解Windows进程
进程就像一个”运行中的程序”:
plaintext程序 (Program) 进程 (Process) ├── exe文件在磁盘上 ├── 程序在内存中运行 ├── 静态的代码和数据 ├── 动态的执行状态 └── 双击启动 └── 有独立的内存空间实际例子:
plaintextnotepad.exe (磁盘上的程序) ↓ 双击启动 notepad.exe进程 (PID: 1234) ├── 内存空间 ├── 线程 ├── 打开的文件 └── 系统资源在任务管理器中:
- 每一行 = 一个进程
- PID = 进程ID(唯一标识)
- 内存 = 进程占用的RAM
- CPU = 处理器使用率
进程线程#
Windows 进程由一个或多个同时运行的线程组成。线程是一组可以在进程内独立执行的指令。进程内的线程可以通信和共享数据。线程由操作系统调度执行并在进程的上下文中管理。
📚 知识扩展:进程 vs 线程
进程和线程的关系:
plaintext进程 (Process) ├── 线程1 (主线程) ├── 线程2 ├── 线程3 └── 线程4 就像: 公司 (进程) ├── 员工1 (线程) ├── 员工2 (线程) └── 员工3 (线程)对比表:
特性 进程 (Process) 线程 (Thread) 定义 运行中的程序 进程内的执行单元 内存 独立地址空间 共享进程内存 创建开销 大(几MB) 小(几KB) 通信 IPC(进程间通信) 直接共享数据 崩溃影响 只影响自己 可能导致整个进程崩溃 PID 有唯一PID 有唯一TID(线程ID) 为什么需要多线程?
c// 单线程程序 while (true) { ProcessUIEvents(); // 处理界面 DownloadFile(); // 下载文件 - 阻塞! UpdateDisplay(); // 更新显示 - 等待下载完成 } // 界面会冻结,用户体验差 // 多线程程序 Thread1: Thread2: while (true) { while (true) { ProcessUIEvents(); DownloadFile(); UpdateDisplay(); } } // 界面响应流畅!在恶意软件中的应用:
- 创建线程执行 Payload
- 注入线程到其他进程
- 使用多线程避免主线程被阻塞
进程内存#
Windows 进程还使用内存来存储数据和指令。当进程创建时,会为其分配内存,分配的数量可以由进程本身设置。操作系统使用虚拟内存和物理内存来管理内存。虚拟内存允许操作系统通过创建一个可以被应用程序访问的虚拟地址空间来使用比物理可用内存更多的内存。这些虚拟地址空间被划分为”页 (pages)“,然后分配给进程。
内存类型#
进程可以有不同类型的内存:
- 私有内存 (Private memory) 专用于单个进程,不能被其他进程共享。这种类型的内存用于存储特定于进程的数据。
- 映射内存 (Mapped memory) 可以在两个或多个进程之间共享。它用于在进程之间共享数据,例如共享库、共享内存段和共享文件。映射内存对其他进程可见,但受保护不被其他进程修改。
- 镜像内存 (Image memory) 包含可执行文件的代码和数据。它用于存储进程使用的代码和数据,例如程序的代码、数据和资源。镜像内存通常与加载到进程地址空间的 DLL 文件相关。
💡 初学者提示:三种内存类型详解
1. Private Memory(私有内存):
plaintext进程A 进程B ├── 私有内存 (5MB) ├── 私有内存 (3MB) │ ├── 栈 │ ├── 栈 │ ├── 堆 │ ├── 堆 │ └── 局部变量 │ └── 局部变量 └── 互不可见 └── 互不可见2. Mapped Memory(映射内存):
plaintext进程A 进程B └─> 共享内存区域 <─┘ (可以互相看到和通信) 用途: - 进程间通信 (IPC) - 共享配置数据 - 共享DLL代码3. Image Memory(镜像内存):
plaintextnotepad.exe 进程 ├── notepad.exe 的代码段 (Image) ├── kernel32.dll (Image) ├── user32.dll (Image) └── ntdll.dll (Image) 每个DLL/EXE = 一个镜像Process Hacker 中查看:
plaintext打开 Process Hacker → 选择进程 → Memory → Type列: - Private (私有内存) - Mapped (映射文件/共享内存) - Image (DLL/EXE代码)
进程环境块 (PEB)#
进程环境块 (PEB - Process Environment Block) 是 Windows 中的一个数据结构,包含有关进程的信息,例如其参数、启动信息、分配的堆信息和加载的 DLL,以及其他信息。它由操作系统使用,在进程运行时存储有关进程的信息,并由 Windows 加载器用于启动应用程序。它还存储有关进程的信息,例如进程 ID (PID) 和可执行文件的路径。
每个创建的进程都有自己的 PEB 数据结构,其中包含有关它的一组信息。
📚 知识扩展:PEB的重要性
PEB 是什么?
plaintextPEB = Process Environment Block = 进程的"信息中心" 包含: - 加载了哪些DLL? - 命令行参数是什么? - 程序路径在哪? - 是否被调试? - 堆信息为什么恶意软件开发要了解PEB?
1. 反调试:
c// PEB->BeingDebugged 标志 if (PEB->BeingDebugged) { ExitProcess(0); // 检测到调试器! }2. 枚举加载的DLL:
c// 通过 PEB->Ldr 遍历所有DLL // 找到 kernel32.dll 的基地址 // 解析导出表获取API地址 // → API Hashing 技术的基础3. 命令行欺骗:
c// 修改 PEB->ProcessParameters->CommandLine // 让Process Hacker显示假的命令行4. 隐藏DLL:
c// 从 PEB->Ldr->InMemoryOrderModuleList 中移除DLL // 让工具看不到恶意DLL
PEB 结构#
C 语言中的 PEB 结构体如下所示。可以忽略此结构体的保留成员。
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;c下面解释非保留成员。
BeingDebugged#
BeingDebugged 是 PEB 结构中的一个标志,指示进程是否正在被调试。当进程正在被调试时,它被设置为 1 (TRUE),当它没有被调试时,它被设置为 0 (FALSE)。它由 Windows 加载器使用,以确定是否在附加调试器的情况下启动应用程序。
💡 初学者提示:BeingDebugged 标志
这是最简单的反调试技术:
c#include <Windows.h> // 方法1:使用 IsDebuggerPresent() API if (IsDebuggerPresent()) { printf("检测到调试器!\n"); ExitProcess(-1); } // 方法2:直接读取 PEB->BeingDebugged BOOL IsDebugged() { PPEB pPeb = (PPEB)__readgsqword(0x60); // 64位 // PPEB pPeb = (PPEB)__readfsdword(0x30); // 32位 return pPeb->BeingDebugged; } if (IsDebugged()) { ExitProcess(-1); }如何绕过(作为分析师)?
plaintext在调试器中: 1. 断点在检查之前 2. 手动设置 PEB->BeingDebugged = 0 3. 或使用插件自动清零更高级的反调试:
- 检查调试器进程(ollydbg.exe, x64dbg.exe)
- 检查调试端口(NtQueryInformationProcess)
- 检查硬件断点(DR0-DR7寄存器)
- 时间检测(执行时间过长)
Ldr#
Ldr 是指向 PEB 进程环境块中 PEB_LDR_DATA 结构的指针。此结构包含有关进程加载的动态链接库 (DLL) 模块的信息。它包括进程中加载的 DLL 列表、每个 DLL 的基地址以及每个模块的大小。它由 Windows 加载器使用,以跟踪进程中加载的 DLL。PEB_LDR_DATA 结构体如下所示。
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;cLdr 可用于查找特定 DLL 的基地址,以及其内存空间中驻留哪些函数。这将在未来的模块中使用,以构建自定义版本的 GetModuleHandleA/W ↗,以增加隐蔽性。
📚 知识扩展:Ldr 和模块列表
PEB->Ldr 包含三个链表:
cPEB->Ldr->InLoadOrderModuleList // 按加载顺序 PEB->Ldr->InMemoryOrderModuleList // 按内存顺序 PEB->Ldr->InInitializationOrderModuleList // 按初始化顺序为什么要遍历模块列表?
避免调用 GetModuleHandle:
c// ❌ 正常方法 - 容易被Hook HMODULE hKernel32 = GetModuleHandleA("kernel32.dll"); // ✅ 通过PEB遍历 - 绕过Hook HMODULE hKernel32 = GetModuleHandleViaPEB(L"kernel32.dll");实现示例:
c#include <Windows.h> #include <winternl.h> HMODULE GetModuleHandleViaPEB(LPCWSTR moduleName) { // 获取PEB PPEB pPeb; #ifdef _WIN64 pPeb = (PPEB)__readgsqword(0x60); #else pPeb = (PPEB)__readfsdword(0x30); #endif // 获取模块列表 PPEB_LDR_DATA pLdr = pPeb->Ldr; PLIST_ENTRY pListHead = &pLdr->InMemoryOrderModuleList; PLIST_ENTRY pListEntry = pListHead->Flink; // 遍历所有模块 while (pListEntry != pListHead) { PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD( pListEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); // 比较模块名 if (_wcsicmp(pEntry->BaseDllName.Buffer, moduleName) == 0) { return (HMODULE)pEntry->DllBase; // 找到! } pListEntry = pListEntry->Flink; } return NULL; // 未找到 } // 使用: HMODULE hKernel32 = GetModuleHandleViaPEB(L"kernel32.dll"); printf("kernel32 基地址: 0x%p\n", hKernel32);优势:
- 不调用 GetModuleHandle(避免IAT/Hook)
- 直接访问底层数据结构
- 可以隐藏/取消链接DLL
ProcessParameters#
ProcessParameters 是 PEB 中的一个数据结构。它包含创建进程时传递给进程的命令行参数。Windows 加载器将这些参数添加到进程的 PEB 结构。ProcessParameters 是指向 RTL_USER_PROCESS_PARAMETERS 结构体的指针,如下所示。
typedef struct _RTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;cProcessParameters 将在未来的模块中用于执行诸如命令行欺骗之类的操作。
💡 初学者提示:命令行欺骗
ProcessParameters 包含什么?
cProcessParameters->ImagePathName // 例如:C:\Windows\System32\notepad.exe ProcessParameters->CommandLine // 例如:notepad.exe C:\test.txt命令行欺骗技术:
plaintext创建进程时: 1. CreateProcess("cmd.exe /c harmless.bat") // 看起来无害 2. 进程创建后立即修改 PEB->ProcessParameters->CommandLine 3. 改为:"cmd.exe /c malicious.bat" 4. 恢复原始命令行并执行 Process Hacker 中显示: "cmd.exe /c harmless.bat" (欺骗成功!)为什么有用?
- 绕过命令行监控
- EDR/AV 只看到假的命令行
- 隐藏真实的恶意参数
AtlThunkSListPtr 和 AtlThunkSListPtr32#
AtlThunkSListPtr 和 AtlThunkSListPtr32 由 ATL (Active Template Library) 模块使用,用于存储指向thunking 函数链表的指针。Thunking 函数用于调用在不同地址空间中实现的函数,这些函数通常代表从 DLL(动态链接库)文件导出的函数。ATL 模块使用 thunking 函数的链表来管理 thunking 过程。
PostProcessInitRoutine#
PEB 结构中的 PostProcessInitRoutine 字段用于存储指向一个函数的指针,该函数在进程中所有线程的 TLS(线程本地存储)初始化完成后由操作系统调用。此函数可用于执行进程所需的任何其他初始化任务。
TLS 和 TLS 回调将在需要时稍后详细讨论。
SessionId#
PEB 中的 SessionID 是分配给单个会话的唯一标识符。它用于跟踪会话期间用户的活动。
线程环境块 (TEB)#
线程环境块 (TEB - Thread Environment Block) 是 Windows 中的一个数据结构,用于存储有关线程的信息。它包含线程的环境、安全上下文和其他相关信息。它存储在线程的栈中,并由 Windows 内核用于管理线程。
TEB 结构#
C 语言中的 TEB 结构体如下所示。可以忽略此结构体的保留成员。
typedef struct _TEB {
PVOID Reserved1[12];
PPEB ProcessEnvironmentBlock;
PVOID Reserved2[399];
BYTE Reserved3[1952];
PVOID TlsSlots[64];
BYTE Reserved4[8];
PVOID Reserved5[26];
PVOID ReservedForOle;
PVOID Reserved6[4];
PVOID TlsExpansionSlots;
} TEB, *PTEB;cProcessEnvironmentBlock (PEB)#
是指向上面解释的 PEB 结构的指针,PEB 位于线程环境块 (TEB) 内,用于存储有关当前运行进程的信息。
💡 初学者提示:通过TEB访问PEB
如何获取PEB?
c// 64位系统 PPEB pPeb = (PPEB)__readgsqword(0x60); // 32位系统 PPEB pPeb = (PPEB)__readfsdword(0x30); // 解释: // GS/FS 寄存器 → 指向TEB // TEB+0x60/0x30 → PEB指针为什么从TEB获取?
- TEB总是可以通过段寄存器访问
- 不需要调用任何API
- 完全隐蔽的方式
TlsSlots#
TLS(线程本地存储)Slots 是 TEB 中用于存储线程特定数据的位置。Windows 中的每个线程都有自己的 TEB,每个 TEB 都有一组 TLS Slots。应用程序可以使用这些 Slots 来存储特定于该线程的数据,例如线程特定变量、线程特定句柄、线程特定状态等。
TlsExpansionSlots#
TEB 中的 TLS Expansion Slots 是一组指针,用于存储线程的线程本地存储数据。TLS Expansion Slots 保留供系统 DLL 使用。
进程和线程句柄#
在 Windows 操作系统上,每个进程都有一个不同的进程标识符或进程 ID (PID),操作系统在创建进程时分配该 ID。PID 用于区分一个正在运行的进程与另一个进程。同样的概念适用于正在运行的线程,其中正在运行的线程具有唯一的 ID,用于将其与系统上现有的其他线程(在任何进程中)区分开来。
这些标识符可用于使用以下 WinAPI 打开进程或线程的句柄。
- OpenProcess ↗ - 通过其标识符打开现有进程对象句柄。
- OpenThread ↗ - 通过其标识符打开现有线程对象句柄。
这些 WinAPI 将在需要时稍后详细讨论。现在,知道打开的句柄可用于对其相关的 Windows 对象执行进一步操作就足够了,例如挂起进程或线程。
句柄应始终在不再需要时关闭,以避免句柄泄漏 ↗。这通过 CloseHandle ↗ WinAPI 调用实现。
📚 知识扩展:进程/线程句柄操作
PID vs HANDLE:
plaintextPID (进程ID) HANDLE (句柄) ├── 只是一个数字 ├── 代表访问权限的"票据" ├── 1234, 5678... ├── 0x000001A4, 0x000002B8... ├── 公开可见 ├── 进程特定 └── 需要转换成句柄才能操作 └── 可以直接操作对象完整的操作流程:
c#include <Windows.h> #include <TlHelp32.h> #include <stdio.h> int main() { // 1. 查找目标进程PID(例如notepad.exe) DWORD targetPID = 0; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe32; pe32.dwSize = sizeof(PROCESSENTRY32); if (Process32First(hSnapshot, &pe32)) { do { if (strcmp(pe32.szExeFile, "notepad.exe") == 0) { targetPID = pe32.th32ProcessID; printf("[+] 找到notepad.exe,PID: %d\n", targetPID); break; } } while (Process32Next(hSnapshot, &pe32)); } CloseHandle(hSnapshot); if (targetPID == 0) { printf("[-] 未找到notepad.exe\n"); return -1; } // 2. 打开进程句柄 HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS, // 请求所有权限 FALSE, // 不继承句柄 targetPID // 目标PID ); if (hProcess == NULL) { printf("[-] OpenProcess失败:%d\n", GetLastError()); return -1; } printf("[+] 进程句柄:0x%p\n", hProcess); // 3. 使用句柄执行操作 // 示例A:挂起进程 // NtSuspendProcess(hProcess); // 示例B:读取进程内存 char buffer[100]; SIZE_T bytesRead; ReadProcessMemory(hProcess, (PVOID)0x12340000, buffer, 100, &bytesRead); // 示例C:分配内存 PVOID remoteAddr = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_READWRITE); // 4. 关闭句柄(重要!) CloseHandle(hProcess); printf("[+] 句柄已关闭\n"); return 0; }常用的访问权限:
cPROCESS_ALL_ACCESS // 所有权限 PROCESS_VM_READ // 读取内存 PROCESS_VM_WRITE // 写入内存 PROCESS_VM_OPERATION // 内存操作(Alloc/Protect) PROCESS_CREATE_THREAD // 创建线程 PROCESS_QUERY_INFORMATION // 查询信息 PROCESS_TERMINATE // 终止进程句柄泄漏的危害:
c// ❌ 错误示例 void LeakyFunction() { HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 1234); // ... 使用句柄 // 忘记 CloseHandle(h)! } // 句柄泄漏! // 调用10000次后,系统句柄耗尽 for (int i = 0; i < 10000; i++) { LeakyFunction(); }
🎯 本模块学习要点
必须掌握的概念:
- ✅ 进程 vs 线程的区别
- ✅ 三种内存类型(Private、Mapped、Image)
- ✅ PEB 结构和重要成员
- ✅ TEB 结构
- ✅ 如何通过TEB访问PEB
- ✅ PID vs HANDLE
- ✅ 句柄的正确使用和关闭
PEB的关键成员:
成员 用途 恶意软件应用 BeingDebugged调试标志 反调试 Ldr模块列表 枚举DLL、API Hashing ProcessParameters命令行参数 命令行欺骗 实用技巧:
c// 获取PEB PPEB pPeb = (PPEB)__readgsqword(0x60); // 64位 // 反调试 if (pPeb->BeingDebugged) ExitProcess(-1); // 遍历模块 PPEB_LDR_DATA pLdr = pPeb->Ldr; // 枚举所有加载的DLL... // 获取命令行 PUNICODE_STRING cmdLine = &pPeb->ProcessParameters->CommandLine;下一步: 学习未文档化的结构和高级技巧