diabloidyobane/BlindSpot

GitHub: diabloidyobane/BlindSpot

逆向分析《全境封锁 2》中一个手动映射的作弊 DLL,记录了绕过 pe-sieve 检测盲点并完成 PE 重构与静态分析的完整工作流。

Stars: 14 | Forks: 0

Find what pe-sieve misses. Reverse engineering a manually-mapped cheat DLL.

# 盲点 *逆向分析《全境封锁 2》中一个手动映射的商业作弊 DLL。* ![Status](https://img.shields.io/badge/Status-Published-dc2626?style=flat-square&labelColor=0a0a0a) ![Focus](https://img.shields.io/badge/Focus-Reverse_Engineering-dc2626?style=flat-square&labelColor=0a0a0a) ![Category](https://img.shields.io/badge/Category-Implant_Analysis-dc2626?style=flat-square&labelColor=0a0a0a) ![AC](https://img.shields.io/badge/Anti--Cheat-Safe-dc2626?style=flat-square&labelColor=0a0a0a) ![Platform](https://img.shields.io/badge/Platform-Windows_x64-525252?style=flat-square&labelColor=0a0a0a) ![Tools](https://img.shields.io/badge/Tools-IDA_Pro-525252?style=flat-square&labelColor=0a0a0a) ![Tools](https://img.shields.io/badge/Tools-HollowsHunter-525252?style=flat-square&labelColor=0a0a0a) ![License](https://img.shields.io/badge/License-MIT-525252?style=flat-square&labelColor=0a0a0a) 对注入到 `TheDivision2.exe`(育碧 Snowdrop 引擎)中的商业作弊 DLL 的静态分析。该植入体通过将 `NtMapViewOfSection` 与 `MEM_PRIVATE` 内存分配及 PE 头擦除相结合,成功 evade 了[pe-sieve](https://github.com/hasherezade/pe-sieve)(目前最强的公开用户态内存扫描器)。本仓库记录了发现、dump、重构为可被 IDA 加载的 PE 文件,以及将其交叉引用(cross-reference)回宿主镜像的全过程。 为了使研究专注于技术本身而非特定产品,全文刻意省略了该作弊器的品牌名称。 ## 概述 此处的发现附带一篇技术论文:**[《全境封锁 2》中手动映射植入体的工作流研究](implant_paper.md)**。论文涵盖了手动映射的解剖结构、pe-sieve 的枚举盲点、针对 275 个可执行区域的异常排名、通过 HollowsHunter 恢复 IAT,以及在目标镜像中发现 37 个命中的绝对指针交叉引用扫描。 这种级别的静态分析适用于任何擦除其 PE 头并手动解析导入(import)的用户态植入体:包括商业作弊器、游戏内注入器、EDR 用户态模块以及某些恶意软件加载器。 ## 文件说明 | 路径 | 描述 | |---|---| | `implant_paper.md` | 完整技术论文:方法、发现与讨论 | | `dump/implant_reconstructed.dll` | 围绕 dump 主体构建的合成 PE32+ 封装,可在 IDA 中加载 (2.8 MB) | | `dump/implant_reconstructed.dll.id0` | IDA 数据库 — 主存储 (15 MB) | | `dump/implant_reconstructed.dll.id1` | IDA 数据库 — 辅助存储 (11 MB) | | `dump/implant_reconstructed.dll.nam` | IDA 数据库 — 命名地址 | | `dump/implant_reconstructed.dll.til` | IDA 数据库 — 类型信息 | | `dump/implant_xrefs_abs.csv` | 从植入体指向宿主镜像的 37 个绝对 8 字节指针(RVA, VA, 文件偏移) | | `dump/implant_xrefs_rel.csv` | rel32 扫描结果(0 个命中,见论文第 5 节) | | `dump/implant_imports.txt` | 通过 HollowsHunter `/imp 3` 恢复的包含 244 项的 IAT | | `scripts/scan_exec_private.py` | `VirtualQueryEx` 遍历器,列出所有已提交的 RWX 私有内存区域 | | `scripts/dump_manualmap.py` | 区域 dump 工具,写入 `execpriv__.bin` | | `scripts/dump_contiguous_image.py` | 变体 dump 工具,连续区域扫描 | | `scripts/rebuild_headerless.py` | 伪造合成 PE32+ 头,以便 IDA 能打开 dump 文件 | | `scripts/dump_implant_fresh.py` | 实时进程自动 dump 工具(不依赖特定目标,带大小过滤) | | `scripts/dump_loader_region.py` | 第一阶段(Stage-1)加载器区域 dump 工具 | | `scripts/extend_hh_dump.py` | 扩展 HollowsHunter dump 以捕获区域边界 | 重构后的 DLL 在运行时已擦除了其原始 PE 头,因此这里的版本是在 [WALKTHROUGH.md](WALKTHROUGH.md) 中记录的合成 PE 头重建后,加载到 IDA 中的结果。IDA 数据库(`.id0/.id1/.nam/.til` 边车文件)保留了导入、注释和命名函数,因此你可以直接定位到 hook 表和弹药辅助函数的位置,而无需从头开始分析。 使用 `File > Open... > dump/implant_reconstructed.dll` 打开 IDA 数据库 — IDA 会根据名称自动找到这些边车文件。在你保存自己的更改后,IDA 可能会生成一个打包的 `implant_reconstructed.dll.i64` 数据库;如果你提交了改进,这也是一个正常的可提交文件。 `dump/` 中的交叉引用 CSV 文件让你能够验证,在对自己的植入体副本执行该工作流时,是否能定位到相同的 hook 表。 ## 实际过程 论文是严谨客观的;但实际的工作流却并非如此。简述如下: 1. **pe-sieve 一无所获。** 因为作弊菜单就在屏幕上,所以我确信植入体已经注入到 TD2 中。pe-sieve 扫描了进程并报告 `0 implants`。尝试了默认参数,然后是 `/shellc 3 /data 3 /refl` — 在被分类为植入体的输出中仍然什么也没有。这就是触发条件。 2. **询问 Claude 为什么 pe-sieve 会漏报。** 我 dump 了 pe-sieve 的 `scan_report.json`,并与 Claude 一起梳理了枚举模型。答案正是 `EnumProcessModulesEx` 的盲点:没有 PEB 模块条目的 `MEM_PRIVATE` 区域永远不会被扫描器扫到。这指明了我的下一步行动 — 彻底绕过枚举,直接遍历虚拟内存。 3. **`VirtualQueryEx` 遍历器 + 大小排序。** 编写了 `scan_exec_private.py`。过滤条件为 `MEM_COMMIT` + 任何 `EXECUTE` 标志 + 无文件映射。剩余 11 个候选区域。按大小排序。最大的为 2.8 MB。次大的为 64 KB。搞定 — 这就是我们的目标。 4. **PE 头已被擦除。** 我使用 `dump_manualmap.py` dump 了该区域。前 0x1000 字节全为零,随后是 x64 prologue。IDA 拒绝加载这个原始的 bin 文件。再次求助 Claude:我们得出结论,我需要伪造一个合成的 PE32+ 头(DOS 存根、NT 头、覆盖整个镜像的单个 `.text` 节),这样 IDA 才会将这个 dump 当作真正的 DLL 处理。这就诞生了 `rebuild_headerless.py`。 5. **IDA 干净利落地打开了重构的 DLL。** 导入仍然无法解析,因为 IAT 是在运行时被 thunk 化的。我运行了带 `/imp 3` 参数的 HollowsHunter,遍历主体内部呈 IAT 形状的指针数组 — 在 9 个系统 DLL 中获取了 244 个条目。将它们作为命名符号粘贴回 IDA 数据库中。 6. **寻找无限弹药。** 将植入体与 `TheDivision2.exe` 进行交叉引用。从植入体中获得了 37 个指向宿主镜像的绝对 8 字节指针 — 这就是植入体的 hook 表。在 IDA 中顺着这些 RVA 回溯到宿主 EXE,找到了它正在 patch 的调用点。其中两个位于武器 tick 逻辑路径中:`WeaponInstance::Tick` (RVA `0x4F6A1C0`) 以及 RVA `0x4F6B340` 处的弹药递减辅助函数。作弊器并没有直接修改弹药字段 — 它 hook 了*递减操作*使其成为 no-op。我在受控测试中自行将递减指令清零并看到弹匣永远不会耗尽,从而证实了这一点。 实际的技术细节(内存布局、字节模式、比率数学)请参阅[论文](implant_paper.md)。上面的叙述是实际操作中工作流的真实体验。包含时间预算、经验教训的完整逐步记录详见:[WALKTHROUGH.md](WALKTHROUGH.md)。 ## 方法论 该流程在单台工作站上仅需 **90 秒的物理执行时间**,无需驱动、调试器或内核访问权限。分为六个步骤: ### 1. pe-sieve 基线测试 ``` pe-sieve64.exe /pid ``` 确认触发条件:pe-sieve 报告 `0 implants`,因为植入体没有 PEB 模块条目。这正是工作流其余部分所要攻克的盲点。 ### 2. VirtualQueryEx 遍历 ``` python scripts/scan_exec_private.py ``` 在循环中调用 `VirtualQueryEx`,按 `RegionSize` 步进。过滤 `MEM_COMMIT` + 任何 `PAGE_EXECUTE*` 标志。丢弃任何 `GetMappedFileNameW` 返回文件路径的区域。在一次典型的 TD2 运行中会生成约 11 个候选区域。 ### 3. 异常排名 按大小降序对存活区域进行排序。在这个植入体中,最大的区域为 **2.8 MB**;次大为 **64 KB**。45:1 的比例使植入体暴露无遗。合法的 JIT/CLR 内存分配很少会超过几百 KB 且是碎片化的;而单个连续的 DLL 主体则处于兆字节级别。 ### 4. 字节模式确认 读取候选基址的前 64 字节。有三种可能的状态: | 状态 | +0x0 处的字节 | 含义 | |---|---|---| | 有效 PE | `MZ...` | 正常映射的镜像 | | 头部被擦除 | `00 00 00 ...` 持续至少 0x1000 | 植入体代码从 +0x1000 开始 | | 随机 | 看起来像代码 | 头部被擦除但没有零填充,或是隔离的内存分配 | 该植入体属于**状态 2**。位于 +0x1000 处的 x64 prologue(`48 89 5C 24 10 48 89 7C 24 18 55 48 8D AC 24 ...`)预留了 0xB1E0 字节的栈帧 — 这种量级属于 tick 函数,而非叶子函数。 ### 5. 通过 HollowsHunter 恢复 IAT ``` hollows_hunter64.exe /pname TheDivision2.exe /imp 3 /shellc 3 /data 3 /refl /threads /minidmp ``` HollowsHunter 的 `/imp 3` 扫描 dump 的主体,寻找 IAT 形状的指针数组,并将每个条目解析为 DLL + 导出函数。该植入体:**跨 9 个 DLL 的 244 个导入**(`kernel32`、`ntdll`、`user32`、`gdi32`、`imm32`、`shell32`、`msvcp140`、`d3dcompiler_47`、`dxgi`)。 ### 6. 对宿主镜像的交叉引用枚举 ``` python scripts/extract_xrefs.py dump/implant.bin --host TheDivision2.exe ``` 读取植入体主体,计算宿主镜像范围,然后以 8 字节为步长扫描,寻找处于该范围内的 64 位值。单独扫描 `E8/E9 ?? ?? ?? ??` rel32 模式并计算其目标。 在该植入体中:**37 个绝对指针命中,0 个 rel32 命中**。这种不对称性本身就是一个发现。宿主 EXE 加载的基址距离植入体足够远,以至于从植入体发出的 rel32 根本无法触及它(超出了 2 GB 范围),这迫使植入体必须使用指针表和间接调用。 ### 辅助:线程起始地址扫描 ``` python scripts/enum_thread_starts.py ``` 通过 `Thread32First`/`Thread32Next` 遍历所有线程,通过 `NtQueryInformationThread(9)` 查询 `Win32StartAddress`。在目标进程的 120 个线程中,**零个线程的起始地址位于植入体范围内**。结合植入体包含 `OpenThread`/`SetThreadContext`/`Thread32Next` 导入的情况,这支持了**线程劫持执行模型**的推断:植入体 patch 现有宿主线程的上下文,将执行流重定向到其代码中,然后再恢复原状。 ## 测工具对比 | 工具 | 模式 | 是否检测到? | 原因 | |---|---|---|---| | pe-sieve | `/dmode 3 /imp 2` (默认) | 否 | `EnumProcessModulesEx` 盲点 | | pe-sieve | `/shellc 3 /data 3 /refl` | 部分 | 主体过大不符合 shellcode 模式;被 dump 但未被标记为植入体 | | HollowsHunter | `/imp 3 /shellc 3 /threads` | 是 | 与 pe-sieve 引擎相同,捕获范围更广 | | 自定义 `VirtualQueryEx` 遍历器 | 大小 + 无文件排序 | 是(决定性) | 不依赖枚举假设 | | `Win32StartAddress` 线程扫描 | `NtQueryInformationThread(9)` | 否 | 线程劫持不产生匹配的起始地址 | | Volatility / Malfind | 内核侧 VAD 遍历 | 能够检测到 | 无论 PEB 可见性如何,都会标记 `MEM_PRIVATE +RWX` | ## 植入体内存布局 三个连续的区域构成了活跃的植入体: | 区域 | 大小 | 类型 | 保护属性 | 映射来源 | 内容 | |---|---|---|---|---|---| | `0x1C2D2B70000` | 2.8 MB | MEM_PRIVATE | RWX | 无 | DLL 主体,PE 头已擦除,代码从 +0x1000 开始 | | `0x1C2D2EB0000` | <4 KB | MEM_PRIVATE | RW | 无 | 包含 `gameoverlayrenderer64.dll` 路径的上下文结构体 | | `0x1C2D2EC0000` | 4 KB | MEM_PRIVATE | RX | 无 | 线程入口存根、寄存器快照、间接调用、尾跳转 | 位于 `0x1C2D2EB0000` 处的 Steam overlay 路径字符串符合模块名伪装的特征(填充路径使得被劫持线程随后调用的 `GetModuleFileNameW` 返回 Steam DLL),但我们在本次研究中并未确认其具体使用的位置。 ## 相关工具 这篇 writeup 深度依赖于两款工具。致谢及链接: - **[hasherezade/pe-sieve](https://github.com/hasherezade/pe-sieve)** — 本项工作记录了其枚举盲点的公开内存扫描器。这个工作流的存在,*正是因为* pe-sieve 是目前最强的用户态扫描器;如果手动映射的植入体能绕过它,那基本上就能绕过用户态级别的所有检测。HollowsHunter 也是 Hasherezade 编写的,正是它恢复了 IAT。 - **[diabloidyobane/PEReconstruct](https://github.com/diabloidyobane/PEReconstruct)** — 我的目标无关 DLL 重构工具包。这里 `scripts/` 目录下的脚本是针对 Blindspot 的子集;完整的工具包(更广泛的扫描器、合成 PE 头伪造器、IAT 恢复辅助工具、hook 解析器)都在那个仓库中。它适用于任何你能对其调用 `OpenProcess()` 的进程。不需要驱动、调试器或符号文件。 如果你要在其他目标上寻找手动映射的植入体,请先运行 pe-sieve 确认盲点,然后再将 PEReconstruct 指向该进程。 ## 对 AI 角度的客观看法 Claude Code 在这里完成了实质性的工作。当我卡壳时,它解释了 pe-sieve 的枚举模型,结对编程完成了用于重建的合成 PE32+ 头布局,梳理了 IAT 恢复逻辑,并抓出了我的 `VirtualQueryEx` 遍历器中本会漏掉 `MEM_MAPPED` 候选者的两个 bug。在处理这类静态分析时,安全过滤器完全没有造成阻碍。 但它并不能替代底层技能。要跟随这一工作流,你仍然需要: - **静态逆向(RE)熟练度**,以便一眼看出被擦除头的 x64 prologue 长什么样,识别出呈 IAT 形状的指针数组而非噪声,并在不命名的情况下阅读反编译的 hook handler - **熟悉 Windows 内部机制**,了解 `VAD`、`PEB`、`LdrLoadDll`,section 与 private 分配的区别,以及为什么 `EnumProcessModulesEx` 是错误的 primitive - **C++ 舒适度**,能够在 IDA 中阅读宿主 EXE 的反编译代码,并跟进大量使用 vtable 的游戏引擎代码 - **IDA 工作流**,能够浏览 3000 多个函数、命名结构体、传递类型并高效地为 RVA 表添加书签 该模型只能加速你本来就懂得如何做的工作。把这段提示词交给一个没有逆向背景的人,他们会给出一个听起来自信满满却错得离谱的指导,导致他们的虚拟机蓝屏(BSOD)。上面的工作流是建立在多年手动分析经验基础上的,Claude Code(Claude Max,200 美元/月的订阅档位)在其中充当了更快的参考资料和打字员。 这不是一个即插即用的秘诀。如果你想复现它:请先在先决条件上花些时间,然后再回来。 ## 免责声明 本材料仅用于教育和研究目的。此处的分析和产物旨在提升安全社区对用户态植入技术的理解。我们不重新分发原始的植入体二进制文件;本仓库仅包含分析输出(逆向工程后的 dump 文件、合成 PE 封装说明、交叉引用表)。 如果你开发内存扫描器、EDR 产品、反作弊平台或游戏端完整性系统,此处的工作流记录了针对目前最强公开扫描器的一种真实绕过方法。请使用它来测试你的检测覆盖率。
标签:DAST, JARM, Python, 云资产清单, 内存分析, 恶意软件分析, 无后门, 游戏作弊分析, 端点可见性, 逆向工具, 逆向工程