news 2026/10/15 13:59:58

Windows PEBFake 进程伪装:PEB 结构修改与攻防实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows PEBFake 进程伪装:PEB 结构修改与攻防实战

简介:PEBFake是一份面向Windows安全研究与逆向工程学习者的C++技术资源,聚焦于通过修改PEB(进程环境块)结构来伪装当前进程的ImagePath、进程名等关键参数,帮助读者理解进程信息隐藏的底层原理与实现思路。资源包共3个文件,包含1个cpp源码、1个md说明文档和1个png效果截图,压缩包约52KB,体量轻巧,便于快速阅读与本地编译验证。源码部分展示了获取目标进程句柄、定位PEB地址、修改ImagePath与ProcessName字段以及调整内存保护属性等关键环节,配合说明文档可梳理出完整的实现脉络。目前已有763人学习关注,适合具备一定C++与WinAPI基础、希望深入理解PEB结构与进程伪装技术的安全研究人员参考,也可作为逆向工程与系统安全课程的学习素材。

1. 进程伪装的攻防博弈:PEBFake 到底在改什么

在 Windows 安全对抗和红蓝演练场景里,进程伪装一直是个绕不开的话题。PEBFake 这个方向做的事情,说白了就是在进程已经跑起来之后,动态修改它 PEB(Process Environment Block)里的关键字段,让当前进程在外部工具眼里看起来像是另一个程序。比如你明明跑的是一个自定义的测试工具,但任务管理器、Process Explorer 甚至某些 EDR 的进程枚举接口看到的却是notepad.exe或者svchost.exe,命令行参数也被替换成了无害的内容。

这个技术的核心价值在于对抗基于进程元数据的检测逻辑。很多安全产品在判断进程是否可疑时,第一层过滤就是看进程名、路径和命令行参数。如果这些信息被篡改,后续的行为分析才会介入,这就给攻击方争取了时间窗口。对防守方来说,理解 PEBFake 的实现原理,才能知道怎么检测这种篡改行为。

适合读这篇的人有两类:一是做安全工具开发或红队技术的工程师,想搞清楚 PEB 修改的具体落地方式;二是做终端检测的防守方,想知道这种伪装在内存里留下什么痕迹。下面我会从 PEB 结构讲起,一步步拆解怎么改、改哪里、改完怎么验证,以及实际落地时会踩哪些坑。

2. PEB 结构与进程伪装的关键字段:先搞清楚改哪里

2.1 PEB 在用户态进程里的位置和访问方式

PEB 是 Windows 为每个进程维护的一个用户态数据结构,位于进程地址空间的高地址区域。它包含了进程的映像基址、命令行参数、环境变量、加载模块列表等关键信息。在 x64 系统上,你可以通过gs:[0x60]拿到 PEB 的指针,x86 则是fs:[0x30]。这个结构在进程启动时由内核初始化,之后用户态代码可以通过 NtCurrentTeb() 宏或者直接读段寄存器来访问。

关键点在于:PEB 位于用户态可读写内存区域,这意味着进程自己可以修改它。这跟 EPROCESS 不一样,EPROCESS 在内核态,普通进程碰不到。所以 PEBFake 这类技术的可行性基础就是 PEB 的用户态可写属性。

// 获取当前进程 PEB 指针的常见方式 #ifdef _WIN64 PEB* pPeb = (PEB*)__readgsqword(0x60); #else PEB* pPeb = (PEB*)__readfsdword(0x30); #endif

这段代码直接通过段寄存器偏移读取 PEB 地址,不依赖任何 API,因此在 API 被 hook 的环境下依然有效。参数说明:x64 用__readgsqword,x86 用__readfsdword,偏移值 0x60 和 0x30 是 Windows 内核固定的布局,从 XP 到 Win11 都适用。

2.2 需要修改的三个核心字段:ImagePathName、CommandLine、CurrentDirectory

PEB 里跟进程伪装直接相关的字段有三个,都在RTL_USER_PROCESS_PARAMETERS结构里:

字段含义伪装目标
ImagePathName进程映像的完整路径改成合法系统程序的路径
CommandLine启动命令行参数替换成无害参数
CurrentDirectory当前工作目录配合路径伪装

