dest4590/gs60-protector

GitHub: dest4590/gs60-protector

一个使用 Rust 编写的 Windows x64 PE 加壳工具,通过加密、混淆和多层反分析技术保护可执行文件免遭逆向工程。

Stars: 0 | Forks: 0

GS60 Protector

一个使用 Rust 编写的 Windows x64 PE 加壳工具。它会获取你的可执行文件,对其进行加密,将其封装在一个手动 PE 加载器中,并应用各种反分析技巧,使逆向工程变得更加困难。你可以自由选择要启用的功能——拒绝强制的臃肿代码。 ## 盒子里有什么 四个主要的 Rust crate: | Crate | 用途 | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `protector` | 加壳器二进制文件。读取目标 EXE 和预编译的 stub,对 payload 进行加密,使用加密密钥修补 stub,将其打包到一个新节区中,并输出结果。 | | `stub` | 运行时加载器 stub。会被嵌入到受保护的二进制文件中。在不调用 IAT 中任何可见内容的情况下,解密/映射/执行你的原始代码。 | | `sandbox` | 用于验证 stub 安全层的测试工具 —— hook 注入器、一个微型调试器和一个测试应用。请参阅下方的 [沙盒](#sandbox)。 | | `xtask` | 构建辅助工具 —— 正常加壳不需要。 | ## 加壳器工作原理 当你运行 `protector.exe -i target.exe -s stub.exe -o protected.exe` 时,会发生以下事情: ### 1. 加载输入 将目标 EXE 和预编译的 stub 二进制文件读取到内存中。stub 是一个独立的 Rust 二进制文件(`stub.exe`),充当运行时加载器 —— 加壳器会在内存中对其进行修补,并将其包裹在你的目标程序周围。 ### 2. 移除元数据 从目标 EXE 中移除 RSDS 调试目录(PDB 路径、GUID)、Rich header(MSVC 构建指纹)以及任何尾部的调试冗余数据。这些字段在运行时毫无用处,但却为分析师提供了免费的版本/编译器指纹。移除它们通常可以节省 2-8 KB。 ### 3. 压缩(可选) 使用你配置中的 `compression_level` 对剥离后的二进制文件尝试 Deflate 压缩。只有在确实能减小体积时才会应用压缩 —— 加壳器会测量压缩率,并跳过对已压缩二进制文件(如 UPX 处理过的、.NET assemblies 等)的压缩。最终的二进制文件会在 `CRYPTO_PARAMS` 中记录一个 `FLAG_COMPRESSED` 位;stub 会在运行时解密前检查此位。 ### 4. 生成加密材料 为每次加壳操作创建全新的随机值: - **256 位 ChaCha20-Poly1305 密钥** – 用于加密 payload - **96 位 nonce** – 每次构建唯一,防止不同运行产生相同的密文 - **128 位身份验证标签** – Poly1305 MAC,验证密文在加密后未被篡改 它们中的每一个都与随机的一次性密码本进行 XOR 掩码处理,并存储在 stub 内部的 `CRYPTO_PARAMS` 结构中。真实值永远不会以明文存储 —— 只会写入 `value XOR mask` 和 `mask`,因此十六进制编辑器无法直接提取密钥。必须同时具备这两部分才能重构任何内容。 ### 5. 加密 payload 使用 ChaCha20-Poly1305 加密准备好的二进制文件。这是经过身份验证的加密 —— 如果在加壳后哪怕修改了密文的一个字节,解密也会失败(并且 stub 会中止运行)。 ### 6. 随机化节区名 将 stub 中的所有节区重命名为随机的 5 字母名称(例如 `.akgf`、`.tbxp`)。分析师和自动化工具通常会寻找 `.text`、`.rdata`、`.data` —— 随机化名称会破坏这种模式。 ### 7. 编译时字符串加密 stub 中的每个字符串字面量(`"kernel32.dll"`、`"user32.dll"`、`"NtDelayExecution"`、错误消息等)都会在编译时使用 `xor_str` 宏进行加密。每个字符串都会根据其字节使用黄金比率哈希(`0x9E37_79B9 * 31 + byte`)获得一个基于内容的唯一 XOR 密钥。`.rdata` 中只存在经过 XOR 掩码处理的字节;明文绝不会出现在二进制文件中。在运行时,字符串会在首次使用时在栈上解密,并且存放密钥的栈槽会被立即清零。 这取代了原本对于 `strings` 命令扫描来说如同金矿般的明文 —— 分析师在二进制文件上运行 `strings.exe` 将找不到任何可读内容。 ### 8. 将 CRYPTO_PARAMS 修补进 stub 加壳器通过搜索其 16 字节的签名(`PEPROTECTOR_SIG!`)来定位 stub 二进制文件中的 `CRYPTO_PARAMS`。然后,它将所有运行时参数写入该结构体: ``` Offset Size Field 0 16 Signature (search marker: `PEPROTECTOR_SIG!`) 16 32 key_mask (random OTP for key) 48 32 key_xored (key XOR key_mask) 80 12 nonce_mask 92 12 nonce_xored 104 8 payload_size (LE u64) 112 8 section_name (8-byte randomized name of the payload section) 120 4 flags (bit 0=compressed, 1=anti_debug, 2=watermark, 3=hook_detect, 4=show_error, 5=anti_vm, 6=reduce_entropy) 124 16 tag_mask 140 16 tag_xored (Poly1305 auth tag XOR tag_mask) 156 bytes total ``` ### 9. 将加密 payload 作为新 PE 节区追加 使用记录在 `CRYPTO_PARAMS` 中的随机名称向 stub 添加一个新节区。原始数据就是加密的密文。Windows 会延迟映射 PE 节区 —— 具有大 `VirtualSize` 的节区只会在实际被触碰时才分配物理页,因此巨大的 payload 不会浪费 RAM。 ### 10. 复制资源和 overlay 从原始 EXE 中提取 `.rsrc` 节区(图标、版本信息、manifest),并将其附加到 stub 中,以便受保护的二进制文件在 Explorer、任务管理器和 VirusTotal 元数据视图中看起来完全相同。任何尾部的 overlay 数据(由安装程序或自解压程序附加到原始二进制文件末尾的数据)都会原样保留。 ### 11. 混淆 PE 头 应用在你的配置中启用的静态分析干扰措施: - **垃圾导入** – 注入一个带有听起来真实的 DLL/函数名(`NtQueryInformationProcess`、`IsDebuggerPresent` 等)的虚假导入表。stub 在跳转到 payload 之前会将这些清零,因此它们永远不会执行。 - **诱饵节区** – 附加一个 12 KB 的虚假可执行节区,其 `VirtualSize` 膨胀到了 512 MB。IDA 和 Ghidra 会花费数秒钟试图反汇编这 512 MB 看起来像结构化代码的内容。Windows 只会提交实际的 12 KB 物理数据。 - **伪造证书** – 嵌入一个虚假的 Authenticode 证书结构,并将 PE 的安全目录指向它。非专业的工具会报告该二进制文件已“签名”。 ### 12. 打印体积报告 显示一个包含 原始 → 剥离 → 压缩 → 加密 → 最终 体积及其百分比的表格。这对于调整 `compression_level` 或决定是否值得为特定目标启用压缩非常有用。 ## 构建 ``` cargo build --release ``` 将所有内容编译到 `target/release/` 中。加壳器最终名为 `protector.exe`,stub 为 `stub.exe`。你处于 Windows x64 环境下,这也是此项目唯一的目标平台。一切都由 Windows 特定的 feature 标志保护。 由于加密库依赖关系,第一次构建需要一些时间。重新构建则很快。通常你只需要重新构建一次 stub(如果你修改了它),然后就可以在许多次加壳操作中重复使用同一个 stub 二进制文件。 ## 用法 基本命令: ``` protector.exe -i target.exe -s stub.exe -o protected.exe ``` 就是这样。加壳器读取你的目标程序,读取 stub,将它们打包在一起,并输出结果。控制台输出会显示每一步发生了什么 —— 请留意压缩统计数据,看看压缩是否对你的特定二进制文件确有帮助。 **典型工作流:** 1. 构建一次 stub:`cargo build --release -p stub` 2. 将 `target/release/stub.exe` 复制到某个永久位置或你的 CI pipeline 中。 3. 对不同的目标重复运行加壳器,重复使用同一个 stub。 如果你要对多个二进制文件进行加壳,stub 在构建方面是瓶颈,所以构建一次就可以了。 ## 配置 在运行加壳器的同一目录下放置一个 `protector.yaml` 文件以自定义行为: ``` anti_debug: true anti_vm: true hook_detect: true show_error: true junk_imports: true decoy_section: true fake_cert: true watermark: false reduce_entropy: true compression_level: 4 ``` 所有设置都是可选的。缺失的设置会使用上面显示的默认值。以下是每个旋钮的作用: - **`anti_debug`**:在解密代码前运行 13 种不同的反调试检查。 - **`anti_vm`**:查询 RAM、CPU 核心数和运行时间以检测 VM 沙盒。 - **`hook_detect`**:扫描 ntdll 中的关键 NT 函数以寻找内联 hook。 - **`show_error`**:在安全检查失败时显示自定义的 GDI 消息框,而不是静默退出。 - **`junk_imports`**:注入虚假的导入描述符以混淆静态分析工具。 - **`decoy_section`**:附加一个带有诱饵函数的虚假代码节区以阻碍反汇编器。 - **`fake_cert`**:嵌入一个虚假的 Authenticode 证书结构。 - **`watermark`**:后台线程将水印附加到窗口标题。 - **`reduce_entropy`**:对 payload 进行 Base64 编码以降低节区熵值。 - **`memory_encryption`**:就地对内存进行 XOR 加密,并在 VEH 内部进行解密。 - **`compression_level`**:控制 Deflate 压缩级别 (0-9)。 加壳器最后会打印出它应用了哪些设置,以便你可以验证你的配置是否确实生效了。 ## 加载器在运行时的工作原理 当有人运行你受保护的二进制文件时,stub 会首先接管。在你的原始代码获得第一条 CPU 指令之前,会发生以下事情: ### 阶段 1 – API 解析(无 IAT) stub 的导入表被刻意保持在最小规模。所有的 Windows API 地址都是通过遍历 `PEB.InLoadOrderModuleList` 并使用 ROR13 对每个导出名称进行哈希处理来动态解析的。目标函数名称的哈希值会与预先计算的编译时常量进行比较 —— 二进制文件中永远不会出现字符串。这意味着静态导入扫描几乎显示不出任何有用的信息。 ### 阶段 2 – Syscall 基础设施 stub 需要进行原始的 NT syscall(例如 `NtDelayExecution`、`NtQueryInformationProcess`),而不经过 ntdll 中被 hook 的包装器(EDR 会修补这些包装器以拦截调用)。会按顺序尝试三种回退策略: 1. **\KnownDlls 映射** – 打开 `\KnownDlls\ntdll.dll`、`\KnownDlls\kernel32.dll` 和 `\KnownDlls\kernelbase.dll`(内核保存在只读节区对象中的启动时的干净副本)并将它们映射到进程中。EDR 尚未修补这些副本。stub 的 `Apis` 结构体中的**所有**函数指针都是通过这些干净的映像解析的,因此每一次 API 调用(VirtualAlloc、LoadLibraryA、CreateThread 等)都通过未挂钩的路径进行。新鲜的 ntdll 也用于 SSN 提取;干净的 kernel32/kernelbase 处理转发的导出(例如 kernel32 → kernelbase 链),而不会退回到被挂钩的原始版本。 2. **Hell's Gate** – 如果无法使用干净的 ntdll,则直接从内存中 stub 的前 8 个字节(`mov r10, rcx` / `mov eax, `)读取 SSN。 3. **Tartarus Gate** – 如果前几个字节被挂钩了(例如被 `JMP rel32` 替换了),但在 stub 更深处仍保留着 `mov eax`,则扫描最多 32 个字节来寻找它。 4. **Halo's Gate** – 如果整个 stub 都被替换了,则通过以已知的步长(0x10、0x20、0x30 或 0x40)读取相邻的干净 stub 并加上索引差值来推导 SSN。 为了进行分发,stub 会在 ntdll 自身的可执行节区中寻找一个 `syscall; ret` gadget 并跳转到它,而不是直接内联发出 `syscall` 指令。内核看到的返回地址是 ntdll,这满足了现代 EDR 使用的调用堆栈验证(CSV)检查。 ### 阶段 3 – 安全检查 所有检查都在解密之前进行。任何失败都会打开带有特定错误代码和消息的自定义 GDI 错误窗口,然后退出。 **反调试检查**(如果 `anti_debug: true`): | # | 检查 | 技术 | | --- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | 300ms 睡眠 | 让迟附着的调试器稳定下来,然后捕获它 | | 2 | 线程隐藏 | 通过直接 syscall 调用 `NtSetInformationThread(ThreadHideFromDebugger)` —— x64dbg 会丢失此线程的所有调试事件 | | 3 | ntdll 修补 | 覆盖 `DbgUiRemoteBreakin` → `JMP $`(无限循环)和 `DbgBreakPoint` → `RET`,因此附着调试器会挂起或静默失败 | 4 | `IsDebuggerPresent()` | Win32 API 检查 | | 5 | `PEB.BeingDebugged` | 直接读取 `gs:[0x60]+0x02` | | 6 | `PEB.NtGlobalFlag` | `+0xBC` 处的堆调试标志;存在调试器时值为 `0x70` | | 7 | VEH 硬件断点探测 | 通过内联汇编设置 CPU 陷阱标志,在 VEH 处理程序中捕获 `EXCEPTION_SINGLE_STEP`,从上下文读取 DR0–DR3 —— 即使 `GetThreadContext` 被挂钩也能捕获断点 | | 8 | 硬件断点 | 跨所有线程通过 `GetThreadContext` 读取 `CONTEXT.Dr0–Dr3` | | 9 | `OutputDebugStringA` 技巧 | 将 `LastError` 设置为一个哨兵值,调用 `OutputDebugString`,检查调试器是否将其重置为 0 | | 10 | 调试端口 | 通过直接 syscall 调用 `NtQueryInformationProcess(ProcessDebugPort=7)` | | 11 | INT3 扫描 | 检查 `NtQueryInformationProcess` 的第一个字节是否为 `0xCC`(调试器断点) | | 12 | RDTSC 计时 | 测量紧凑的 1000 次迭代循环;单步执行会将时间差膨胀到远超 1000 万个周期 | | 13 | 已加载 DLL 扫描 | 遍历 PEB.InMemoryOrderModuleList,哈希每个 DLL 名称(不区分大小写的 ROR13),标记与已知的调试器/沙盒 DLL(ScyllaHide、Sandboxie、dbghelp、dbgcore、x64dbg、IDA、Skylleen)的匹配项 | **反虚拟机检查**(如果 `anti_vm: true`): - **运行时间**:直接检查 `KUSER_SHARED_DATA` 以确保系统已运行超过 2 分钟。快速的沙盒重置将无法通过此项。 - **硬件资源**:通过直接 syscall 使用 `NtQuerySystemInformation` 验证系统是否至少有 4 个 CPU 核心和 >4GB 的 RAM。分析 VM 通常资源受限。 **EDR hook 检测**(如果 `hook_detect: true`):扫描 ntdll 中 11 个关键的 `Nt*` stub,寻找已知的内联 hook 字节模式(`E9` JMP rel32、`FF 25` JMP [RIP+x]、`CC` INT3、`EB` 短 JMP、`49 BB` MOV R11 imm64)。只有在修补了 **2 个或更多** stub 时才返回检测结果 —— 单个函数 hook 在合法软件中很常见(自定义分配器、API 记录器),而 EDR 总是会挂钩多个函数。 ### 阶段 4 – 解密 密钥、nonce 和 Poly1305 验证标签通过将它们存储的掩码部分进行 XOR 运算来重构。ChaCha20-Poly1305 一步完成解密和验证 —— 如果验证失败(payload 字节在加壳后被更改),stub 将退出而不触碰明文。解密后立即: - 使用 volatile 写入将密钥、nonce 和标签用零覆盖(编译器无法将其消除) - stub 自身映像中的加密 payload 节区被清零(通过 `VirtualProtect` 短暂设置为 RW,然后擦除),这样内存转储也就找不到密文了 ### 阶段 5 – 手动 PE 映射 解密后的字节是一个完整的 PE 映像。stub 会手动映射它: 1. 在 PE 的首选基址处 `VirtualAlloc`;如果被占用,则回退到任何可用的地址 2. 将 PE 头和所有节区复制到分配的区域中 3. 如果映像未在其首选基址加载,则应用基址重定位(`.reloc` 中的 `IMAGE_REL_BASED_DIR64` 条目) 4. 解析正常的导入表(`IMAGE_DIRECTORY_ENTRY_IMPORT`)—— 为每个 DLL/函数执行 `LoadLibraryA` + `GetProcAddress` 5. 解析延迟加载导入(`IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT`)—— 许多现代 EXE 使用这些;如果没有这一步,延迟加载 thunk 在首次调用时就会发生异常 6. 使用 `RtlAddFunctionTable` 注册 `.pdata` 异常表 —— 这是 x64 SEH 和 Rust panic 在映射的映像中正确展开所必需的 7. 设置 TLS(`IMAGE_DIRECTORY_ENTRY_TLS`)—— 分配 TLS 索引,为主线程复制 TLS 原始数据模板,并发布 `PAYLOAD_TLS_STATE`,以便 stub 自身的 TLS callback 能为每个新线程触发 payload 的 `DLL_THREAD_ATTACH`/`DETACH` callback 8. 恢复每个节区的内存保护(代码为 `RX`,数据为 `RW`,常量为 `RO`)—— 节区最初映射为 `RW`,以便重定位和 IAT 修补可以对其进行写入 9. 在 OEP 跳转之前按顺序调用 payload TLS callback(`DLL_PROCESS_ATTACH`) ### 阶段 6 – 内存加密 (VEH²) 如果 `memory_encryption: true`,stub 会在 OEP 转移之前立即对加载的 payload 映像的每一个字节(不包括 PE 头)进行 XOR 加密。加密使用由 `image_base * 2654435761 + !entry_rva` 设置种子的逐页 LCG,每一页通过 `key ^ page_index` 获得唯一密钥。这确保了内存转储工具和 `ReadProcessMemory` 快照只能看到密文。 解密发生在一个 **向量化异常处理程序 (VEH)** 内部:stub 注册一个 VEH 处理程序,对映像进行加密,然后执行 `int3`。处理程序触发,在 VEH 上下文中(EDR 无法在此拦截 `xor_image` 循环)对映像进行解密,将 RIP 推进越过断点,然后返回。此时,控制权落入 OEP 跳转,所有内存均已恢复。 关键细节: - **VEH 而非内联** – XOR 循环在异常处理程序上下文中运行,而不是在正常执行路径中,使得其在巡逻主代码流的 EDR hook 扫描器面前不可见 - **亚毫秒级窗口** – 加密仅存活于 VEH 分发期间,在现代硬件上通常不到 10 µs - **VEH 内无 VirtualProtect** – 映像页在加密前被设置为 `PAGE_EXECUTE_READWRITE`,因此处理程序可以在不调用 `VirtualProtect` 的情况下读/写它们(由于 VAD 锁,在 VEH 内部调用它会导致死锁) - **所有 API 均在运行时解析** – `AddVectoredExceptionHandler` 与其他所有 API 一样,是通过相同的 ROR13 哈希机制从 kernel32 解析的;没有静态导入条目 ### 阶段 7 – 清理和 OEP 跳转 就在转移控制权之前,stub 会抹除自己的痕迹: - **将解密后的 PE 缓冲区清零**(逐个字节进行 volatile 写入)—— 映射后明文绝不会留在内存中 - **清空 stub 自身的 IAT** —— 遍历每一个 `IMAGE_IMPORT_DESCRIPTOR`,将描述符和已填充的函数指针槽都清零。同时也会将 `DataDirectory[1]` 清零,这样就没有指针能指回现已为空的链。内存扫描器将无法看到 stub 导入了哪些 DLL。 - **将主模块条目的 `PEB.ImageBaseAddress` 和 `LDR.DllBase` 修补**为 payload 的映射地址,以便 `GetModuleHandle(NULL)` 在 payload 内部返回正确的基址 - **注册 CFG 调用目标** – 如果 payload 是使用 `/guard:cf` 编译的,则其所有页面都需要通过 `SetProcessValidCallTargets` 注册为有效的间接调用目标,否则第一次间接调用就会崩溃 - **OEP 页面检查**(如果 `anti_debug: true`)—— 对入口点页面调用 `VirtualQuery`。如果页面类型不是 `MEM_PRIVATE`,则说明 hook 引擎已用映射/映像支持的视图替换了这些页。显示错误屏幕并退出。 - **破坏映射映像中的 MZ/PE 签名**(仅限控制台子系统)—— 将前 2 个字节(`MZ`)和 `PE\0\0` 签名清零。寻找这些魔数的内存转储工具和进程扫描器将无法识别该区域为 PE。 然后,stub 将 RSP 对齐到 16 字节边界,减去 `0x28`(影子空间 + 返回地址槽),并直接跳转到 OEP —— 没有 `CALL`,因此 payload 永远不会返回到此处。 ### OEP 之后 – 水印线程(可选) 如果 `watermark: true`,则在 OEP 跳转之前会生成一个后台线程。它使用直接的 `NtDelayExecution` syscall(没有 ntdll hook 可以拦截它)每秒循环一次,并且: - 通过 `EnumWindows` 枚举属于当前进程的所有顶级窗口 - 将 `" (protected by gs60)"` 附加到任何尚未包含该内容的窗口标题中 - 通过 `GetConsoleTitleW` / `SetConsoleTitleW` 对控制台窗口标题执行相同的操作 从进程启动到 OEP 的整个序列只需几毫秒,对正在运行的 payload 代码完全透明。 ## 沙盒 `sandbox` crate 是一个自包含的测试平台,用于验证 stub 的防御能力。它包含五个二进制文件: | Binary | 功能 | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `hooker` | 将 EDR 风格的内联 hook 安装到目标进程中。使用 `--spawn` 在 stub 运行之前进行注入。 | | `dbgr` | 附着到目标进程的微型调试器(需要 CLI 参数),用于验证反调试检查是否正确触发。 | | `test_app` | 一个简单的循环二进制文件 —— 用加壳器打包它作为可预测的沙盒目标。 | | `crackme` | 一个简单的控制台 crackme,在给出正确密码时会打印一个 flag。 | | `verify_decoy` | 验证受保护二进制文件中的诱饵节区结构 —— 检查序言、调用图、跳转表和外部交叉引用。 | ### 使用 hooker stub 的安全检查在启动时、在你的 payload 的 `main()` 被到达之前运行。挂钩一个已经通过了 stub 阶段的、正在运行的进程对这些检查没有影响。请始终使用 `--spawn`: ``` # Spawn 目标挂起,安装 hooks,然后恢复 – 检查在 hooks 就位的情况下运行 sandbox\hooker.exe --spawn protected.exe --mode edr-detect # Hook 正在运行的进程(仅对测试 post-OEP payload 代码有用) sandbox\hooker.exe protected.exe --mode anti-debug ``` 可用的 `--mode` 值: | Mode | 安装的内容 | 预期结果 | | ------------ | ----------------------------------------------------------------- | ------------------------------------------------------- | | `anti-debug` | `NtDelayExecution` 上的 `E9`,`NtQueryInformationProcess` 上的 `FF25` | Stub 继续 —— 直接 syscall 会绕过这些 | | `edr-detect` | 跨 5 个 Nt\* 函数的所有 5 种 hook 字节模式 | `detect_edr_hooks()` 触发 → stub 显示错误并退出 | | `halo-gate` | `NtDelayExecution` 上的垃圾序言 | Halo's Gate 恢复正确的 SSN → stub 工作 | | `all` | 按顺序的所有三个阶段 | 上述各项的组合 | 默认模式是 `edr-detect`。 ### 构建沙盒 ``` cargo build --release -p sandbox ``` 在 `target/release/` 中输出 `dbgr.exe`、`hooker.exe`、`test_app.exe`、`crackme.exe` 和 `verify_decoy.exe`。 ## 项目布局 ``` pe-protector/ |-- protector/ # Packer CLI | `-- src/ | |-- main.rs # CLI orchestration, size report | |-- packer.rs # Payload packing, CRYPTO_PARAMS patching | |-- pe.rs # PE helpers, metadata stripping, section ops | |-- crypto.rs # ChaCha20 helpers | |-- obfuscate.rs # Junk imports, decoy section, fake cert | `-- config.rs # protector.yaml loading |-- stub/ # Runtime loader | `-- src/ | |-- main.rs # CRYPTO_PARAMS layout, TLS callbacks, entry point | |-- loader.rs # Manual PE mapping, TLS, CFG, OEP transfer | |-- api.rs # PEB walk + ROR13 export resolution | |-- syscall.rs # Direct/indirect syscall helpers | |-- veh_encrypt.rs # VEH² memory encryption (XOR via VEH handler) | |-- veh_breakpoint.rs # VEH-based HWBP detection (Trap Flag probe) | |-- anti/ # Anti-analysis checks | | |-- debug.rs # Anti-debug checks | | |-- vm.rs # Anti-VM checks | | |-- tools.rs # DLL scanning, TitanHide, parent process | | `-- fake.rs # Decoy checks to waste reverser time | |-- hook_detect.rs # EDR hook scanning | |-- error_screen.rs # Custom GDI error window | |-- error_codes.rs # Error code constants | |-- fresh_ntdll.rs # Map clean ntdll from KnownDlls | |-- xor_str.rs # Compile-time XOR string encryption macros | |-- types.rs # PE/PEB/TLS struct definitions | `-- crypto.rs # Base64 decoding for entropy reduction |-- sandbox/ # Test harness (hooker, dbgr, test_app) |-- xtask/ # Build helpers `-- Cargo.toml # Workspace manifest ``` ## 技术细节 ### CRYPTO_PARAMS 结构体(156 字节,嵌入在 stub 中) 这是加壳器和加载器之间的握手。加壳器通过搜索 16 字节的签名来定位它,并在写入输出二进制文件之前就地写入所有运行时参数。 ``` Offset Size Field Description 0 16 sig Search marker (`PEPROTECTOR_SIG!`) – packer finds the struct by this 16 32 key_mask Random OTP for the ChaCha20 key 48 32 key_xored key XOR key_mask – real key only exists briefly at runtime 80 12 nonce_mask Random OTP for the nonce 92 12 nonce_xored nonce XOR nonce_mask 104 8 payload_size Size of encrypted payload in bytes (LE u64) 112 8 section_name Randomized name of the PE section holding the payload 120 4 flags bit 0=compressed, 1=anti_debug, 2=watermark, 3=hook_detect, 4=show_error, 5=anti_vm, 6=reduce_entropy, 7=memory_encrypt, 7=memory_encrypt 124 16 tag_mask Random OTP for the Poly1305 auth tag 140 16 tag_xored tag XOR tag_mask 156 bytes total ``` 在运行时,stub 对每个掩码对进行 XOR 运算以恢复真实值,使用它们,然后立即将这三者全部清零:掩码、经过 XOR 运算的值以及恢复的明文。Volatile 写入确保编译器永远不会省略清零操作。 ### 文件职责 **Stub crate** (`stub/src/`): - `main.rs` - 入口点,解析 API,初始化 syscall 基础设施。 - `loader/mod.rs` - 编排完整的 payload 加载:PE 映射、导入解析、TLS 设置、异常表注册、CFG、OEP 跳转。 - `loader/pe_map.rs` - PE 头解析、节区复制、重定位修复、权限恢复。 - `loader/imports.rs` - 正常和加载导入解析,IAT 清空。 - `loader/tls.rs` - TLS 索引分配、callback 调用、CFG 目标注册。 - `loader/watermark.rs` - 将水印附加到窗口标题的后台线程。 - `api.rs` - PEB 遍历和 ROR13 哈希,以在没有 IAT 的情况下动态解析 Windows API。 - `syscall.rs` - 使用 Hell's Gate / Tartarus Gate / Halo's Gate 的直接/间接 syscall stub 和 SSN 提取。 - `veh_encrypt.rs` - VEH² 内存加密:注册 VEH 处理程序,加密 payload 映像,然后通过 `int3` 在处理程序内部对其进行解密。 - `veh_breakpoint.rs` - 基于 VEH 的硬件断点检测:设置 CPU 陷阱标志,在 VEH 处理程序中捕获 `EXCEPTION_SINGLE_STEP`,从异常上下文中检查 DR0–DR3。 - `types.rs` - PE/PEB/TLS 结构体定义、上下文寄存器偏移量。 - `anti/debug.rs` - 反调试检查(睡眠、线程隐藏、PEB 检查、VEH 硬件断点探测、所有线程上的硬件断点检测、看门狗)。 - `anti/vm.rs` - 反虚拟机检查(CPUID、RAM、核心数、运行时间)。 - `anti/tools.rs` - DLL 扫描、TitanHide 检测、父进程验证。 - `anti/fake.rs` - 用于浪费逆向人员时间的诱饵检查。 - `hook_detect.rs` - EDR hook 检测:扫描 Nt\* stub 以寻找内联 hook 字节模式(JMP rel32、JMP [RIP+x]、INT3 等)。在修补了 2 个或更多 stub 时触发。 - `error_screen.rs` - 自定义 GDI 错误窗口。在任何安全检查失败时显示,而不是静默退出。显示错误代码和消息。 - `error_codes.rs` - 错误屏幕使用的错误代码常量。 - `fresh_ntdll.rs` - 从 \KnownDlls\ 映射 ntdll、kernel32 和 kernelbase 的干净副本,用于未挂钩的 API 解析和 SSN 提取。`resolve_forwarder` 会查询 `CLEAN_BASES` 静态变量,以便通过干净的映像解析转发的导出。 - `xor_str.rs` - 编译时 XOR 字符串加密宏(`enc_cstr!` 用于以 null 结尾的字符串,`enc_str!` 用于原始字节)。 - `crypto.rs` - 用于降低熵值的 Base64 解码。 **Protector crate** (`protector/src/`): - `main.rs` - CLI 解析、编排、打印体积报告表。 - `packer.rs` - 核心 `pack()` 函数:剥离元数据、压缩、加密、修补 stub。 - `pe.rs` - PE 头解析/操作(读/写节区、剥离调试信息、计算校验和)。 - `crypto.rs` - ChaCha20 流密码。 - `obfuscate.rs` - 垃圾导入注入、伪造证书生成、诱饵节区创建。 - `config.rs` - 加载并解析 `protector.yaml` 配置。 ### 关键设计决策 **手动 PE 映射** - 对映像布局、保护和执行进行完全控制。不依赖 Windows 加载器的特性。 **ROR13 API 解析** - 遍历 PEB,哈希导出名称,在运行时解析 API。没有静态 IAT 意味着没有可供挂钩的明显导入地址。 **间接 syscall** - 使用 ntdll 内部的 `syscall; ret` gadget 而不是内联发出 `syscall`。内核将 ntdll 视为调用者,满足了现代 EDR 使用的调用堆栈验证检查。 **XOR 掩码密钥** - ChaCha20 密钥和 nonce 仅在需要它们的短暂时刻在内存中呈现为明文。在绝大多数时间里,它们以 XOR 掩码的形式存放在 `CRYPTO_PARAMS` 中。 **逐构建随机化** - 每个受保护的二进制文件都有不同的节区名、密钥、nonce、垃圾导入等。没有两个加壳后的二进制文件看起来是一样的。降低了基于签名的检测率。
标签:DNS 反向解析, PE打包器, Rust, 反逆向工程, 可视化界面, 混淆加密, 网络流量审计, 软件保护, 通知系统