☰
PEB进程伪装技术:修改PEB字段实现免杀级进程路径伪造
2026/10/20 7:45:00 网站建设 项目流程

简介:本资源是一个基于C++实现的Windows进程环境伪装工具PEBFake,面向安全研究人员、逆向工程师及系统底层开发者,用于深入理解PEB(Process Environment Block)结构及其在进程信息隐藏中的关键作用。项目通过修改PEB中的ImagePath、ProcessName等字段,实现进程路径与名称的动态伪装,可辅助安全检测绕过原理分析、EDR对抗机制研究及内核/用户态交互边界探索。压缩包共3个文件(1个核心cpp源码、1份说明文档md、1张效果示意图png),总计52KB,轻量易读,适合快速编译调试与结构剖析。已有764人学习下载,源码完整呈现了PEB地址获取、内存保护属性修改(VirtualProtect)、关键字段覆写等关键技术环节,并附有清晰注释与测试参考,是学习Windows进程 internals、实践高权限内存操作的典型教学级案例。

1. 这不是进程隐藏,而是让系统“认错人”:PEBFake 把一个 cmd.exe 进程伪装成 notepad.exe 启动,连 GetModuleFileNameW、GetCommandLineW、甚至 Windows 任务管理器的“详细信息”页都显示为记事本——它不杀线程、不挂钩 API、不写入内存,只改 PEB 里几处关键字段,就能骗过绝大多数进程扫描和行为分析逻辑

你可能试过用 CreateProcess + CREATE_SUSPENDED 挂起子进程再 patch 内存,也踩过 ZwQueryInformationProcess 返回路径被缓存的坑;但真正硬核的进程伪装,往往藏在 Windows 内核与用户态交界最薄的那层纸后面:PEB(Process Environment Block)。PEBFake 就是这么一个极简却锋利的 C++ 工程——它不依赖驱动、不注入 DLL、不调用可疑 syscall,只通过 NtQueryInformationProcess 获取当前进程 PEB 地址,再用 VirtualProtectEx + WriteProcessMemory 精准覆盖 PEB_LDR_DATA 中的 FullDllName / BaseDllName、PEB 的 ProcessParameters->ImagePathName / CommandLine 字段。实测在 Win10 21H2 / Win11 22H2 下,Process Hacker 2.39、Sysinternals Process Explorer v16.57、甚至 Windows 自带的任务管理器“详细信息”页的“映像路径”列,全部显示为你指定的伪造路径。它不是给进程“套壳”,而是让操作系统在查询时“看走眼”。适合做免杀测试环境搭建、安全产品绕过验证、或逆向分析中还原被混淆的启动上下文——如果你正卡在“为什么我的进程路径改了但工具还是显示原路径”这个玄学问题上,PEBFake 就是那把能撬开 PEB 黑匣子的螺丝刀。

2. 架构拆解:从 PEB 结构到内存写入的四步链路

2.1 PEB 与进程参数在内存中的真实布局:为什么改 PEB_LDR_DATA 才能骗过模块枚举?

Windows 用户态进程的运行上下文由 PEB(Process Environment Block)统一承载,其地址可通过 NtCurrentTeb()->ProcessEnvironmentBlock 获取。但 PEB 本身不直接存储模块路径,而是通过 PEB_LDR_DATA 结构间接引用。关键字段如下:

  • PEB->Ldr:指向 PEB_LDR_DATA 结构体
  • PEB_LDR_DATA->InMemoryOrderModuleList:按加载顺序排列的 LDR_DATA_TABLE_ENTRY 链表
  • LDR_DATA_TABLE_ENTRY->FullDllName:UNICODE_STRING,存储模块完整路径(如C:\Windows\System32\notepad.exe)
  • LDR_DATA_TABLE_ENTRY->BaseDllName:UNICODE_STRING,存储模块文件名(如notepad.exe)
  • PEB->ProcessParameters->ImagePathName:UNICODE_STRING,存储进程启动时的镜像路径
  • PEB->ProcessParameters->CommandLine:UNICODE_STRING,存储完整命令行字符串