这三个字段都是UNICODE_STRING类型,包含 Length、MaximumLength 和 Buffer 指针。修改时不能只改 Buffer 内容,还要同步更新 Length,否则某些工具读取时会因为长度不匹配而暴露异常。

typedef struct _RTL_USER_PROCESS_PARAMETERS { ULONG MaximumLength; ULONG Length; ULONG Flags; ULONG DebugFlags; HANDLE ConsoleHandle; ULONG ConsoleFlags; HANDLE StandardInput; HANDLE StandardOutput; HANDLE StandardError; CURDIR CurrentDirectory; UNICODE_STRING DllPath; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine; // ... 后续字段省略 } RTL_USER_PROCESS_PARAMETERS;

这个结构体定义展示了前几个字段的布局。实际修改时,你需要先通过pPeb->ProcessParameters拿到结构体指针,然后定位到 ImagePathName 和 CommandLine 的 UNICODE_STRING。注意 CurrentDirectory 是个 CURDIR 结构,里面嵌套了 DOS 路径和 UNICODE_STRING,改的时候要一起处理。

2.3 为什么只改 PEB 不够:还要同步修改进程堆里的字符串

这里有个血泪经验:PEB 里的 UNICODE_STRING 的 Buffer 指针指向的是进程堆上的一块内存,如果你只把 Buffer 指向一个新的字符串常量,某些工具在读取时会去检查这块内存是否在合法堆区域内。更稳妥的做法是直接在原 Buffer 上覆写内容,然后调整 Length。

但原 Buffer 的长度可能不够放下新的路径。比如原来的路径是C:\test\tool.exe,你想改成C:\Windows\System32\svchost.exe,长度超了。这时候有两个选择:一是分配一块新的堆内存,把新路径写进去,然后让 Buffer 指向新内存;二是找一个足够长的已有字符串区域来覆写。第一种方式更通用,但要注意新分配的内存属性要和原内存一致,否则会被某些检测逻辑发现。

// 覆写 UNICODE_STRING 的通用函数 void FakeUnicodeString(UNICODE_STRING* pStr, const wchar_t* newContent) { SIZE_T newLen = wcslen(newContent) * sizeof(wchar_t); // 如果原缓冲区够大,直接覆写 if (pStr->MaximumLength >= newLen + sizeof(wchar_t)) { memcpy(pStr->Buffer, newContent, newLen); pStr->Length = (USHORT)newLen; pStr->Buffer[newLen / sizeof(wchar_t)] = L'\0'; } else { // 重新分配内存 PVOID newBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, newLen + sizeof(wchar_t)); if (newBuf) { memcpy(newBuf, newContent, newLen); pStr->Buffer = (PWSTR)newBuf; pStr->Length = (USHORT)newLen; pStr->MaximumLength = (USHORT)(newLen + sizeof(wchar_t)); } } }

这个函数先判断原缓冲区是否够用,够就直接覆写,不够就重新分配。参数说明:pStr是要修改的 UNICODE_STRING 指针,newContent是新的宽字符串。注意MaximumLength要包含结尾的 null 字符,Length不包含。重新分配时用GetProcessHeap拿默认堆,不要用malloc,因为 PEB 里的字符串通常来自进程默认堆,混用不同堆管理器会留下痕迹。

3. 动手实现 PEBFake:从读取 PEB 到完成伪装

3.1 最小可用代码框架:读取 PEB 并定位 ProcessParameters

先写一个最小的框架,把 PEB 和 ProcessParameters 的地址打出来,确认能正确读取。

#include <windows.h> #include <winternl.h> #include <stdio.h> int wmain() { // 获取 PEB 指针 #ifdef _WIN64 PEB* pPeb = (PEB*)__readgsqword(0x60); #else PEB* pPeb = (PEB*)__readfsdword(0x30); #endif if (!pPeb) { wprintf(L"[-] Failed to get PEB pointer\n"); return 1; } // 获取 ProcessParameters RTL_USER_PROCESS_PARAMETERS* pParams = (RTL_USER_PROCESS_PARAMETERS*)pPeb->ProcessParameters; if (!pParams) { wprintf(L"[-] ProcessParameters is null\n"); return 1; } wprintf(L"[+] PEB: %p\n", pPeb); wprintf(L"[+] ProcessParameters: %p\n", pParams); wprintf(L"[+] ImagePathName: %wZ\n", &pParams->ImagePathName); wprintf(L"[+] CommandLine: %wZ\n", &pParams->CommandLine); return 0; }

这段代码先通过段寄存器拿到 PEB,然后从 PEB 里读出 ProcessParameters 指针,最后打印 ImagePathName 和 CommandLine。%wZ是 Windows 内核调试常用的格式化符,能直接打印 UNICODE_STRING。编译时记得链接ntdll.lib,因为RTL_USER_PROCESS_PARAMETERS的定义在winternl.h里可能不完整,需要自己补全或者从 ntdll 的符号里拿。

3.2 修改 ImagePathName 和 CommandLine 的完整步骤

有了上面的框架,修改就简单了。核心逻辑是:拿到 UNICODE_STRING,调用前面写的 FakeUnicodeString,然后验证。

void DoPEBFake() { #ifdef _WIN64 PEB* pPeb = (PEB*)__readgsqword(0x60); #else PEB* pPeb = (PEB*)__readfsdword(0x30); #endif RTL_USER_PROCESS_PARAMETERS* pParams = (RTL_USER_PROCESS_PARAMETERS*)pPeb->ProcessParameters; // 伪装成 svchost.exe const wchar_t* fakePath = L"C:\\Windows\\System32\\svchost.exe"; const wchar_t* fakeCmd = L"C:\\Windows\\System32\\svchost.exe -k netsvcs -p"; FakeUnicodeString(&pParams->ImagePathName, fakePath); FakeUnicodeString(&pParams->CommandLine, fakeCmd); // 同步修改 CurrentDirectory const wchar_t* fakeDir = L"C:\\Windows\\System32\\"; // CurrentDirectory 的处理略复杂,需要同时改 DosPath 和 UNICODE_STRING // 这里先只改 UNICODE_STRING 部分 FakeUnicodeString(&pParams->CurrentDirectory.DosPath, fakeDir); }

这段代码把 ImagePathName 改成了 svchost 的路径,CommandLine 也替换成了对应的参数。注意 CurrentDirectory 的 DosPath 是个 UNICODE_STRING,可以直接用同一个函数处理。但 CurrentDirectory 里还有个 HANDLE 指向目录句柄,那个改不了,不过大多数工具不会去检查句柄对应的实际路径。

改完之后,用 Process Explorer 或者任务管理器看,进程名应该已经变成 svchost.exe 了。但这里有个坑:任务管理器显示的进程名其实是从 EPROCESS 的 ImageFileName 来的,那个在内核态,改不了。所以任务管理器可能还是显示原始名字。真正能骗过的是那些通过 PEB 读取进程信息的工具,比如某些 EDR 的用户态枚举接口。

3.3 验证伪装效果:用 API 和工具交叉检查

改完之后怎么知道有没有生效?最直接的方法是用GetModuleFileName和GetCommandLine这两个 API 读一下。

// 验证伪装效果 wchar_t modulePath[MAX_PATH] = {0}; GetModuleFileNameW(NULL, modulePath, MAX_PATH); wprintf(L"[+] GetModuleFileName: %s\n", modulePath); LPWSTR cmdLine = GetCommandLineW(); wprintf(L"[+] GetCommandLine: %s\n", cmdLine);

GetModuleFileName内部会去读 PEB 的 ImagePathName,所以改完之后它返回的应该是伪装路径。GetCommandLine读的是 PEB 的 CommandLine,同样会返回伪装后的内容。如果这两个 API 返回的还是原始值,说明修改没生效,检查一下是不是改错了字段或者 Length 没同步。

另外可以用wmic process get name,commandline或者 PowerShell 的Get-Process来交叉验证。注意不同工具读取进程信息的来源不一样:有的走 PEB,有的走内核查询,有的走 WMI。PEBFake 只能骗过走 PEB 的那部分工具,走内核查询的照样能看到真实信息。

4. 避坑与排查:PEBFake 落地时最容易翻车的五个点

4.1 修改后进程崩溃:Length 和 MaximumLength 不匹配

现象:改完 PEB 之后进程直接崩了,或者调用某些 API 时返回异常。

原因:UNICODE_STRING 的 Length 和 MaximumLength 没同步更新。Length 是当前字符串的实际字节数(不含结尾 null),MaximumLength 是缓冲区总大小。如果只改了 Buffer 内容没改 Length,后续代码按旧 Length 读取时会读到残留数据;如果 Length 大于 MaximumLength,某些校验逻辑会直接报错。

解决:每次修改都严格按Length = wcslen(newContent) * sizeof(wchar_t)和MaximumLength = Length + sizeof(wchar_t)来设置。改完后用wprintf(L"%wZ", pStr)打印一下确认。

4.2 伪装后部分工具仍显示原始名称:数据来源不同

现象:Process Explorer 显示的是伪装后的名字,但任务管理器还是原始名字。

原因:任务管理器读的是内核态 EPROCESS 的 ImageFileName,这个字段在进程创建时就固定了,用户态改不了。Process Explorer 默认也读内核数据,但可以配置成读 PEB。

解决:接受这个限制。PEBFake 的目标不是骗过所有工具,而是骗过那些依赖 PEB 的检测逻辑。如果非要让任务管理器也显示伪装名,那需要内核驱动级别的修改,超出了用户态技术的范围。

4.3 重新分配内存后行为异常:堆属性不一致

现象:用 HeapAlloc 分配新缓冲区后,进程运行一段时间出现访问违例。

原因:PEB 里的字符串原本可能来自进程启动时的初始堆,属性是只读或者可执行。你用 HeapAlloc 分配的新内存属性是默认可读写,某些校验逻辑会检查内存页属性,发现不一致就触发异常。

解决:尽量在原缓冲区上覆写,避免重新分配。如果必须分配,用VirtualAlloc指定PAGE_READWRITE属性,并且把内存放在和原缓冲区相近的地址区域。更稳妥的做法是提前在进程启动参数里预留足够长的路径,这样就不用重新分配了。

4.4 修改 CurrentDirectory 后文件操作失败

现象:改完 CurrentDirectory 后,进程里用相对路径打开文件全部失败。

原因:CurrentDirectory 不仅影响 PEB 里的显示,还影响内核里进程的当前目录对象。你只改了 PEB 里的字符串,内核里的目录句柄还是指向原来的路径,两者不一致导致相对路径解析混乱。

解决:如果不需要伪装当前目录,就别改这个字段。如果非要改,用SetCurrentDirectoryW先切换内核目录,再改 PEB 里的字符串,保持两者同步。但注意SetCurrentDirectoryW会触发内核调用,可能被某些监控捕获。

4.5 伪装后的进程被 EDR 标记:行为特征没跟上

现象:PEB 改得很完美,但 EDR 还是报警了。

原因:EDR 不只看进程名和路径,还会看进程的加载模块、网络连接、注册表操作等行为。你伪装成 svchost,但 svchost 不会去读某个特定目录下的配置文件,这个行为差异直接暴露了。

解决:PEBFake 只是伪装的第一步,后续的行为也要配合。比如伪装成 svchost 后,网络连接的目标端口、访问的注册表键都要尽量贴近真实 svchost 的行为模式。这属于更高阶的对抗,单靠 PEB 修改解决不了。

5. 进阶技巧:让 PEBFake 更隐蔽的两个细节

5.1 处理 PEB 里的 ProcessParameters 副本问题

Windows 在某些版本里会在 PEB 之外再存一份 ProcessParameters 的副本,比如在RTL_USER_PROCESS_PARAMETERS的扩展区域或者线程环境块里。如果你只改了 PEB 指向的那一份,某些 API 可能会去读副本,导致伪装失效。我一般会先用NtQueryInformationProcess配合ProcessBasicInformation拿到 PEB 地址,然后对比pPeb->ProcessParameters和通过其他途径拿到的指针是否一致。如果不一致,说明有副本,需要一并修改。

具体做法是遍历进程的堆内存,搜索原始路径字符串的出现位置,把所有匹配的地方都覆写掉。这个操作比较暴力,但能覆盖大多数副本场景。搜索时注意区分大小写和路径分隔符的变体,C:\Windows和C:/Windows都要考虑。

5.2 伪装后的自检清单

每次做完 PEBFake,我会跑一遍下面这个自检清单,确认没有遗漏:

检查项方法预期结果
ImagePathNameGetModuleFileNameW返回伪装路径
CommandLineGetCommandLineW返回伪装参数
CurrentDirectoryGetCurrentDirectoryW返回伪装目录
环境变量GetEnvironmentVariableW关键变量无原始路径残留
加载模块EnumProcessModules无异常模块路径

这个清单能覆盖大部分用户态可检测的点。如果全部通过,说明 PEB 层面的伪装已经做到位了。剩下的就是行为层面的对抗,那需要另外一套思路。

5.3 一个容易被忽略的细节:PEB 的 BeingDebugged 标志

PEB 里有个BeingDebugged字段,偏移 0x02。有些检测逻辑会读这个字段判断进程是否被调试。如果你在做 PEBFake 的同时还在调试进程,这个字段会是 1,直接暴露。我一般会在修改路径之前先把它置 0,避免被顺带检测到。

// 清除 BeingDebugged 标志 pPeb->BeingDebugged = 0;

这行代码很简单,但效果立竿见影。很多反调试检测第一步就是看这个字段,置 0 之后能绕过大部分基础检测。注意这个字段是 BYTE 类型,直接赋值就行,不需要考虑字节序。

最后说个我自己的习惯:每次改完 PEB,不要急着跑完整流程,先用一个独立的验证程序去读目标进程的 PEB,确认修改生效了再继续。这个验证程序最好用不同的 API 组合,比如同时用ReadProcessMemory和NtQueryInformationProcess,交叉验证能发现很多单一路径发现不了的问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 13:59:46

【计算机单片机毕业设计指导(内含开题报告参考)】基于单片机的智能洗衣机水位衣物重量检测控制系统设计,基于STM32的洗衣机重量检测洗涤烘干与控制装置设计(032805)

文章目录20 个相关毕业设计备选题目项目研究背景摘要总体方案核心功能技术路线开题报告一、选题依据&#xff08;一&#xff09;研究背景&#xff08;二&#xff09;研究的意义&#xff08;三&#xff09;国内外研究现状&#xff08;四&#xff09;参考文献二、设计或研究内容、…

作者头像 李华
网站建设 2026/10/15 13:59:45

逆数学:公理系统与数学定理的逻辑强度分析

1. 逆数学的逻辑学探索价值在数学基础研究的版图中&#xff0c;逆数学&#xff08;Reverse Mathematics&#xff09;正逐渐成为检验逻辑体系强度的精密仪器。这个诞生于20世纪70年代的研究方向&#xff0c;通过独特的"逆向思维"揭示了数学定理与公理系统之间深刻的依…

作者头像 李华
网站建设 2026/10/15 13:58:00

纯HTML打造高性能社区新闻聚合页实战

1. 项目概述与设计思路最近帮本地社区中心搭建了一个新闻聚合页面&#xff0c;要求简洁明了、加载快速且兼容各种设备。用纯HTML实现是最轻量的解决方案&#xff0c;不需要依赖任何框架或后端服务。这种静态页面特别适合内容相对固定、更新频率不高的场景&#xff0c;比如社区公…

作者头像 李华
网站建设 2026/10/15 13:57:45

Unity游戏美术资源自动化校验工具设计与实践

1. 项目背景与核心价值在游戏开发团队中&#xff0c;美术资源的规范化管理一直是影响项目质量和开发效率的关键因素。以Unity项目为例&#xff0c;一个中型游戏通常包含数千个模型和材质资源&#xff0c;如果缺乏有效的自动化校验手段&#xff0c;仅靠人工检查不仅效率低下&…

作者头像 李华
网站建设 2026/10/15 13:57:32

Flutter数据层架构:Repository模式实践与优化

1. 项目概述在Flutter应用开发中&#xff0c;数据层的混乱管理一直是困扰开发者的痛点。Repository模式作为一种经典架构设计&#xff0c;能够有效解决数据源耦合、业务逻辑污染等问题。我在最近的一个电商类Flutter项目中&#xff0c;深度实践了Repository模式的数据层抽象方案…

作者头像 李华
网站建设 2026/10/15 13:56:48

STM32 FSMC驱动W5300 16位并口:从配置到吞吐优化

简介&#xff1a;STM32_W5300_16Bit标准例程面向嵌入式网络开发初学者与进阶工程师&#xff0c;聚焦STM32通过FSMC总线与W5300以太网控制器进行16位并行通信的完整实现&#xff0c;帮助解决网络接入、高速数据传输与异常断网恢复等实际问题。压缩包共999个文件&#xff0c;约15…

作者头像 李华