OmarShehata11/Mirage
GitHub: OmarShehata11/Mirage
Mirage 是一款 Windows 内核驱动程序,通过加权评分机制检测恶意软件在内核层使用的反分析技术。
Stars: 0 | Forks: 0
MIRAGE
Windows 上反分析行为的内核模式检测
## 概述 Mirage 是一个 **Windows Driver Model (WDM) 内核驱动程序**,用于检测恶意软件为阻碍取证检查而使用的反分析技术。它在内核层观察进程创建、线程创建和 Object Manager 句柄事件,定期扫描近期创建的进程以检查可疑状态,并向用户模式控制器发送针对特定技术的行为事件。 每个检测到的技术都会为每个进程的运行总分贡献一个加权分数。超过可配置阈值(默认为 **7.0 / 13.0**)的进程将被标记为表现出反分析行为。 ### 与现有工具的不同之处 | 工具 | 功能 | 局限性 | |------|-------------|-------------------| | **Sysmon** | 记录进程/网络/注册表遥测数据 | 不会将反分析作为信号类别呈现 | | **商业 EDR** | 内部使用 ETW-TI 进行自我保护 | 特定于供应商,分类器不透明 | | **ScyllaHide / TitanHide** | 向反调试技术隐藏分析师的调试器 | 由分析师部署,并非防御监视器 | | **Mirage** | **检测反分析行为并将其作为事件呈现** | 不执行阻止——仅进行观察 | ## 架构 ``` graph TB subgraph USER["USER MODE"] GUI["GUI ControllerJohannes"] AGG["Event Aggregator
+ Policy Engine"] LOG["JSONL Logger"] GUI --> AGG --> LOG end subgraph IOCTL["DeviceIoControl · poll ~200ms"] IRP["IRP_MJ_DEVICE_CONTROL
→ MirageRingBufferDrain()
→ memcpy to SystemBuffer
→ IoCompleteRequest"] end subgraph KERNEL["KERNEL MODE · PASSIVE_LEVEL"] RING["Ring Buffer
128 × MIRAGE_EVENT (544 B)
KSPIN_LOCK · ~68 KiB pool"] subgraph PRODUCERS["Detection Producers"] SWEEP["Sweep Thread
500ms interval
TTL 20 sweeps"] OBCB["ObCallback Pre-Op
altitude 385000
PsProcessType"] TNOTIFY["Thread Notify
PID == 4 filter
AuxKlib module walk"] end TABLE["Tracked Process Table
LIST_ENTRY + FAST_MUTEX
per-PID score + bitmask"] PNOTIFY["PsSetCreateProcessNotifyRoutineEx
add on create · remove on exit"] end GUI -.->|"IOCTL_MIRAGE_GET_EVENTS"| IRP IRP -->|drain| RING SWEEP -->|emit| RING OBCB -->|emit| RING TNOTIFY -->|emit| RING SWEEP -->|"snapshot / probe"| TABLE PNOTIFY -->|"add / remove"| TABLE style USER fill:#1a1a2e,stroke:#3d3d4f,color:#fafafa style KERNEL fill:#0e0e14,stroke:#991b1b,color:#fafafa style IOCTL fill:#1a1a24,stroke:#f59e0b,color:#f59e0b style RING fill:#070710,stroke:#f59e0b,color:#f59e0b style TABLE fill:#070710,stroke:#10b981,color:#10b981 style PRODUCERS fill:#1a1a24,stroke:#dc2626,color:#fafafa ``` ## 检测技术 ``` graph LR subgraph SCORING["Per-Process Scoring · threshold 7.0 / 13.0"] T1["Self-Debug Attach
+1.0
ProcessDebugPort"] T2["Big Memory
+2.0
ProcessVmCounters"] T3["Hidden Sys Thread
+2.5
AuxKlib module walk"] T4["Job Kill-on-Close
+2.5
ZwQueryInfoJobObject"] T5["ObCallback Strip
+2.0
altitude comparison"] T6["ThreadHideFromDbg
+3.0
⚠️ planned"] end T1 --> SUM["Running
Total"] T2 --> SUM T3 --> SUM T4 --> SUM T5 --> SUM T6 -.->|"not implemented"| SUM SUM -->|"≥ 7.0"| FLAG["🚩 FLAGGED"] SUM -->|"< 7.0"| OK["✓ clean"] style T6 fill:#2a1f08,stroke:#f59e0b,color:#f59e0b style FLAG fill:#991b1b,stroke:#dc2626,color:#fafafa style OK fill:#0f2a1c,stroke:#10b981,color:#10b981 ``` | # | 技术 | 权重 | Kernel API 路径 | 检测类型 | 状态 | |---|-----------|--------|----------------|---------------|--------| | 01 | **自调试附加 (Self-debug attach)** | 1.0 | `ZwQueryInformationProcess(ProcessDebugPort)` | 扫描 | ✅ 已发布 | | 02 | **大内存反检查 (Big-memory anti-inspect)** | 2.0 | `ZwQueryInformationProcess(ProcessVmCounters)` | 扫描 | ✅ 已发布 | | 03 | **隐藏系统线程 (Hidden System thread)** | 2.5 | `PsSetCreateThreadNotifyRoutine` + `AuxKlibQueryModuleInformation` | 事件驱动 | ✅ 已发布 | | 04 | **Job 关闭时终止 (Job kill-on-close)** | 2.5 | `PsGetProcessJob` → `ObOpenObjectByPointer` → `ZwQueryInformationJobObject` | 扫描 | ✅ 已发布 | | 05 | **ObCallback 句柄剥离 (ObCallback handle strip)** | 2.0 | `ObRegisterCallbacks` 在 altitude `385000` 的 pre-op | 事件驱动 | ✅ 已发布 | | 06 | **ThreadHideFromDebugger** | 3.0 | ETW-TI 或直接读取 `ETHREAD` 偏移量 | — | ⚠️ 计划中 | ### 为什么将 ThreadHideFromDebugger 排除在外 存在两种检测路径。**ETW-TI** 很干净且构建稳定,但需要 PPL 保护的驱动程序(仅限于 Microsoft 签名的二进制文件)。**直接读取 ETHREAD** 虽然可行,但使用了硬编码的结构体偏移量,这些偏移量在每次 Windows 构建版本中都会更改。在不同版本中保持五个稳定的检测,胜过六个其中之一在每次 Patch Tuesday 都会失效的检测。 ## 源码结构 ``` Mirage/ ├── Main.c # DriverEntry, DriverUnload, IRP dispatch ├── Mirage.h # Manual type decls, function prototypes, defensive #ifndef guards ├── MirageSweep.c # Tracked-process table, sweep thread, two-pass architecture ├── MirageTechniquesCovered.c # Self-debug, big-memory, job kill-on-close, AuxKlib helper ├── MirageObCallback.c # ObRegisterCallbacks registration + pre-op handler ├── MirageShared.h # Wire-format MIRAGE_EVENT struct, IOCTL codes (shared with user mode) ├── MirageRingBuffer.c # Ring buffer + emit/drain + stats │ ├── Tests/ │ ├── TestSelfDebug.c # user-mode · DebugActiveProcess on a child │ ├── TestBigMemory.c # user-mode · VirtualAlloc 1.5 GB │ ├── TestHiddenSystemThread.c # kernel · shellcode in pool + PsCreateSystemThread │ ├── TestKillOnClose.c # user-mode · job with KILL_ON_JOB_CLOSE │ ├── TestObStripDriver.c # kernel · ObCallback at altitude 1000 that strips PROCESS_TERMINATE │ └── TestObStrip.c # user-mode · OpenProcess(TERMINATE) on a target through the test driver │ └── Paper/ ├── main.tex # ACM SIGCONF research paper └── references.bib # 16 references ``` ## 双趟扫描设计 扫描线程每 **500ms** 运行一次,并探测每个被追踪的进程。该架构将每次扫描分为两个阶段,以限制锁保持时间: ``` sequenceDiagram participant ST as Sweep Thread participant TBL as Tracked Process Table
(FAST_MUTEX) participant KERN as Kernel APIs
(Zw*, Ps*, Ob*) participant RING as Ring Buffer
(KSPIN_LOCK) Note over ST: Phase 1 — snapshot (lock held) ST->>TBL: acquire FAST_MUTEX ST->>TBL: walk LIST_ENTRY, copy PIDs to stack buffer ST->>TBL: increment sweep counter per entry ST->>TBL: release FAST_MUTEX Note over ST: Phase 2 — probe (no lock) loop for each PID in snapshot ST->>KERN: MirageCheckSelfDebug(PID) ST->>KERN: MirageCheckBigMemory(PID) ST->>KERN: MirageCheckJobKillOnClose(PID) alt detection positive ST->>TBL: re-acquire mutex ST->>TBL: re-locate entry by PID (may be gone) ST->>TBL: update score + bitmask ST->>TBL: release mutex ST->>RING: MirageRingBufferEmit(event) end end ``` **为什么要分两趟?** 探测函数会调用 `ZwOpenProcess`、`ZwQueryInformationProcess`、`ObOpenObjectByPointer` 和 `ZwQueryInformationJobObject` —— 这些调用可能需要数毫秒,如果一直保持 mutex,将会不可接受地延长锁保持时间。在检测为阳性时通过 PID 重新定位,可以处理进程在快照和探测之间退出的竞态问题。 ## ObCallback Altitude 链 ``` graph LR A["Caller
OpenProcess(
PROCESS_TERMINATE)"] --> B subgraph CHAIN["Object Manager Callback Chain"] direction LR B["altitude 1000
Malware / Test Driver
strips PROCESS_TERMINATE"] --> C["altitude 95000
wdfilter.sys
(Defender)"] --> D["altitude 385000
Mirage
compares orig vs current"] end D --> E["Handle created
without TERMINATE"] style B fill:#991b1b,stroke:#dc2626,color:#fafafa style C fill:#1a1a24,stroke:#3d3d4f,color:#94a3b8 style D fill:#0f2a1c,stroke:#10b981,color:#10b981 ``` Mirage 注册在**高 altitude**,因此它会在链中**最后**运行。当我们的 pre-op 触发时,其他所有 callback 都已有机会修改 `DesiredAccess`。我们进行对比: ``` ACCESS_MASK stripped = OriginalDesiredAccess & ~DesiredAccess; if (stripped & PROCESS_TERMINATE) { // someone upstream stripped it → emit detection } ``` ## 隐藏系统线程检测流程 ``` flowchart TD A["Thread created anywhere
in the system"] --> B{PID == 4?} B -->|No| Z["return immediately
99%+ of callbacks"] B -->|Yes| C["Query thread start address
ZwQueryInformationThreadclass 9"] C --> D{"addr ≥
MM_SYSTEM_RANGE_START?"} D -->|No| Z2["skip
user-mode addr in PID 4
is a boot artifact"] D -->|Yes| E["Walk loaded modules
AuxKlibQueryModuleInformation"]
E --> F{"addr inside any[base, base+size)?"} F -->|Yes| Z3["legitimate
known driver code"] F -->|No| G["🚩 DETECTION
Hidden System thread
at unassociated address"] style G fill:#991b1b,stroke:#dc2626,color:#fafafa style Z fill:#1a1a24,stroke:#3d3d4f,color:#71717a style Z2 fill:#1a1a24,stroke:#3d3d4f,color:#71717a style Z3 fill:#0f2a1c,stroke:#10b981,color:#10b981 ``` ### 为什么不使用 `PsGetThreadStartAddress` 该函数存在于 `ntoskrnl.exe` 中,但在测试环境的 Windows 构建版本中**未被导出**(通过在 WinDbg 中使用 `x nt!PsGet*` 验证 —— 导出表中没有该符号)。`MmGetSystemRoutineAddress` 返回 `NULL`。我们退而使用 `ZwQueryInformationThread` 和信息类 `ThreadQuerySetWin32StartAddress`(值为 9)—— 这与 Process Hacker / System Informer 使用的路径相同。 ## 构建与部署 ### 前置条件 - 启用了**测试签名**(`bcdedit /set testsigning on`)的 Windows 11 虚拟机 - 安装了 **Windows Driver Kit (WDK)** 的 Visual Studio 2022 - WinDbg 或 [DbgView](https://learn.microsoft.com/en-us/sysinternals/downloads/debugview) 用于观察内核输出 ### 构建 1. 在 Visual Studio 中打开 `Mirage.sln` 2. 将目标设置为 **x64 / Debug** 3. 构建解决方案 (Ctrl+Shift+B) ### 安装与运行 ``` :: Copy Mirage.sys to the test VM sc create Mirage type= kernel binPath= "C:\Drivers\Mirage.sys" sc start Mirage :: Observe output :: Open DbgView as Administrator → Capture → Capture Kernel ``` ### 运行测试工具 ``` :: 1. Self-debug detection TestSelfDebug.exe :: → Mirage logs: ProcessDebugPort non-NULL on child PID :: 2. Big memory detection TestBigMemory.exe :: → Mirage logs: PrivateUsage >= 1 GB :: 3. Hidden System thread detection (kernel test driver) sc create TestHidden type= kernel binPath= "C:\Drivers\TestHiddenSystemThread.sys" sc start TestHidden :: → Mirage logs: thread in PID 4 at unassociated address sc stop TestHidden && sc delete TestHidden :: 4. Job kill-on-close detection TestKillOnClose.exe :: → Mirage logs: KILL_ON_JOB_CLOSE flag set :: 5. ObCallback handle-strip detection (two-part) sc create TestStripDrv type= kernel binPath= "C:\Drivers\TestObStripDriver.sys" sc start TestStripDrv TestObStrip.exe :: → Mirage logs: PROCESS_TERMINATE stripped on target PID sc stop TestStripDrv && sc delete TestStripDrv ``` ## 关键实现细节 ### 手动 API 声明 Mirage 使用的几个 Kernel API 存在于 `ntoskrnl.exe` 中,但在不同版本的 WDK 头文件中没有一致的声明: | API / 类型 | 问题 | 解决方案 | |-----------|-------|-----------| | `ZwQueryInformationJobObject` | 未声明 | 在 `Mirage.h` 中手动编写 `NTSYSAPI` 原型 | | `ZwOpenThread` | 未声明 | 手动编写 `NTSYSAPI` 原型 | | `PsGetProcessJob` | 受 `NTDDI_VERSION` 限制 | 手动编写 `NTKERNELAPI` 原型 | | `PsGetThreadStartAddress` | 在测试构建中未导出 | 回退到 `ZwQueryInformationThread` 类 9 | | `VM_COUNTERS_EX` | 不在内核头文件中 | 手动定义结构体 typedef | | `JOBOBJECT_EXTENDED_LIMIT_INFORMATION` | 不在内核头文件中 | 手动定义结构体 typedef | | `PROCESS_TERMINATE` | 并非总是被定义 | `#ifndef` 守卫 (0x0001) | | `ProcessVmCounters` | 信息类不在头文件中 | `#define ProcessVmCounters 3` | ### `ntifs.h` 与 `ntddk.h` 的问题 `MirageTechniquesCovered.c` 需要使用 `#include
以内核级别的决心和超乎寻常的 DbgPrint 输出量打造。
标签:DAST, EDR, Windows驱动开发, 内核态, 反分析检测, 客户端加密, 恶意软件分析, 脆弱性评估