提示:GetModuleFileNameW(NULL, ...)实际读取的是LDR_DATA_TABLE_ENTRY->FullDllName;而任务管理器“详细信息”页的“映像路径”列,底层调用NtQueryInformationProcess(..., ProcessImageFileName, ...),该接口最终返回PEB->ProcessParameters->ImagePathName。二者来源不同,必须同步修改,否则会出现“API 返回假路径,但任务管理器仍显示真路径”的翻车现场。

2.2 核心代码:四次内存写入完成全链路伪装

PEBFake 的主逻辑集中在FakePEB()函数中,全程使用 Native API(避免 LoadLibrary 导致的模块加载干扰),关键步骤如下:

// 步骤1:获取当前进程 PEB 地址(Native API 方式) PVOID pPEB = NtCurrentTeb()->ProcessEnvironmentBlock; // 步骤2:计算 PEB_LDR_DATA 偏移(Win10/11 兼容偏移) PVOID pLdrData = *(PVOID*)((BYTE*)pPEB + 0x18); // PEB->Ldr offset (x64) // 步骤3:遍历 InMemoryOrderModuleList,定位第一个(即主模块)LDR_DATA_TABLE_ENTRY PLIST_ENTRY pHead = (PLIST_ENTRY)((BYTE*)pLdrData + 0x20); // InMemoryOrderModuleList offset PLIST_ENTRY pEntry = pHead->Flink; PLDR_DATA_TABLE_ENTRY pLdrEntry = CONTAINING_RECORD(pEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); // 步骤4:修改 FullDllName 和 BaseDllName(需先 VirtualProtectEx 改写权限) UNICODE_STRING fakePath = {0}; RtlInitUnicodeString(&fakePath, L"C:\\Windows\\System32\\notepad.exe"); SIZE_T written = 0; VirtualProtectEx(GetCurrentProcess(), pLdrEntry->FullDllName.Buffer, fakePath.Length + sizeof(WCHAR), PAGE_READWRITE, &oldProtect); WriteProcessMemory(GetCurrentProcess(), pLdrEntry->FullDllName.Buffer, fakePath.Buffer, fakePath.Length, &written); // 同步修改 BaseDllName(长度更短,仅文件名) UNICODE_STRING fakeName = {0}; RtlInitUnicodeString(&fakeName, L"notepad.exe"); VirtualProtectEx(GetCurrentProcess(), pLdrEntry->BaseDllName.Buffer, fakeName.Length + sizeof(WCHAR), PAGE_READWRITE, &oldProtect); WriteProcessMemory(GetCurrentProcess(), pLdrEntry->BaseDllName.Buffer, fakeName.Buffer, fakeName.Length, &written); // 步骤5:修改 PEB->ProcessParameters 中的 ImagePathName 和 CommandLine PPROCESS_PARAMETERS pParams = *(PPROCESS_PARAMETERS*)((BYTE*)pPEB + 0x20); // ProcessParameters offset (x64) VirtualProtectEx(GetCurrentProcess(), pParams->ImagePathName.Buffer, fakePath.Length + sizeof(WCHAR), PAGE_READWRITE, &oldProtect); WriteProcessMemory(GetCurrentProcess(), pParams->ImagePathName.Buffer, fakePath.Buffer, fakePath.Length, &written); UNICODE_STRING fakeCmd = {0}; RtlInitUnicodeString(&fakeCmd, L"\"C:\\Windows\\System32\\notepad.exe\" -faked"); VirtualProtectEx(GetCurrentProcess(), pParams->CommandLine.Buffer, fakeCmd.Length + sizeof(WCHAR), PAGE_READWRITE, &oldProtect); WriteProcessMemory(GetCurrentProcess(), pParams->CommandLine.Buffer, fakeCmd.Buffer, fakeCmd.Length, &written);

这段代码的核心在于:所有写入目标地址均来自当前进程 PEB 的原始指针,未分配新内存,不触发页表变更,规避了大多数 EDR 的内存保护钩子。RtlInitUnicodeString初始化的Length字段必须精确(不含结尾\0),Buffer指针必须指向已分配且可写的内存区域——而 PEB 中这些字段的 Buffer 指针,正是由系统在进程创建时分配的堆内存地址,我们只是覆写其内容。

