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, 反逆向工程, 可视化界面, 混淆加密, 网络流量审计, 软件保护, 通知系统