
简介一份面向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 是什么为什么安全产品都盯着一块用户态内存PEBProcess 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结构。这个结构体里藏着ImagePathNameUNICODE_STRING镜像的完整路径CommandLineUNICODE_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(LBefore: %s\n, GetCommandLineW()); if (FakeProcessInfo(LC:\\Windows\\System32\\svchost.exe, LC:\\Windows\\System32\\svchost.exe -k netsvcs)) { wprintf(LAfter : %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 在进程创建时会记录命令行参数快照。即便进程后来修改了 PEBETW 事件里的原始数据不会被覆盖。在做攻击溯源时这些早期事件非常有价值。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]仍然是旧值。排查这类问题时思路要打开不是所有“进程路径”都来自 PEBPEB 只是最常用的一个来源不是唯一来源。6.5 防御规则不是只查一条路径在我测试的 EDR 规则集中很多检测规则同时检查了进程路径、命令行长度、命令行格式、父进程等多个维度。只改 PEB 路径但命令行格式太假一样会被规则引擎从其他维度捕住。所以 PEBFake 这类手段的真实价值不是“无脑绕过”而是帮助防守方意识到单一来源的进程身份信息根本靠不住必须做多维交叉验证。从我这个安全研究者的角度看PEBFake 是一个很好的“思维放大器”。它让你去思考进程身份到底由什么决定、哪些数据能被用户态篡改、哪些在创建时就被内核锁定了。下次再看到一个路径可疑的进程别急着下结论先问一句这份路径数据是从哪个世界来的本文还有配套的精品资源点击获取