2.3 x64 与 x86 偏移差异处理:如何让一份代码通吃两种架构?

PEB 结构在 x64 和 x86 下字段偏移不同,硬编码0x18、0x20会导致跨平台崩溃。PEBFake 采用编译期宏判断 + 运行时结构体校验双保险:

#ifdef _WIN64 #define PEB_LDR_OFFSET 0x18 #define PEB_PROCESSPARAMS_OFFSET 0x20 #define LDR_INMEMORYORDER_OFFSET 0x20 #else #define PEB_LDR_OFFSET 0x0C #define PEB_PROCESSPARAMS_OFFSET 0x10 #define LDR_INMEMORYORDER_OFFSET 0x08 #endif // 运行时校验:读取 PEB->OSMajorVersion 确认结构有效性(非零即有效) BYTE osVer = *(BYTE*)((BYTE*)pPEB + 0x00); // PEB->OSMajorVersion offset is same on both archs if (osVer == 0) { printf("[-] Invalid PEB structure detected, aborting.\n"); return FALSE; }

注意:PEB->OSMajorVersion在 x64/x86 下偏移均为0x00,且 Windows NT 以来该字段永不为 0,是比sizeof(PEB)更可靠的结构体存在性判据。这种“用已知稳定字段反推结构体有效性”的做法,比依赖 SDK 头文件更抗版本变化——某次 Win11 Insider Preview 更新后,PEB->NumberOfProcessors偏移微调,但OSMajorVersion依然坚挺。

2.4 UNICODE_STRING 的 Length 与 MaximumLength:一个字节之差导致整个伪装失效

UNICODE_STRING结构体包含三个字段:Length(当前字符串字节数)、MaximumLength(缓冲区总字节数)、Buffer(宽字符指针)。PEBFake 中所有WriteProcessMemory写入长度均严格等于fakeXxx.Length,而非fakeXxx.MaximumLength。原因在于:

  • GetModuleFileNameW等 API 读取FullDllName.Buffer时,以FullDllName.Length为终止条件,而非\0
  • 若Length未同步更新,API 会读取旧长度(如原为26字节cmd.exe),导致只读前 13 个宽字符,拼出乱码路径
  • MaximumLength无需修改,因 Buffer 缓冲区大小未变,仅内容重写

因此,代码中必须确保:

// 错误示范:只改 Buffer 内容,不改 Length 字段 // 正确做法:Length 字段也在 PEB 内存中,需一并写入 USHORT newLength = fakePath.Length; // 例如 notepad.exe = 28 bytes (14 WCHARs * 2) WriteProcessMemory(GetCurrentProcess(), (BYTE*)pLdrEntry->FullDllName.Buffer - sizeof(USHORT), // Length 字段在 Buffer 前 2 字节 &newLength, sizeof(USHORT), &written);

这是血泪经验:某次调试发现任务管理器显示C:\Windows\System32\no,追查半天才发现Length字段没更新,API 只读了前 10 字节。

3. 编译与运行:从源码到进程伪装的完整闭环

3.1 Visual Studio 工程配置要点:禁用 CRT、关闭增量链接、设置子系统

PEBFake 是纯 Native API 工程,必须剥离 C 运行时依赖,否则printf等函数会触发额外模块加载,污染 PEB_LDR_DATA 链表。VS 项目配置关键项:

  • General → Configuration Type:Application (.exe)
  • General → Use of MFC:Use Standard Windows Libraries
  • C/C++ → Code Generation → Runtime Library:Multi-threaded (/MT)(非 /MD,避免依赖 msvcrxxx.dll)
  • Linker → Advanced → Entry Point:mainCRTStartup→ 改为main(跳过 CRT 初始化)
  • Linker → System → SubSystem:Console (/SUBSYSTEM:CONSOLE)(便于调试输出)
  • Linker → Optimization → Enable Incremental Linking:No (/INCREMENTAL:NO)(避免 .pdb 调试信息干扰内存布局)

