简介:一份面向Windows安全研究与逆向工程学习者的C++工具资源,围绕进程环境块(PEB)的结构修改,演示如何伪装当前进程的ImagePath、进程名及相关参数,帮助读者理解用户态与内核态之间的信息交互及安全软件检测进程身份的基本原理。资源包为zip格式,共3个文件,以C++源码为核心,配有一份md说明文档和一张png示意图,总计52KB,便于对照源码阅读实现思路与运行效果。已有759人学习下载,适合对PEB机制、进程隐藏技术和Windows API调用有一定基础,并希望进行合规实验的中高级开发者。通过源码中的句柄获取、PEB地址解析、内存属性修改等关键步骤,读者可以掌握在安全研究场景下分析进程伪装手段的完整路径,同时了解此类技术可能带来的系统稳定性与法律合规风险。 做安全研究的朋友多半都遇到过这种场景:任务管理器里明明显示进程路径是C:\Windows\System32\svchost.exe -k netsvcs,怎么看都挺正常,但懂行的人扫了一眼就说是“装的”。以前我也觉得这是玄学,直到自己动手去翻进程的PEB,才发现这里面的门道比想象中深得多。PEBFake这类工具的核心思路,就是把进程的“户口本”直接改了——你看到的路径、命令行参数,全是伪造出来的。
这篇文章我想从 PEB 的结构聊起,把 PEBFake 修改进程路径和参数的原理、实操细节、以及为什么只改 PEB 远远不够,完整过一遍。无论你是做恶意样本分析、EDR 规则评估,还是红队对抗,这都能帮你少踩几个坑。
1. 先搞清楚 PEB 是什么?为什么安全产品都盯着一块用户态内存
PEB(Process Environment Block,进程环境块)是 Windows 为每个进程维护的核心数据结构。它放在用户态地址空间里,记录着这个进程的镜像路径、命令行参数、环境变量、当前目录、已加载模块链表、堆地址等重要信息。每个进程都有自己独立的 PEB,从系统层面来看,它就像这个进程的“户籍档案”。
访问 PEB 在用户态其实非常方便。x64 系统下,通过gs:[0x60]可以拿到 PEB 地址,x86 则是fs:[0x30]。因为 PEB 本身是用户态结构,进程自己就有权限读写,不需要任何特权。这意味着,只要你想,完全可以手动改掉 PEB 里记录的进程路径和命令行,这就是 PEBFake 这类工具能够存在的底层基础。
那安全产品为什么也盯着这块内存?因为大多数 EDR、HIDS 在判断进程是否可疑时,第一步就是取进程路径和命令行参数。比如检测powershell.exe -enc ...、rundll32.exe执行可疑脚本、或者横向移动中常见的wmic process call create。如果进程路径、参数能随便被修改,那许多基于字符串匹配的检测规则都会被绕过。所以理解 PEB 的伪造方式,本质上也是理解安全产品检测盲区的过程。
1.1 定位 PEB:不同位数的进程有不同走法
64 位进程取 PEB 最常见的写法是:
PPEB GetPeb() { #if defined(_M_X64) return (PPEB)__readgsqword(0x60); #elif defined(_M_IX86) return (PPEB)__readfsdword(0x30); #endif }而用 Windbg 分析进程时,!peb命令直接把 PEB 内容打印出来,非常直观。需要注意的是,32 位进程在 64 位系统上跑会被 WoW64 转换层处理,访问路径略有不同,做样本分析时得区分清楚。
1.2 与路径参数有关的核心字段
拿_PEB结构体来说,跟路径参数最密切的指针是ProcessParameters,它指向RTL_USER_PROCESS_PARAMETERS结构。这个结构体里藏着:
ImagePathName(UNICODE_STRING):镜像的完整路径CommandLine(UNICODE_STRING):启动命令行CurrentDirectory:当前目录Environment:环境变量块
PEBFake 要伪装进程路径和参数,瞄准的主要就是ImagePathName和CommandLine这两个字段。
2. PEBFake 的“Fake”到底改了哪里:核心结构的逐字段拆解
要理解 PEBFake 做了什么,先要理解UNICODE_STRING的布局。它不是一个简单的字符串指针,而是一个带长度信息的三段式结构:
typedef struct _UNICODE_STRING { USHORT Length; // 当前字符串字节长度(不含结尾符) USHORT MaximumLength; // Buffer 分配的字节容量 PWSTR Buffer; // 指向宽字符串缓冲区 } UNICODE_STRING;很多初学的人只盯着Buffer改字符串内容,结果发现 API 读出来是乱码或者空值,问题往往出在Length和MaximumLength没有同步更新。修改UNICODE_STRING时,这三个字段必须是自洽的,否则系统函数在解析时会读越界或读不到完整内容。
2.1 修改路径和参数的最小操作集
假设我们要把当前进程伪装成C:\Windows\System32\svchost.exe -k netsvcs,至少要处理这些地方:
ProcessParameters->ImagePathName.Buffer:指向伪造路径ProcessParameters->ImagePathName.Length:更新为新路径字节长度ProcessParameters->ImagePathName.MaximumLength:更新为新缓冲区容量ProcessParameters->CommandLine:同样处理命令行部分
改完这些之后,调用GetModuleFileNameW和GetCommandLineW都会返回伪造内容。但这只是表面工作,距离“真正像”还差不少。
2.2 保存原始字符串再覆盖,还是直接重定向 Buffer
实际实现时有两条路线:原地覆盖或者换 Buffer。
原地覆盖是把伪造字符串直接拷贝到原来的 Buffer 里。优点是修改后其他模块持有的指针引用不会变化,但受限于原 Buffer 大小,只适合伪造更短或等长的字符串,而且 Windows 10 之后,部分系统组件会保留一份启动时的原始副本用于诊断,原地覆盖并不能保证所有展示位置都被改写。
换 Buffer是先用VirtualAlloc或RtlAllocateHeap新分配一块内存,把伪造字符串写进去,再让Buffer指针指向新内存。这个方法更灵活,但需要小心内存生命周期管理,防止二次释放或者泄露。
PEBFake 这类工具大多采用换 Buffer 的方式,因为它适配不同长度的伪造内容,通用性更好。下面我在实操部分用代码把完整流程过一遍。
3. 实操:手写一个最小可运行的 PEB 伪装样本
这里我用 C/C++ 写一个最小 Demo,运行后会把当前进程的路径和命令行改成指定的伪造内容。为了让更多人能直接跑,我也会给一个 Python ctypes 版本。
3.1 C/C++ 实现:直接操作 PEB 的 ProcessParameters
#include <windows.h> #include <winternl.h> #include <intrin.h> #include <stdio.h> // 兼容旧版 SDK 缺省定义 #ifndef ProcessParametersOffset #define ProcessParametersOffset 0x20 // x64 PEB->ProcessParameters 偏移 #endif PPEB GetPeb() { #if defined(_M_X64) return (PPEB)__readgsqword(0x60); #else return (PPEB)__readfsdword(0x30); #endif } BOOL FakeProcessInfo(PCWSTR fakePath, PCWSTR fakeCmdLine) { PPEB peb = GetPeb(); if (!peb || !peb->ProcessParameters) { return FALSE; } PRTL_USER_PROCESS_PARAMETERS params = peb->ProcessParameters; // 构造新的 UNICODE_STRING UNICODE_STRING newPath; RtlInitUnicodeString(&newPath, fakePath); UNICODE_STRING newCmd; RtlInitUnicodeString(&newCmd, fakeCmdLine); // 为新字符串分配内存,并替换 Buffer PWSTR pathBuf = (PWSTR)VirtualAlloc(NULL, newPath.MaximumLength, MEM_COMMIT, PAGE_READWRITE); PWSTR cmdBuf = (PWSTR)VirtualAlloc(NULL, newCmd.MaximumLength, MEM_COMMIT, PAGE_READWRITE); if (!pathBuf || !cmdBuf) { return FALSE; } memcpy(pathBuf, newPath.Buffer, newPath.Length); memcpy(cmdBuf, newCmd.Buffer, newCmd.Length); params->ImagePathName.Buffer = pathBuf; params->ImagePathName.Length = newPath.Length; params->ImagePathName.MaximumLength = newPath.MaximumLength; params->CommandLine.Buffer = cmdBuf; params->CommandLine.Length = newCmd.Length; params->CommandLine.MaximumLength = newCmd.MaximumLength; return TRUE; } int wmain() { wprintf(L"Before: %s\n", GetCommandLineW()); if (FakeProcessInfo(L"C:\\Windows\\System32\\svchost.exe", L"C:\\Windows\\System32\\svchost.exe -k netsvcs")) { wprintf(L"After : %s\n", GetCommandLineW()); } // 阻止程序立即退出 getchar(); return 0; }编译时注意用 x64 Release,并且链接ntdll.lib(或者运行时动态获取RtlInitUnicodeString)。这个 Demo 只在当前进程内生效,不会影响系统其他进程。
3.2 Python 版本:用 ctypes 也能快速验证
import ctypes from ctypes import wintypes ntdll = ctypes.WinDLL("ntdll") class UNICODE_STRING(ctypes.Structure): _fields_ = [ ("Length", wintypes.USHORT), ("MaximumLength", wintypes.USHORT), ("Buffer", wintypes.LPWSTR), ] # x64 PEB -> ProcessParameters 偏移(不同系统版本一致) PROCESS_PARAMETERS_OFFSET = 0x20 # 读取 gs:[0x60] peb_addr = None # 具体读取方式依赖汇编,这里简化用 NtCurrentPeb(仅示意) # 真实实现可用: # __readgsqword(0x60)Python 里直接读gs段寄存器不方便,通常需要一小段内联汇编或借用ctypes调用NtCurrentTeb再换算。因此更推荐直接用 C/C++ 进行验证,Python 版本仅作为概念演示。
注意:修改 PEB 的代码如果写错,轻则进程崩溃,重则触发系统回收异常,建议先在虚拟机或隔离环境里测试,不要在自己日常用的机器上轻易跑。
3.3 验证效果:从用户态 API 到任务管理器
运行 Demo 后,GetCommandLineW()确实返回了伪造的命令行。但打开任务管理器的“详细信息”标签页,有时候进程路径却仍然显示真实的 exe 路径。这并不奇怪,因为任务管理器部分数据来自系统维护的进程映像路径,而不是当前进程 PEB 里的内容。
这就是 PEBFake 这类工具的一个重要边界:它对用户态读取 API 有效,对内核态信息源不一定有效。验证效果时,不能只看一两个 API 的返回结果,要看具体的数据来源。
4. 只改 PEB 就万事大吉?边界与失效场景分析
PEB 修改之后表面看成功了,但实际对抗场景里,很多安全产品并不会只信用户态 PEB 这一份数据。它们会交叉验证多路信息,一旦发现不一致,反而会提高告警优先级。
4.1 用户态 vs 内核态:信息源不止一处
常见的信息来源包括:
| 数据来源 | 说明 | 是否能被 PEBFake 影响 |
|---|---|---|
| PEB 的 ProcessParameters | 用户态 API 主要读取这里 | 能 |
内核EPROCESS中的 ImageFileName | 任务管理器/性能监视器常用 | 不能,PEBFake 不涉及内核对象 |
| ETW 进程创建事件 | 进程创建早期捕获的原始路径和命令行 | 不能,事件数据在创建时就生成了 |
NtQueryInformationProcess的ProcessImageFileName | 返回内核维护的路径 | 不能 |
| 内存字符串扫描 | 扫描 PEB Buffer、堆、栈、映射内存中的路径特征 | 部分能绕过,但容易因残留副本暴露 |
搞清楚了这些,你就能理解为什么有些 EDR 在遇到“任务管理器显示的路径和 PEB 里的路径不一致”时会直接标记为高危行为。因为正常情况下,用户态展示的信息和内核态信息应该是吻合的,一旦对不上,就是明显的改号痕迹。
4.2 字符串残留:改了 PEB 不等于改了所有副本
修改Buffer指针后,原始字符串内容仍然留存在原缓冲区里。如果 EDR 做全内存扫描,扫描到 PEB 原 Buffer 区域时,会看到“进程当前声称的路径”和“进程残留的旧路径”同时存在。很多内存扫描引擎正是利用这种不一致性来识别进程伪装。
所以 PEBFake 要做到更隐蔽,还得考虑在修改 PEB 的同时擦掉原始字符串残留,或者在更早的进程创建阶段通过自定义启动逻辑来规避痕迹。这些都属于高级对抗范畴,在此不展开。
4.3 父进程链和当前目录也是线索
路径和命令行只是最表面的一层。安全分析还会看父进程是谁、当前目录是否匹配、进程启动时间是否合理、命令行中的参数风格是否自洽。比如你伪装成C:\Windows\System32\svchost.exe -k netsvcs,但父进程却是notepad.exe,明眼人一眼就能看出问题。
PEBFake 通常只负责“改身份”,不负责“编故事”。在真实演练中,路径伪装需要和其他进程行为、父子关系、执行链配合,才能构成完整的“叙事”。
5. 防御方视角:如何发现进程路径/参数被“掉包”
聊完攻击视角,再从防御视角看看怎么发现这类伪装。蓝队同学在排查安全事件时,如果怀疑某个进程的身份是伪造的,可以从几个角度验证:
5.1 内核态路径与用户态路径交叉验证
在管理员权限下用Process Explorer之类的工具,或者直接用 PowerShell 调用Get-CimInstance Win32_Process,拿到的 ExecutablePath 通常来自 WMI 的内核信息。如果这份信息和任务管理器或当前进程内看到的路径不一致,就要高度警惕。
也可以写一段小工具,通过NtQueryInformationProcess的ProcessImageFileName获取内核维护的镜像路径,与当前进程GetModuleFileName的返回结果对比。
5.2 检查 ProcessParameters 附近的内存布局
正常进程的RTL_USER_PROCESS_PARAMETERS结构里,ImagePathName的Buffer通常指向 PEB 附近或进程堆中的一块内存,而且Length与实际字符串内容是对得上的。防御方可以扫描这个结构,检查Buffer指针是否指向了一块异常的高位地址(比如VirtualAlloc分配的散块),同时验证字符串内容与创建时间是否冲突。
5.3 关注 ETW 进程创建事件
Windows 的Microsoft-Windows-Kernel-ProcessETW Provider 在进程创建时会记录命令行参数快照。即便进程后来修改了 PEB,ETW 事件里的原始数据不会被覆盖。在做攻击溯源时,这些早期事件非常有价值。
5.4 检测对 PEB 的异常写操作
在受控环境里,可以给 PEB 相关内存页设置保护(如 PAGE_READONLY),一旦有代码试图修改 PEB 字段,就会触发访问违规。现代 EDR 也常常挂钩NtWriteVirtualMemory或监控关键结构体附近的写入。当然,这种方案误报率不低,更适合做专门的检测规则实验。
6. 实际测试中我踩过的坑与排查思路
最后分享一下我实际使用和复现 PEBFake 类工具时的几个坑,希望后来者少走弯路。
6.1 64 位编译却拿到 32 位 PEB
最开始我图省事,工程编译成了 x86,然后在 64 位系统上跑,结果__readgsqword(0x60)读到的根本不是 64 位 PEB,而是 WoW64 环境下的结构,偏移对不上,Patch 之后 API 全乱。后来统一编译成 x64 才正常。做这类操作前先确认目标进程位数,比什么都重要。
6.2 只改字符串内容、忘记更新 Length
这是最常见的低级错误。直接把Buffer里的内容替换成更长的字符串,但Length没改,结果是 API 读出来的路径被截断,甚至因为MaximumLength不足直接崩溃。记住,UNICODE_STRING的一切读取逻辑都依赖长度字段,改Buffer之前先把长度算清楚。
6.3 新分配内存被释放后成了悬空指针
我在早期版本用malloc分配伪造路径的 Buffer,但代码里没有保存原始指针,也没有管理好释放时机。程序某个分支里把内存释放掉之后,PEB 里的Buffer还指向那块地址,后续读取直接访问违例。如果是做长期驻留的工具,这块内存最好私有管理、不轻易释放。
6.4 GetCommandLine 返回正常,但其他 API 返回旧值
有些系统组件会在进程启动时缓存路径到局部变量或线程本地存储中,修改 PEB 并不能影响这些缓存。比如某些版本的 CRT 运行库会缓存程序名,改完 PEB 后argv[0]仍然是旧值。排查这类问题时,思路要打开:不是所有“进程路径”都来自 PEB,PEB 只是最常用的一个来源,不是唯一来源。
6.5 防御规则不是只查一条路径
在我测试的 EDR 规则集中,很多检测规则同时检查了进程路径、命令行长度、命令行格式、父进程等多个维度。只改 PEB 路径但命令行格式太假,一样会被规则引擎从其他维度捕住。所以 PEBFake 这类手段的真实价值,不是“无脑绕过”,而是帮助防守方意识到:单一来源的进程身份信息根本靠不住,必须做多维交叉验证。
从我这个安全研究者的角度看,PEBFake 是一个很好的“思维放大器”。它让你去思考进程身份到底由什么决定、哪些数据能被用户态篡改、哪些在创建时就被内核锁定了。下次再看到一个路径可疑的进程,别急着下结论,先问一句:这份路径数据,是从哪个世界来的?
本文还有配套的精品资源,点击获取