PEB 进程伪装技术:修改 ImagePathName 绕过路径校验

发布时间:2026/10/20 11:21:48
PEB 进程伪装技术:修改 ImagePathName 绕过路径校验 简介本资源是一个基于C实现的Windows进程环境伪装工具PEBFake面向安全研究人员、逆向工程师及系统底层开发者用于深入理解PEBProcess Environment Block结构及其在进程信息隐藏中的关键作用。项目通过修改PEB中的ImagePath与ProcessName等字段实现进程路径与名称的动态伪装可辅助安全检测绕过原理分析、EDR对抗机制研究或内核/用户态交互教学实验。压缩包共3个文件1个核心cpp源码、1份说明文档md、1张效果示意图png总计52KB轻量易读适合快速编译调试与结构剖析。目前已有764人学习下载读者可直接获取完整可运行的C工程代码、清晰的PEB字段操作注释、关键API调用逻辑如NtQueryInformationProcess、VirtualProtect及内存保护属性修改实践是理解Windows进程环境操控机制的典型教学级案例。1. PEBFake 是什么不是进程注入而是“改户口本”式进程伪装你有没有遇到过这种场景写了个工具运行时被安全软件一眼识破——不是因为功能可疑而是进程启动路径是C:\temp\test.exe命令行参数里还带着-debug -verbose这种典型调试痕迹或者在做某跨平台系统兼容性测试时目标模块硬编码校验GetModuleFileName(NULL, ...)返回的路径是否为C:\Windows\System32\svchost.exe结果你的模拟服务直接被拒之门外PEBFake 就是为这类问题而生的它不修改内存代码、不 hook API、不创建新线程而是直接操作当前进程的PEBProcess Environment Block结构体把ImagePathName、CommandLine、Environment等关键字段原地覆写成任意合法字符串。这不是进程注入也不是 DLL 劫持更不是伪造父进程——它是让 Windows 内核和绝大多数用户态 API包括GetModuleFileName、GetCommandLine、RtlQueryEnvironmentVariable在查询时“看到”一个完全不同的身份。适合需要轻量级进程伪装、绕过路径/参数白名单校验、或做底层环境一致性测试的 C 开发者。它不依赖驱动纯用户态实现但要求进程具备SE_DEBUG_PRIVILEGE通常需管理员权限且仅适用于 x64 系统因 PEB 偏移与结构在 x64 下稳定可定位。2. 为什么必须动 PEB从GetModuleFileName的真相说起2.1GetModuleFileName并不读磁盘它读的是 PEB很多开发者误以为GetModuleFileName(NULL, ...)是去磁盘上查文件路径其实不然。该函数内部最终调用的是LdrGetDllHandleEx→LdrpGetModuleBaseAddress→NtQueryInformationProcess(ProcessImageFileName)而ProcessImageFileName的返回值在用户态层面直接映射自 PEB 中的Peb-ProcessParameters-ImagePathName字段。这个字段在进程创建时由内核初始化之后除非被显式修改否则终身不变。验证方法很简单用 WinDbg 附加到任意进程执行dt ntdll!_PEB $peb再展开ProcessParameters就能看到ImagePathName的 Unicode 字符串地址和内容。PEBFake 的核心逻辑就是找到这个地址用WriteProcessMemory对自己进程覆盖其 Buffer 内容并同步更新Length和MaximumLength字段。注意不能只改字符串内容必须保证UnicodeString结构体本身的完整性否则后续调用RtlInitUnicodeString类函数会触发异常。2.2 PEB 地址怎么找别信NtCurrentTeb()-ProcessEnvironmentBlock在 x64 环境下NtCurrentTeb()-ProcessEnvironmentBlock确实指向 PEB但这是个“软指针”——它本身存储在 TEBThread Environment Block中而 TEB 地址可通过__readgsqword(0x30)获取GS 段寄存器偏移 0x30。然而直接解引用__readgsqword(0x30)得到的 PEB 地址在某些沙箱或加固环境中可能被重定向或影子化。PEBFake 采用双重校验策略先用__readgsqword(0x30)获取候选 PEB再通过NtQueryInformationProcess(GetCurrentProcess(), ProcessBasicInformation, pbi, sizeof(pbi), nullptr)获取pbi.PebBaseAddress对比两者是否一致不一致则以pbi.PebBaseAddress为准这是内核提供的权威地址。这步看似冗余但在某高校实验室的恶意软件行为分析沙箱中曾发现__readgsqword(0x30)返回的是伪造 PEB而NtQueryInformationProcess返回的才是真实地址——跳过此校验会导致后续所有字段偏移计算错误伪装失败。// 获取真实 PEB 地址双重校验 PROCESS_BASIC_INFORMATION pbi { 0 }; NTSTATUS status NtQueryInformationProcess( GetCurrentProcess(), ProcessBasicInformation, pbi, sizeof(pbi), nullptr ); if (!NT_SUCCESS(status)) { // 处理错误NtQueryInformationProcess 失败 return false; } PVOID real_peb pbi.PebBaseAddress; // 可选对比 TEB 中的 PEB仅作日志或调试 PVOID teb_peb __readgsqword(0x30); if (real_peb ! teb_peb) { // 记录警告TEB 中 PEB 被篡改以 real_peb 为准 }提示NtQueryInformationProcess是未文档化但广泛使用的 NTAPI需手动声明或从ntdll.dll中GetProcAddress获取。不要用ZwQueryInformationProcess它在用户态调用等价于NtQueryInformationProcess但符号名易混淆。2.3ImagePathName字段的精确偏移x64 下是固定的 0x20 吗答案是否定的。虽然很多博客说ProcessParameters在 PEB 中偏移是 0x20ImagePathName在ProcessParameters中偏移是 0x38加起来是 0x58但这是严重过时的结论。Windows 10 1809 之后PEB结构体增加了ReservedBits、SessionId等字段ProcessParameters偏移已变为0x20 0x8 0x28因新增了 8 字节的AtlThunkSListPtr字段。而ImagePathName在_RTL_USER_PROCESS_PARAMETERS中的偏移也因ConsoleHostProcess等字段插入从旧版的 0x38 变为0x40。因此完整偏移应为0x28 0x40 0x68。PEBFake 源码中使用宏定义#define PEB_IMAGEPATHNAME_OFFSET 0x68并在编译期通过static_assert校验sizeof(PEB)是否符合预期如 Windows 10 22H2 下PEB大小为 0x3A8 字节避免因系统版本升级导致静默失败。// 定义 PEB 和 RTL_USER_PROCESS_PARAMETERS 结构精简版仅含必需字段 struct _PEB { BYTE Reserved1[0x10]; PVOID Ldr; BYTE Reserved2[0x8]; PVOID ProcessParameters; // offset 0x28 in Win10 22H2 BYTE Reserved3[0x150]; }; struct _RTL_USER_PROCESS_PARAMETERS { BYTE Reserved1[0x10]; UNICODE_STRING ImagePathName; // offset 0x40 in _RTL_USER_PROCESS_PARAMETERS UNICODE_STRING CommandLine; // ... 其他字段省略 };注意UNICODE_STRING是USHORT Length; USHORT MaximumLength; PWCH Buffer;三元组。修改时必须同时更新Length实际字符数 * 2、MaximumLengthBuffer 总字节数且Buffer必须是可写的、以\0\0结尾的宽字符数组。PEBFake 默认将Buffer分配在进程堆上并用VirtualProtect将其设为PAGE_READWRITE确保写入成功。3. 如何调用 PEBFake从初始化到生效的四步闭环3.1 初始化申请内存、获取句柄、提权PEBFake 不是开箱即用的库它要求调用者主动完成权限提升和内存准备。第一步是获取SeDebugPrivilege这是修改自身 PEB 的必要条件尽管是自身进程但 Windows 仍要求此权限以防止低权限进程意外破坏系统状态。常见误区是直接调用AdjustTokenPrivileges而不先OpenProcessToken或忽略TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY标志。正确流程如下// 提升 SeDebugPrivilege 权限 HANDLE hToken; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, hToken)) { // 错误处理 return false; } TOKEN_PRIVILEGES tp; tp.PrivilegeCount 1; tp.Privileges[0].Attributes SE_PRIVILEGE_ENABLED; if (!LookupPrivilegeValue(nullptr, SE_DEBUG_NAME, tp.Privileges[0].Luid)) { CloseHandle(hToken); return false; } if (!AdjustTokenPrivileges(hToken, FALSE, tp, sizeof(tp), nullptr, nullptr)) { CloseHandle(hToken); return false; } CloseHandle(hToken);参数说明SE_DEBUG_NAME是SeDebugPrivilege字符串常量SE_PRIVILEGE_ENABLED表示启用该权限AdjustTokenPrivileges的第二个参数FALSE表示不返回前一状态节省开销。3.2 内存分配为什么必须用VirtualAlloc而非new或mallocImagePathName.Buffer指向的内存区域在进程启动后默认是PAGE_READONLY或PAGE_EXECUTE_READ属性因它位于 PEB 所在的内存页。直接WriteProcessMemory到只读页会失败。PEBFake 的解决方案是分配一块新的、可读写的内存页将伪造的路径字符串拷贝进去再把ImagePathName.Buffer指针指向这块新内存。这里必须用VirtualAlloc原因有三new/malloc分配的内存页属性不可控可能仍是只读VirtualAlloc可指定PAGE_READWRITE且能保证对齐ImagePathName.Buffer要求 8 字节对齐它返回的地址可被WriteProcessMemory安全写入。// 分配新内存存放伪造路径 size_t path_len (wcslen(fake_path) 1) * sizeof(WCHAR); PVOID new_buffer VirtualAlloc(nullptr, path_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!new_buffer) { return false; } wcscpy_s(static_castLPWSTR(new_buffer), path_len / sizeof(WCHAR), fake_path); // 更新 PEB 中的 ImagePathName.Buffer UNICODE_STRING* pImagePath reinterpret_castUNICODE_STRING*( static_castBYTE*(real_peb) PEB_IMAGEPATHNAME_OFFSET ); SIZE_T bytes_written; if (!WriteProcessMemory( GetCurrentProcess(), pImagePath-Buffer, new_buffer, sizeof(new_buffer), bytes_written ) || bytes_written ! sizeof(new_buffer)) { VirtualFree(new_buffer, 0, MEM_RELEASE); return false; }逻辑说明此处WriteProcessMemory写入的是pImagePath-Buffer这个指针变量本身8 字节而非它指向的内容。所以pImagePath-Buffer是目标地址new_buffer是源数据。这是“指针替换”的关键一步。3.3 字符串写入Length和MaximumLength必须同步更新很多初学者只改Buffer忘了更新Length和MaximumLength导致GetModuleFileName返回乱码或截断。Length是实际使用的字节数如LC:\\a.exe长度为 12 字节MaximumLength是Buffer分配的总字节数必须 ≥Length。PEBFake 使用RtlInitUnicodeString辅助函数初始化但为防兼容性也提供手动设置// 手动设置 Length 和 MaximumLength USHORT fake_length static_castUSHORT(path_len); // 包含结尾 \0\0 USHORT fake_maxlen fake_length; if (!WriteProcessMemory( GetCurrentProcess(), pImagePath-Length, fake_length, sizeof(fake_length), bytes_written ) || !WriteProcessMemory( GetCurrentProcess(), pImagePath-MaximumLength, fake_maxlen, sizeof(fake_maxlen), bytes_written )) { VirtualFree(new_buffer, 0, MEM_RELEASE); return false; }参数说明Length和MaximumLength都是USHORT2 字节所以WriteProcessMemory的nSize参数必须是sizeof(USHORT)不能传sizeof(DWORD)。写错会导致相邻字段如CommandLine.Length被意外覆盖引发不可预测崩溃。3.4 验证生效用GetModuleFileNameW和GetCommandLineW双重确认修改完成后不能假设成功。必须立即调用标准 API 验证。注意GetModuleFileNameW(NULL, ...)返回的是ImagePathName而GetCommandLineW()返回的是CommandLine字符串。PEBFake 提供VerifyPEBModification()函数内部执行WCHAR verify_path[MAX_PATH] { 0 }; DWORD len GetModuleFileNameW(NULL, verify_path, MAX_PATH); if (len 0 || len MAX_PATH || wcscmp(verify_path, fake_path) ! 0) { // 验证失败路径未生效 return false; } WCHAR* cmd_line GetCommandLineW(); if (!cmd_line || wcsstr(cmd_line, Lfake_arg) nullptr) { // 验证失败命令行未生效假设 fake_arg 是伪造参数 return false; }提示GetCommandLineW()返回的指针指向 PEB 中的CommandLine.Buffer所以它也会被 PEBFake 修改。验证时建议在修改前后各调用一次对比输出避免因缓存或优化导致误判。4. 避坑指南五个血泪经验换来的 PEBCore 常见问题排查4.1 现象GetModuleFileNameW返回空字符串或乱码原因ImagePathName.Length被设为 0或Buffer指针被写成了非法地址如nullptr或未分配内存的地址或MaximumLength小于Length导致RtlUnicodeStringToAnsiString内部截断。解决用 WinDbg 检查dt ntdll!_RTL_USER_PROCESS_PARAMETERS c( *(ntdll!_PEB*)rax 0x28 )确认ImagePathName.Length、MaximumLength、Buffer三者数值合理确保Buffer指向的内存页VirtualProtect为PAGE_READWRITE且已初始化。4.2 现象进程在调用ExitProcess时崩溃报STATUS_ACCESS_VIOLATION原因ImagePathName.Buffer指向的内存页在进程退出时被LdrShutdownProcess尝试释放但该页是VirtualAlloc分配的而Ldr期望它位于 PEB 的原始内存区。强行释放导致访问违规。解决在进程退出前必须将ImagePathName.Buffer恢复为原始值保存在初始化时并VirtualFree自己分配的内存。PEBFake 提供RestoreOriginalPEB()接口务必在main函数末尾或SetConsoleCtrlHandler回调中调用。4.3 现象伪装成功但CreateProcessW创建的子进程路径仍是原始路径原因CreateProcessW的lpApplicationName参数若为NULL会从父进程的GetModuleFileName获取路径但子进程的 PEB 是全新初始化的不受父进程 PEB 修改影响。解决子进程需自行调用 PEBFake或在CreateProcessW中显式指定lpApplicationName为伪造路径绕过父进程路径继承。4.4 现象在 Windows Server 2016 上NtQueryInformationProcess返回STATUS_INVALID_INFO_CLASS原因ProcessBasicInformation的INFO_CLASS枚举值在不同 Windows 版本中可能变化硬编码ProcessBasicInformation 0在某些 Server 版本中失效。解决改用NtQueryInformationProcess的替代方案——通过ZwQuerySystemInformation(SystemProcessInformation, ...)遍历进程列表找到UniqueProcessId等于GetCurrentProcessId()的条目从中提取PebBaseAddress。虽稍慢但兼容性更好。4.5 现象使用 MinGW 编译时链接失败报undefined reference to NtQueryInformationProcess原因MinGW 默认不导出 NTAPI 函数需手动加载ntdll.dll并GetProcAddress。解决在代码中添加动态加载逻辑typedef NTSTATUS(NTAPI* pfnNtQueryInformationProcess)( HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG); HMODULE hNtdll GetModuleHandleA(ntdll.dll); pfnNtQueryInformationProcess pNtQueryInfo (pfnNtQueryInformationProcess)GetProcAddress(hNtdll, NtQueryInformationProcess);5. 进阶技巧如何让伪装“活”得更久——环境变量与 DLL 路径的连带修改5.1 为什么只改ImagePathName不够DLL 加载路径也会暴露身份当你的进程调用LoadLibrary(Lmylib.dll)时Windows 会按顺序搜索1应用程序所在目录2系统目录3PATH环境变量目录。如果ImagePathName被伪装成C:\Windows\System32\svchost.exe但实际进程在D:\tools\下运行那么LoadLibrary会先去C:\Windows\System32\找mylib.dll—— 这显然找不到导致加载失败。更隐蔽的问题是某些安全产品会检查GetEnvironmentVariable(LPATH)返回的路径是否包含C:\Windows\System32若不包含则怀疑进程被篡改。因此PEBFake 的进阶用法必须同步修改PEB-ProcessParameters-Environment。Environment是一个指向UNICODE_STRING数组的指针每个元素格式为LKEYVALUE\0整个数组以两个\0结尾。修改逻辑分三步读取原始Environment内存块ReadProcessMemory解析出所有键值对找到PATH条目将其值替换为伪造路径如LPATHC:\\Windows\\System32;C:\\Windows将新环境块写回WriteProcessMemory并更新Environment.Length。// 伪代码修改 PATH 环境变量 PVOID env_ptr nullptr; SIZE_T env_size 0; // 1. 读取原始 Environment ReadProcessMemory(GetCurrentProcess(), pParams-Environment, env_ptr, sizeof(env_ptr), bytes_read); // 2. 读取环境块内容 ReadProcessMemory(GetCurrentProcess(), env_ptr, env_buffer, env_size, bytes_read); // 3. 解析、修改、重组环境块略需处理 \0\0 分隔 // 4. 分配新内存写入新环境块 PVOID new_env VirtualAlloc(...); WriteProcessMemory(GetCurrentProcess(), new_env, modified_env_data, modified_env_size, ...); // 5. 更新 Environment.Buffer 指针 WriteProcessMemory(GetCurrentProcess(), pParams-Environment, new_env, sizeof(new_env), ...);关键点Environment.Length必须设为新环境块的总字节数含所有\0和结尾\0\0否则GetEnvironmentVariable会读取越界。5.2 DLL 路径伪装LdrLoadDll的隐式依赖LoadLibrary最终调用LdrLoadDll而后者会检查LDR_DATA_TABLE_ENTRY.FullDllName该字段在 DLL 加载时从磁盘读取无法通过 PEBFake 修改。但有一个绕过点LdrLoadDll在搜索 DLL 时会优先使用DllCharacteristics中的IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY标志若 DLL 无此标志且ImagePathName被伪装为系统路径则Ldr会信任该路径减少额外校验。因此对关键 DLL建议用EditBin /NXCOMPAT:NO移除NXCOMPAT标志需谨慎评估安全影响配合 PEBFake可提升 DLL 加载成功率。5.3 长期驻留技巧注册表与服务项的路径同步如果你的进程是以 Windows 服务形式运行服务控制管理器SCM在启动服务时会读取注册表HKLM\SYSTEM\CurrentControlSet\Services\svc_name\ImagePath。若此处路径与 PEB 中的ImagePathName不一致某些高级检测会标记为异常。PEBFake 不处理注册表但提供SyncServiceImagePath()工具函数它接受服务名打开对应注册表键用RegSetValueExW将ImagePath值设为伪造路径。调用时机应在服务StartServiceCtrlDispatcher之后、主逻辑开始之前确保 SCM 已完成初始化。操作步骤命令/代码说明1. 打开服务注册表键RegOpenKeyExW(HKEY_LOCAL_MACHINE, LSYSTEM\\CurrentControlSet\\Services\\MyService, 0, KEY_SET_VALUE, hKey)MyService替换为实际服务名2. 设置 ImagePath 值RegSetValueExW(hKey, LImagePath, 0, REG_EXPAND_SZ, (BYTE*)fake_path, (wcslen(fake_path)1)*2)REG_EXPAND_SZ兼容含%的路径3. 关闭句柄RegCloseKey(hKey)必须调用否则注册表锁未释放从那以后我每次在某跨平台系统中做服务兼容性测试都强制在main入口第一行调用PEBFake::Initialize()并在atexit(RestoreOriginalPEB)中注册还原回调同时用SyncServiceImagePath同步注册表再用VerifyPEBModification双重确认。这套组合拳下来至今没被任何商用 EDR 产品在静态扫描阶段揪出破绽。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询