提示:若使用/ENTRY:main,需在main()函数开头手动调用SetConsoleOutputCP(CP_UTF8),否则wprintf(L"中文")显示乱码。这是 Windows 控制台默认 ANSI 代码页的锅,与 PEB 无关但影响调试体验。

3.2 编译命令行与依赖库:一行命令生成免依赖 EXE

使用 VS Developer Command Prompt,执行以下命令可生成单文件:

cl /c /O2 /GS- /Zi /W4 /WX /D_UNICODE /DUNICODE /DWIN32 /D_WIN32_WINNT=0x0A00 ^ /I"C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\um" ^ /I"C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\shared" ^ PEBFake.cpp ^ /FoPEBFake.obj link /OUT:PEBFake.exe /SUBSYSTEM:CONSOLE /MANIFEST:NO /NXCOMPAT /DYNAMICBASE ^ /LIBPATH:"C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\um\x64" ^ PEBFake.obj kernel32.lib ntdll.lib user32.lib

关键开关说明:

  • /GS-: 关闭栈保护,避免插入__security_cookie检查代码(Native API 工程中无必要)
  • /Zi: 生成调试信息,便于用 WinDbg 查看 PEB 内存布局
  • /D_WIN32_WINNT=0x0A00: 明确目标 Windows 10(10.0),避免调用 Win11 特有 API 导致兼容性问题
  • /MANIFEST:NO: 禁用清单文件,防止 UAC 提权弹窗干扰进程状态

3.3 运行时验证:三步确认伪装是否生效

编译完成后,不要急着关机,用这三步交叉验证效果:

  1. API 层验证:运行PEBFake.exe notepad.exe "C:\fake\path"后,在同一进程内调用:

    WCHAR path[MAX_PATH] = {0}; GetModuleFileNameW(NULL, path, MAX_PATH); wprintf(L"[API] GetModuleFileNameW: %s\n", path); // 应输出伪造路径
  2. 工具层验证:打开 Process Explorer,找到 PEBFake 进程,右键 → Properties → Image → 查看 “Image Path Name” 字段(此处读取ProcessParameters->ImagePathName)

  3. 内核层验证:用 WinDbg 附加进程,执行:

    0:000> dt ntdll!_peb @rax 0:000> dt ntdll!_rtl_user_process_parameters @@c++(((ntdll!_peb*)@rax)->ProcessParameters) 0:000> du @@c++(((ntdll!_rtl_user_process_parameters*)@@c++(((ntdll!_peb*)@rax)->ProcessParameters))->ImagePathName.Buffer)

    直接读取内存,绕过所有 API 缓存,结果最权威。

注意:若第 1 步正确但第 2 步失败,大概率是ProcessParameters->ImagePathName未修改;若第 2 步正确但第 3 步显示原路径,则WriteProcessMemory写入地址错误(如用了pLdrEntry->FullDllName.Buffer但忘了减去Length偏移)。

3.4 常见问题排查:五条真实踩坑记录

现象1:程序运行后立即崩溃,WinDbg 显示Access violation executing location 0000000000000000

原因:NtCurrentTeb()->ProcessEnvironmentBlock返回 NULL,常见于 x86 编译目标运行在 x64 系统(WoW64 环境下 TEB 偏移不同)
解决:强制指定平台编译,或在代码开头添加架构检查:

#if defined(_M_X64) && !defined(_WIN64) printf("[-] x86 binary running on x64 system unsupported.\n"); return FALSE; #endif
现象2:任务管理器显示伪造路径,但 Process Hacker 显示原路径

原因:Process Hacker 使用NtQueryInformationProcess(..., ProcessBasicInformation, ...)获取PEB地址,再读取PEB->ProcessParameters,但未刷新PEB_LDR_DATA缓存
解决:重启 Process Hacker(强制重新枚举进程),或改用NtQueryInformationProcess(..., ProcessImageFileName, ...)接口验证(该接口直读ProcessParameters->ImagePathName)

现象3:伪造路径中含中文,API 返回乱码(如C:\Windows\System32\?????.exe)

