Life45/revhv
GitHub: Life45/revhv
基于 Intel EPT 的 Type-2 Hypervisor,专为运行时追踪高度混淆或虚拟化的 Windows 内核驱动程序的控制流边界转移而设计。
Stars: 1 | Forks: 0
# revhv
revhv 是一个针对现代 Windows 系统的 type-2 Intel x86-64 hypervisor。该项目的目的是在静态逆向分析成本过高或存在盲区时,让高度混淆或虚拟化的二进制文件(尤其是内核驱动程序)变得更容易分析。它也非常适合作为深入分析混淆二进制文件之前的初步分析工具。其实现在设计上刻意贴近 Intel SDM,并专注于收集有价值的执行数据。
当前的追踪模型建立在受监控地址范围与系统其余部分之间的控制流转移之上。在实践中,这意味着 revhv 可以显示混淆目标在运行时实际触及了哪些内核 API 或其他外部代码路径。
最好将其视为一个动态单步执行引擎:当目标以任何方式将执行权转移到系统中的任何其他代码时,它就会介入,记录该事件,然后恢复执行,直到执行返回到目标,如此循环往复。
## 设计目标
- 尽可能保持 VMX-root 状态和执行与宿主操作系统隔离。
- 贴近硬件行为和 Intel SDM。
- 追踪控制流转移的开销要足够低,以便能够应用于真实目标。
- 保留足够的崩溃状态信息,使故障可诊断,而不是直接静默重启机器。
- 通过符号解析、模块快照和可配置的格式化,使捕获的数据具有实用价值。
## 仓库内容
- `revhv-km`:KMDF 驱动程序,负责进入 VMX 操作模式、虚拟化每个逻辑处理器、管理 EPT 状态、处理 VM-exit、暴露 hypercall,以及输出日志和追踪数据。
- `revhv-um`:用户态控制器,负责命令分发、符号解析、模块枚举、日志排空、追踪轮询、追踪配置和离线追踪解析。
- `common`:两个组件共用的追踪、hypercall、日志记录和导出格式。
## 状态与范围
本项目面向具有一定先验知识的逆向工程师,用于底层逆向和运行时分析。当前的实现已经可以用于发现不透明目标的跨边界执行,但该项目离完工还有很大距离,仍在积极开发中。
我已经准备好了一篇关于使用该项目进行示例分析的文章,并且更详细地解释了其内部原理和我的思考过程。文章只需要再润色一下。完成后,将可以在我的 [GitHub 个人主页](https://github.com/Life45) 上查看。
## 构建项目
其他依赖项已作为 git 子模块添加到每个项目组件的 `external` 文件夹下,因此建议使用递归方式克隆 git 仓库。
本项目以 Visual Studio 解决方案的形式提供。目前仅配置了 x64 Debug 配置。
构建 `revhv-km` 需要 WDK。
`revhv-km` 驱动程序既可以通过传统方法加载(当 DSE 被禁用时,例如测试签名模式),也可以通过手动映射加载。然而,驱动程序的 PE 和 NT 头会被其内存管理器用于映射宿主页表,而大多数手动映射工具在映射驱动程序时会剥离/忽略这些头,从而导致系统崩溃。因此,若要使用此类手动映射工具,必须对映射工具或 `revhv-km` 进行必要的实现更改。
在 `revhv-um` 中使用符号解析功能需要管理员权限。
`revhv-km` 和 `revhv-um` 均已在 Windows 11 和 Windows 10 的 VMWare 嵌套虚拟化虚拟机以及 Windows 11 物理机(bare metal)上进行了测试。
## 与宿主操作系统的隔离
尽管 revhv 是一个 type-2 hypervisor,但其 VMX-root 环境被刻意尽可能地与宿主操作系统分离开来。
对于 VMX-root 的执行,revhv 不会共享以下内容:
- 页表(所有级别)
- GDT
- IDT
- 栈
- PAT
- EFER
只有 hypervisor 映像及其分配的内存池会被映射到宿主 VMX-root 地址空间中。VMX-root 路径中不使用 NT 内核 API,这些 API 也不会被映射到那里。
其他选择:
- NMI、`#DF` 和 `#MC` 使用专用的 IST
- 宿主状态中的 `GSBASE` 被设置为零
- `FSBASE` 用作当前的 `vcpu` 指针
## 追踪模型
核心追踪机制在每个 vCPU 上使用两种 EPTP 配置:
- `normal execution`:在受监控范围之外运行代码时使用。
- `target execution`:在受监控范围之内运行代码时使用。
当为某个地址范围启用自动追踪时,revhv 会设置 EPT 权限,使得跨越该范围边界的执行转移引发 EPT 违规。VM-exit 处理程序随后会翻转当前的 EPTP:
- normal -> target:当执行进入受监控范围时
- target -> normal:当执行离开受监控范围时
在发生 target -> normal 转移时,revhv 会向每个核心的环形缓冲区写入一条二进制追踪记录。这将生成一份控制流追踪记录,记录受监控代码何时将执行权转移到另一个模块或区域。
这刻意缩小了范围,有别于逐条指令的追踪。其目的是捕获边界,从而解答诸如下列问题:
- 加壳或虚拟化的驱动程序实际调用了哪些内核 API?
- 在转移点存在哪些参数/客户机状态?
同样的边界概念也可以反过来用于揭示进入某个区域的钩子或入站控制流,尽管这并非当前实现的重点。
## 追踪数据路径
追踪日志分为两个独立的系统:
- 标准日志:通过 `LOG_INFO`、`LOG_ERROR` 及相关宏发出的格式化消息。它们会进入一个同步的全局环形缓冲区,并可选地发送至串口 COM1。
- 追踪日志:通过 `hv::trace::emit` 发出的高速二进制条目。按设计,它们是按 vCPU 分配的、无锁的,并且在核心之间互不同步。
这两种日志机制均可在任何 IRQL 以及 vmx-root 模式下使用。
用户态会为每个逻辑处理器启动一个轮询线程,并通过 hypercall 将每个核心的环形缓冲区数据排空转储到 `trace_core_N.bin` 中。当自动追踪启动时,控制器还会写入:
- `modules.bin`:捕获开始时已加载内核模块的快照
- `trace_cfg.bin`:导出的捕获配置和可选的格式化规则
离线解析随后会:
1. 加载 `modules.bin`
2. 加载 `trace_cfg.bin`(如果存在)
3. 打开所有 `trace_core_N.bin` 文件
4. 执行按时间戳排序的 K 路归并
5. 从磁盘上的模块映像中惰性解析符号
6. 写入格式化的合并日志
### 离线解析的限制
离线解析必须在捕获自动追踪日志的同一台机器上进行,原因很简单:符号解析依赖于能够从模块列表下载模块的 PDB,而这是通过使用 `modules.bin` 中存储的完整路径从磁盘加载模块来实现的。在另一台机器上,该路径可能根本不存在,或者模块版本不一致。
未来版本的 revhv 将解决这一限制。
## 可配置的捕获
追踪条目并未硬编码为单一布局。revhv 支持:
- 默认应用的通用转移配置
- 以客户机 RIP 为键的精确地址覆盖
- 用于离线解析器的可选自定义格式字符串
默认的通用配置会捕获:
- `rip`
- `retaddr`
可以为每个捕获点配置额外的字段,包括:
- `rsp`, `rax`, `rbx`, `rcx`, `rdx`, `rsi`, `rdi`, `rbp`
- `r8` 到 `r15`
- `retaddr`
捕获规则示例:
```
at config exact nt!NtOpenFile rip retaddr rcx rdx r8 r9
```
离线格式化规则示例:
```
at config fmt exact nt!ExFreePool "{retaddr} -> {rip}(pool = {rcx:x})"
```
这种区分很重要:出于性能原因,`revhv-km` 仅记录原始数据,而 `revhv-um` 决定后续如何渲染它。它无法渲染未被记录的数据。
## 不使用 SEH 的异常处理
`revhv-km` 在 VMX-root 模式下不依赖 SEH。
相反,它使用一个小型的显式异常捕获器:
- `R14` 存放预期会发生故障的指令地址。
- `R15` 存放恢复地址。
- 当发生选定的宿主异常时,陷阱处理程序会检查 `RIP == R14`
- 如果成立,异常详情将被保存到当前的 `vcpu` 中
- 执行在 `R15` 处恢复
这用于 hypervisor 刻意尝试易故障指令并需要进行异常处理的操作。
目前,`#UD` 和 `#GP` 通过这种方式处理。其他意外的宿主异常被视为致命错误。
## 不可恢复的宿主错误
当 hypervisor 遇到不可恢复的宿主错误时,其目标是以一种保留诊断状态并避免三重故障或重启的方式停止运行。
发起错误处理的核心会执行以下操作:
1. 自身去虚拟化
2. 切换回有效的宿主代码段上下文
3. 原子化地标记“正在进行崩溃处理”
4. 通过 x2APIC 或 xAPIC 向所有其他逻辑处理器发送 NMI
5. 等待所有其他核心确认
6. 调用 `KeBugCheckEx(MANUALLY_INITIATED_CRASH, 'rvhv', ...)`
响应的核心会执行以下操作:
1. 在专用的 IST 上接收 NMI(如果当时正在 vmx-root 中执行),或者执行客户机 NMI VMEXIT(如果当时正在 vmx non-root 中执行)
2. 检测到正在进行崩溃处理
3. 自身去虚拟化
4. 递增崩溃确认计数
5. 使用精心构造的 `IRETQ` 帧解除对 NMI 的阻塞
6. 在中断开启的状态下自旋,直到发起核心触发 bugcheck
此行为确保了一条确定性的失败路径,使得在调用 `KeBugCheckEx` 时,Windows 可以接管所有核心的控制权,并允许其捕获崩溃转储。该转储还包含 hypervisor 的所有常规日志,从而为崩溃原因提供有价值的信息(仅当在 Windows 设置中启用了完全内存转储时才有效)。
一个已知的限制是,在早期启动阶段发生不可恢复的错误时。具体来说,当所有核心尚未完全启动时,一个已虚拟化的核心引发了不可恢复的错误。当发起的核心发送 NMI 时,尚未启动的核心会通过 Windows 的 IDT 接收该 NMI。这通常会导致 `NMI_HARDWARE_FAILURE` bugcheck,因为 Windows 那时并未预期收到 NMI。
## 隐蔽性与计时行为
revhv 当前针对计时检查的隐蔽性实现虽然简单,但在大多数情况下已经足够。
当前措施包括:
- 针对 `IA32_MPERF`、`IA32_APERF` 和 `IA32_TIME_STAMP_COUNTER` 的 VM-exit MSR-store 和 VM-entry MSR-load 处理
- 针对 `IA32_PERF_GLOBAL_CTRL` 的 VMCS 处理
- TSC 偏移补偿,以抵消 VM-exit 和 VM-entry 带来的开销
- 基于 VMX 抢占定时器的重新同步,以限制核心间的 TSC 漂移
`IA32_TIME_STAMP_COUNTER` 会在 VM-exit 时保存,但不会在 VM-entry 时加载。相反,revhv 会测量相关的开销,并在执行 `VMRESUME` 之前调整 VMCS TSC 偏移量。
核心理念是:
```
desired_tsc = stored_tsc + native_instruction_overhead - vmentry_overhead - vmexit_to_store_overhead
tsc_offset -= (rdtsc() - desired_tsc)
```
基准测试路径会经过 VM-exit 存根中的一段快速汇编路径,以确保 C++ 处理程序不会对测量结果造成过多污染。
由于一旦每个核心被独立调整,不变的 TSC 也会失去同步,因此 revhv 会通过 VMX 抢占定时器周期性地进行重新同步。若非如此,Windows 的行为将变得极不稳定。
关于 `PMCs` 和 `MPERF/APERF` 的一个已知限制是,CPU 自身在 VMEXIT 上保存这些 MSR 之前,以及在 VMRESUME 上加载它们(落地到下一条客户机指令边界)之前,执行内部操作会带来开销。这已通过针对 `TSC` 的基准测试方法和提供的公式得到处理,理论上该方法同样适用于其他测量。
## 用户态控制器
`revhv-um` 提供了围绕 hypervisor 的大部分工作流:
- 检测所有核心上是否存在 hypervisor
- 解析符号和地址
- 即使在 hypervisor 不存在的情况下枚举已加载的内核模块
- 当 hypervisor 处于活动状态时,通过 hypercall 读取内核内存
- 将标准 hypervisor 日志排空转储到本地文件
- 控制自动追踪的启用和禁用
- 在追踪开始时拍摄模块状态和追踪配置的快照
- 将每个核心的追踪缓冲区排空转储为二进制文件
- 通过符号解析和自定义格式化对原始追踪进行离线解析
实际应用中的一个结果是,即使完全没有 VMX,某些命令也依然有用。当 hypervisor 不存在时,面向离线和符号的命令仍然可以正常工作。
## 命令
部分命令刻意采用了 *WinDbg 风格*,以增加熟悉感。
### 通用命令
- `help` 或 `?`
- 显示通用命令列表。
- `q`、`quit`、`exit`
- 退出控制器。
### 符号与模块工作流
- `ln `
- 将地址解析为最接近的符号,或将符号表达式解析为。
- 支持的形式包括 `module`、`module+offset`、`module!symbol+offset` 和 `module:section+offset`。
- 示例:
```
ln nt!MmCopyMemory+0x100
ln nt:PAGE+0x123
ln 0xfffff80312345678
```
- `lm [filter]`
- 列出已加载的内核模块。
- 即使没有 hypervisor 也能工作。
- 示例:
```
lm
lm nt
```
- `lm export `
- 导出当前模块列表供离线使用。
- 示例:
```
lm export modules.bin
```
### 内存检查
- `db`, `dw`, `dd`, `dq`, `dp`
- 通过 hypercall 转储客户机虚拟内存。
- `db`:字节
- `dw`:字
- `dd`:双字
- `dq`:四字
- `dp`:指针,并对指向的地址进行符号解析
- 形式:
```
db [count] [target_cr3]
dw [count] [target_cr3]
dd [count] [target_cr3]
dq [count] [target_cr3]
dp [count] [target_cr3]
```
示例:
```
db nt!MmCopyMemory 40
dd fffff80312340000 20
dp ntoskrnl!KeBugCheckEx+20 8
dq nt:PAGE+123
```
### 自动追踪
- `at enable [output_dir]`
- 为某个地址范围启用自动追踪。
- 启动按核心分配的追踪轮询线程。
- 将 `modules.bin` 和 `trace_cfg.bin` 保存到输出目录。
- 示例:
```
at enable nt!NtCreateFile 20
at enable fffff80312345678 100 C:\traces
```
- `at disable`
- 停止轮询,刷写剩余条目,并恢复正常 EPT 状态。
- 示例:
```
at disable
```
- `at config generic [f1] [f2] [f3] [f4] [f5]`
- 设置用于转移捕获的默认字段映射。
- 示例:
```
at config generic rip retaddr
```
- `at config exact [f1] [f2] [f3] [f4] [f5]`
- 为某一个确切的客户机 RIP 覆盖捕获映射。
- 示例:
```
at config exact nt!NtOpenFile rip retaddr rcx rdx r8 r9
```
- `at config fmt generic ""`
- 设置离线追踪解析的默认输出格式。
- 示例:
```
at config fmt generic "{rip} {retaddr}"
```
- `at config fmt exact ""`
- 设置按地址分配的格式化规则。
- 示例:
```
at config fmt exact nt!ExFreePool "{retaddr} -> {rip}(pool = {rcx:x})"
```
- `at config fmt clear generic`
- `at config fmt clear exact `
- 移除先前配置的格式化规则。
- 示例:
```
at config fmt clear exact nt!ExFreePool
```
- `at config export `
- 将当前配置写入磁盘供离线解析使用。
- 示例:
```
at config export trace_cfg.bin
```
### 离线追踪解析
- `trace parse [output_file]`
- 解析目录下的所有 `trace_core_N.bin` 文件。
- 按时间戳对它们进行归并。
- 从磁盘上的模块文件解析符号。
- 应用导出的格式化规则。
- 不需要 hypervisor。
- 示例:
```
trace parse modules.bin .\traces
trace parse modules.bin .\traces combined.log
```
### 杂项
- `apic`
- 通过 hypervisor 查询 APIC 信息。
- `df`
- 为测试目的刻意触发宿主双重故障路径。
- 预期结果是系统崩溃,而非死机。其目的是测试 IST 和不可恢复错误机制。
## 典型工作流
一个直观的工作流如下所示:
1. 启动 `revhv-um` 并验证 hypervisor 是否存在。
2. 使用 `ln` 和 `lm` 识别目标模块和地址范围。
3. 设置通用或精确的捕获配置。
4. 为目标范围启用自动追踪。
5. 对目标进行演练(执行相关操作)。
6. 禁用自动追踪。
7. 解析生成的追踪目录。
以下是对一个高度虚拟化的商业反作弊驱动程序进行示例运行时提取的部分日志(为了可读性移除了部分内容,格式配置为仅显示 RIP),展示了其卸载例程的一小部分:
```
...
[core 8] ntoskrnl!KeAcquireGuardedMutex
[core 8] ntoskrnl!KeReleaseGuardedMutex
[core 8] ntoskrnl!NtClose
[core 8] ntoskrnl!ObfDereferenceObject
[core 8] ntoskrnl!ExFreePoolWithTag
[core 8] ntoskrnl!PsSetCreateProcessNotifyRoutineEx
[core 8] ntoskrnl!PsRemoveCreateThreadNotifyRoutine
[core 8] ntoskrnl!PsRemoveLoadImageNotifyRoutine
[core 8] ntoskrnl!ObUnRegisterCallbacks
...
[core 8] ntoskrnl!SeUnregisterImageVerificationCallback
[core 8] ntoskrnl!CmUnRegisterCallback
...
[core 8] ntoskrnl!KeSetEvent
[core 10] ntoskrnl!KeResetEvent
[core 8] ntoskrnl!KeSetEvent
[core 8] ntoskrnl!KeWaitForSingleObject
[core 10] ntoskrnl!KeAcquireGuardedMutex
[core 10] ntoskrnl!KeReleaseGuardedMutex
[core 10] ntoskrnl!PsTerminateSystemThread
...
```
## 致谢
从头开始构建一个 Intel hypervisor 的想法受到了 [jonomango/hv](https://github.com/jonomango/hv) 的启发。
标签:C/C++, DAST, EPT(扩展页表), Hypervisor开发, Intel VT-x, KMDF, Type-2虚拟机管理程序, UML, VMX, Windows内核驱动开发, x86-64架构, 事务性I/O, 云资产清单, 代码混淆分析, 内核调试, 动态二进制分析, 动态脱壳, 底层安全, 恶意软件分析, 控制流追踪, 漏洞分析, 虚拟化安全, 虚拟化执行保护分析, 虚拟机逃逸, 路径探测, 逆向工程