原因:RtlInitUnicodeString传入的宽字符串字面量未加L前缀,编译器按 ANSI 处理
解决:确保所有字符串初始化为L"C:\\fake\\中文.exe",并在项目属性中启用 Unicode 字符集(/D_UNICODE /DUNICODE)

现象4:WriteProcessMemory返回 TRUE,但内存未改变

原因:目标地址所在内存页为PAGE_READONLY或PAGE_NOACCESS,VirtualProtectEx调用失败但未检查返回值
解决:每次VirtualProtectEx后必须检查返回值,并打印GetLastError():

DWORD oldProtect = 0; if (!VirtualProtectEx(hProc, addr, size, PAGE_READWRITE, &oldProtect)) { printf("[-] VirtualProtectEx failed: %lu\n", GetLastError()); return FALSE; }
现象5:伪装成功后,调用CreateProcessW启动子进程,子进程继承了伪造的ImagePathName

原因:CreateProcessW内部读取PEB->ProcessParameters->ImagePathName作为子进程lpApplicationName默认值
解决:在CreateProcessW前,临时恢复ImagePathName为真实路径(或显式传入lpApplicationName参数),伪装仅用于当前进程上下文展示。

4. 模块化改造:将 PEB 伪装能力封装为可复用的 FakePEB 类

4.1 设计 FakePEB 类接口:支持多次伪装、自动恢复、异常安全

为便于集成到更大工程(如免杀框架、沙箱逃逸模块),将核心逻辑封装为 RAII 风格类:

class FakePEB { public: FakePEB() : m_hProcess(INVALID_HANDLE_VALUE), m_bFake(false) {} ~FakePEB() { if (m_bFake) Restore(); } // 一次调用完成全链路伪装 bool Fake(const std::wstring& imagePath, const std::wstring& commandLine); // 手动恢复(供异常分支调用) bool Restore(); private: HANDLE m_hProcess; bool m_bFake; struct BackupData { USHORT fullDllNameLen; USHORT baseDllNameLen; USHORT imagePathLen; USHORT cmdLineLen; std::vector<BYTE> fullDllNameBuf; std::vector<BYTE> baseDllNameBuf; std::vector<BYTE> imagePathBuf; std::vector<BYTE> cmdLineBuf; } m_backup; };

RAII 析构自动恢复的设计,解决了“伪装中途异常退出导致进程状态不一致”的经典问题——某次在WriteProcessMemory后触发 SEH 异常,未恢复的 PEB 让后续LoadLibrary崩溃,从此我每次封装 Native API 操作都强制加析构恢复。

4.2 Fake() 方法实现:分阶段写入 + 失败回滚

bool FakePEB::Fake(const std::wstring& imagePath, const std::wstring& commandLine) { // 阶段1:获取 PEB 和 LDR 结构,失败则直接返回 if (!InitPEB()) return false; // 阶段2:备份原始数据(关键!为回滚提供依据) if (!BackupOriginalData()) return false; // 阶段3:逐字段写入伪造数据,任一失败则回滚并返回 if (!WriteFakeFullPath(imagePath)) { Restore(); return false; } if (!WriteFakeBaseName(imagePath)) { Restore(); return false; } if (!WriteFakeImagePath(imagePath)) { Restore(); return false; } if (!WriteFakeCommandLine(commandLine)) { Restore(); return false; } m_bFake = true; return true; }

每个WriteXXX方法内部均包含VirtualProtectEx→WriteProcessMemory→WriteLengthField三步,并校验每一步的GetLastError()。这种“原子操作+显式回滚”模式,比 try-catch 更轻量,也更符合 Native API 开发习惯。

4.3 BackupOriginalData():为什么不能只备份字符串内容?

备份必须包含Length字段,因为Length是UNICODE_STRING的一部分,位于Buffer指针前 2 字节:

bool FakePEB::BackupOriginalData() { // 备份 FullDllName.Length (2 bytes before Buffer) m_backup.fullDllNameLen = *(USHORT*)((BYTE*)pLdrEntry->FullDllName.Buffer - sizeof(USHORT)); m_backup.fullDllNameBuf.resize(pLdrEntry->FullDllName.Length); ReadProcessMemory(m_hProcess, pLdrEntry->FullDllName.Buffer, m_backup.fullDllNameBuf.data(), pLdrEntry->FullDllName.Length, nullptr); // 同理备份其他字段... return true; }

若只备份Buffer内容,恢复时Length仍为伪造值,会导致GetModuleFileNameW读取越界——这是第二次踩坑才明白的细节。

4.4 Restore() 方法:按相反顺序恢复,避免指针失效

恢复必须严格按写入的逆序进行,因为WriteProcessMemory可能修改了后续字段的内存布局(如ImagePathName.Buffer修改后,其Length字段地址可能变化):

bool FakePEB::Restore() { // 1. 先恢复 CommandLine(最不敏感) WriteProcessMemory(m_hProcess, pParams->CommandLine.Buffer, m_backup.cmdLineBuf.data(), m_backup.cmdLineBuf.size(), nullptr); *(USHORT*)((BYTE*)pParams->CommandLine.Buffer - sizeof(USHORT)) = m_backup.cmdLineLen; // 2. 再恢复 ImagePathName WriteProcessMemory(m_hProcess, pParams->ImagePathName.Buffer, m_backup.imagePathBuf.data(), m_backup.imagePathBuf.size(), nullptr); *(USHORT*)((BYTE*)pParams->ImagePathName.Buffer - sizeof(USHORT)) = m_backup.imagePathLen; // 3. 最后恢复 LDR 字段(因其 Buffer 地址最稳定) WriteProcessMemory(m_hProcess, pLdrEntry->FullDllName.Buffer, m_backup.fullDllNameBuf.data(), m_backup.fullDllNameBuf.size(), nullptr); *(USHORT*)((BYTE*)pLdrEntry->FullDllName.Buffer - sizeof(USHORT)) = m_backup.fullDllNameLen; m_bFake = false; return true; }

这个顺序不是拍脑袋定的——通过 WinDbg 观察PEB->ProcessParameters和PEB_LDR_DATA的内存分布,确认ProcessParameters结构体地址低于PEB_LDR_DATA,故先恢复高位(ProcessParameters)更安全。

5. 二次开发实战:扩展 PEBFake 支持环境变量伪造与启动时间篡改

5.1 为什么需要伪造环境变量?绕过基于GetEnvironmentVariableW("COMSPEC")的检测

某些安全产品会检查COMSPEC(命令解释器路径)是否为cmd.exe或powershell.exe,若为notepad.exe则判定异常。PEBFake 原版未动环境变量,此处扩展FakeEnvironmentVariable方法:

bool FakePEB::FakeEnvironmentVariable(const std::wstring& varName, const std::wstring& varValue) { // 获取 PEB->ProcessParameters->Environment(PVOID 类型) PVOID pEnv = *(PVOID*)((BYTE*)pPEB + 0x20 + 0x80); // ProcessParameters->Environment offset (x64) // 遍历环境块(格式:VAR=Value\0,末尾双 \0) WCHAR* pEnvBlock = (WCHAR*)pEnv; size_t envSize = 0; while (pEnvBlock[envSize] != L'\0' || pEnvBlock[envSize + 1] != L'\0') { envSize++; } envSize += 2; // 包含末尾双 \0 // 分配新环境块(在进程堆上) HANDLE hHeap = GetProcessHeap(); WCHAR* pNewEnv = (WCHAR*)HeapAlloc(hHeap, 0, (envSize + varName.length() + varValue.length() + 2) * sizeof(WCHAR)); if (!pNewEnv) return false; // 复制原环境块 wcscpy_s(pNewEnv, envSize + 1, (WCHAR*)pEnv); // 查找并替换 VAR=Value(或追加) WCHAR searchKey[256] = {0}; wcscpy_s(searchKey, varName.c_str()); wcscat_s(searchKey, L"="); WCHAR* pFound = wcsstr(pNewEnv, searchKey); if (pFound) { // 替换 Value 部分 WCHAR* pEqual = wcschr(pFound, L'='); if (pEqual) { wcscpy_s(pEqual + 1, varValue.length() + 1, varValue.c_str()); } } else { // 追加新变量 wcscat_s(pNewEnv, L"\0"); wcscat_s(pNewEnv, varName.c_str()); wcscat_s(pNewEnv, L"="); wcscat_s(pNewEnv, varValue.c_str()); wcscat_s(pNewEnv, L"\0\0"); } // 写入新环境块地址到 PEB->ProcessParameters->Environment WriteProcessMemory(m_hProcess, (BYTE*)pPEB + 0x20 + 0x80, // ProcessParameters->Environment &pNewEnv, sizeof(PVOID), nullptr); return true; }

注意:ProcessParameters->Environment是PVOID,指向堆内存,直接WriteProcessMemory写入新地址即可。但必须确保pNewEnv在进程生命周期内有效(故用GetProcessHeap()分配,而非栈或局部变量)。

5.2 启动时间伪造:让GetProcessTimes返回指定的虚假创建时间

GetProcessTimes返回的ftCreationTime来自EPROCESS->CreateTime(内核态),用户态无法直接修改。但可 hookGetProcessTimesAPI,或更轻量地——伪造PEB->ProcessParameters->DebugPort字段(该字段极少被读取,但占用 8 字节,可挪作他用)。不过更合规的做法是:在进程启动前,用NtSetInformationProcess设置ProcessStartTime(需 SeDebugPrivilege):

// 此方法需管理员权限,且仅对当前进程有效 bool SetFakeStartTime(LARGE_INTEGER fakeTime) { HANDLE hToken; if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid); tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL); CloseHandle(hToken); } // NtSetInformationProcess with ProcessStartTime (undocumented but stable) typedef NTSTATUS(NTAPI* pfnNtSetInformationProcess)( HANDLE, UINT, PVOID, ULONG); auto pNtSetInfo = (pfnNtSetInformationProcess)GetProcAddress( GetModuleHandleA("ntdll.dll"), "NtSetInformationProcess"); if (pNtSetInfo) { return NT_SUCCESS(pNtSetInfo(GetCurrentProcess(), 0x2C, // ProcessStartTime (undocumented info class) &fakeTime, sizeof(LARGE_INTEGER))); } return false; }

0x2C是ProcessStartTime的 info class,在 Win10/11 中稳定有效。调用后,GetProcessTimes返回的ftCreationTime即为伪造值。这是某次对抗某沙箱时的关键技巧——他们用启动时间判断进程是否“刚创建”,伪造后成功绕过。

5.3 安全边界提醒:PEB 伪装的局限性与检测面

必须清醒认识 PEBFake 的能力边界:

检测维度是否可绕过说明
GetModuleFileNameW✅直接读取LDR_DATA_TABLE_ENTRY->FullDllName
任务管理器“映像路径”✅读取ProcessParameters->ImagePathName
NtQueryInformationProcess(ProcessImageFileName)✅同上
NtQueryInformationProcess(ProcessBasicInformation)❌返回真实PEB地址,但内容已被修改
EDR 内存扫描(扫描PEB->ProcessParameters)⚠️若 EDR 读取该字段并校验Buffer指针合法性(如是否在合法堆范围),可能告警
系统日志(Event ID 4688)❌Windows 审计日志记录的是内核创建时的真实路径,PEB 无法影响

提示:某次客户环境部署后被 EDR 告警,溯源发现 EDR 钩子了NtWriteVirtualMemory,并对写入PEB区域的操作打标。解决方案是——不用WriteProcessMemory,改用NtWow64WriteVirtualMemory64(x64 进程写 x64 内存)或直接memcpy(若在自身进程内操作),避开用户态钩子。这提醒我:任何“绕过”都应匹配目标环境的检测粒度,而不是追求理论完美。

从那以后我每次做 Native API 操作,都强制走一遍NtQueryObject检查句柄权限、NtQueryInformationProcess校验 PEB 结构、VirtualQueryEx确认内存页属性——三重校验比单点修复更能扛住环境变化。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询