~/posts/binary/hyperv-ium-guest-context-debugging.md

Hyper-V IUM:从来宾状态定位调试上下文

沿 VTL 返回事件回溯 VP、VTL 与 VMCS,区分内部偏移和架构字段编码,再通过两次地址转换、栈返回值与调用指令核对 Secure Kernel 的上下文和调试边界。

date[31:24]
read[23:16]
8 分钟
cat[15:8]
二进制
目录
  1. 0x00VTL 与 ring 是两条独立坐标轴
  2. 0x01先选事件,再解释寄存器
  3. 0x02从 VMPTRLD 回溯指针槽
  4. 0x03原始 VMCS 偏移不是字段编码
  5. 0x04两次转换连接栈与调用点
  6. 0x05PE 身份与调试判定各自验证
  7. 0x06附加结果证明了什么
  8. 0x07官方参考

Hyper-V 虚拟 TPM 的后端 TpmEngUM.dll 出现在 vmsp.exe 中,但这个进程处于隔离用户模式(Isolated User Mode,IUM),不是给普通调试器增加管理员权限就能附加的目标。问题首先在于虚拟信任级别(Virtual Trust Level,VTL),其次才是进程与调试接口。

下面分析一条嵌套虚拟化环境中的调试记录:从外层 VMware 的观察点进入 Hyper-V,以 VTL 返回事件定位来宾状态,再用栈返回地址与指令字节确认 Secure Kernel 的代码位置。Windows 和 VMware 的精确版本未完整保留,所有内部偏移限定在该样本;本机验证仅涉及地址算术和指令解码,没有运行嵌套虚拟机或修改内核。

VTL 与 ring 是两条独立坐标轴

同一 VTL 内仍然区分用户态与内核态3 行
执行层 VTL0 VTL1
ring 3 普通应用与管理进程 IUM trustlet
ring 0 常规 Windows 内核 Secure Kernel
隔离基础 Hyper-V 与二级地址转换 Hyper-V 与二级地址转换

VTL0 的 ring 0 不因其内核权限就获得 VTL1 用户页的访问权。Microsoft 将 IUM 与 Secure Kernel 放在 VTL1,并明确列出普通附加、线程注入及内存操作的限制;这与普通令牌访问被拒绝不是同一层问题。Microsoft IUM 文档

flowchart TD
  accTitle: 外层调试器与嵌套虚拟 TPM 的位置
  accDescr: L0 的调试器连接 VMware,观察 L1 Hyper-V;L2 Windows 使用由 L1 VTL1 vmsp 进程提供的虚拟 TPM。
  subgraph L0["L0:外层宿主"]
    D["IDA / GDB 客户端"] --> W["VMware Workstation / GDB stub"]
  end
  subgraph L1["L1:启用 Hyper-V 的 Windows"]
    H["Hyper-V"]
    K["VTL1:Secure Kernel"]
    P["VTL1 ring 3:vmsp.exe / TpmEngUM.dll"]
    H --> K
    K --> P
    subgraph L2["L2:Windows 来宾"]
      T["虚拟 TPM 使用方"]
    end
    T --> P
  end
  W -. "外层调试视图" .-> H

这里的前提是掌握外层虚拟机监控器的调试能力。它不等价于从一台实体机器上的普通 VTL0 进程直接穿过隔离边界。L2 的 TPM 使用方与 L1 的 TPM 后端,也应在对象清单中分别标注。

先选事件,再解释寄存器

HvCallVtlReturn 的调用号是 0x0012,语义是切换到该虚拟处理器启用的下一较低 VTL。Microsoft hypercall 定义

样本定位到的 Hyper-V 内部处理函数显示于 0xfffff8000022c760,识别记录中的表有 0xee 项。这些属于映像分析结果,不是固定地址,也不是公开 hypercall ABI 对内部表长度的承诺。该事件提供了有意义的采样时机,但仍须分清当前 hypervisor 寄存器与保存的 VTL1 来宾寄存器。

GDB 后端缺少内存布局时,IDA 的手工 memory region 让调试器知道要尝试显示哪些范围。下面是相应界面示意图:

调试器缺少内存布局提示与 64 位手工内存区域设置的界面示意图

界面中从 0 到 0xFFFFFFFFFFFFFFFE 的大范围设置,仅描述显示端的读取尝试范围。Readable / Writable / Executable 选项不证明真实页权限,更不会改变 VTL 或 EPT 权限;精确范围优于把全部地址视为可访问。该功能的当前文档用于解释界面语义,不用于推定历史 IDA 版本。

运行时 rebase 之前还应确认映像身份。从 IDT 入口或当前指令反向搜索得到的候选 PE,可能属于普通内核、Secure Kernel 或 Hyper-V。核验映像大小、节区、机器类型与对应文件后,才把 RVA 加到正确模块的运行时基址上。

从 VMPTRLD 回溯指针槽

当前来宾 RIP 可能在独立 trampoline,而非 Secure Kernel 映像中。记录同时展示了 ShvlpVtlReturn 与 HvcallCodeVa 两条间接调用路径;共同线索是前一步执行过 call rax,所以来宾栈可能保留返回映像内部的地址。

寻找来宾栈之前,先沿 VMPTRLD 的输入回溯。以下是样本中相关指令的节选,省略中间无关指令;rdx 在这段路径中指向 VP:

VMCS pointer dataflow · selected instructionsasm
movzx   eax, r8b
mov     rbx, [rdx+rax*8+2A8h]
mov     rcx, [rbx+0E90h]
vmptrld qword ptr [rcx+188h]

因此 VTL 索引为 1 时,指针槽位置是 0x2a8 + 8 = 0x2b0。入口处将 RCX 解释为 VP 是该内部处理路径的观测,不应套用到所有 hypercall 的公开调用约定。

Sample-specific pointer chaintext
vp       = observed VP pointer
vtl1     = read_u64_virtual(vp + 0x2b0)
state    = read_u64_virtual(vtl1 + 0xe90)
vmcs_pa  = read_u64_virtual(state + 0x188)

三步分别解引用,而不是把三个偏移直接相加。另一个结构示意采用 0x180 / 0x188 作为 VP 的 VTL 槽,它展示的是另一份布局;此处以当前指令里的 0x2a8 / 0x2b0 为准。

同一反汇编还存在与 enlightenment 特性有关的分支,VMPTRLD 是其中一条路径的锚点。不应因附近出现 VMCS 字样,就把另一分支也强行解释成同一种内存布局。

原始 VMCS 偏移不是字段编码

State 中保存的八个字节为:

VMCS physical address
000000000040140901000000
  1. vmcs_pa0x00–0x07

按小端解码得到 0x109144000。调试监控端的 phys 命令切换到其物理地址视图;后续记录在该区域观察到:

本样本的存储位置与架构字段分开记录3 行
状态 原始区域偏移 所见值 VMREAD 字段编码
guest CR3 0x8b0 0x04c00002 0x6802
guest RSP 0x918 0xfffff80758344ec8 0x681c
guest RIP 0x920 0xfffff80758310035 0x681e

字段编码用于 VMREAD / VMWRITE,不是“VMCS 基址加这个数字”的偏移。Microsoft 的 enlightened VMCS 文档提供架构编码对照,但那份公开结构也不等于这里观测到的原始区域。

0x8b0 / 0x918 / 0x920 不是可跨 Windows、CPU 和 VMware 版本复用的结构 ABI。切换为物理视图也不自动解决嵌套地址转换:每个地址都要注明属于哪一层 guest physical,是否仍需 EPT/NPT 转换。

两次转换连接栈与调用点

记录中的第一条映射是:

Recorded address chaintext
CR3                  0x0000000004c00002
page-table root      0x0000000004c00000
guest RSP            0xfffff80758344ec8
RSP physical view    0x0000000003e24ec8
stack value          0xfffff8075f460217
return physical view 0x0000000003e9a217

在 RSP 对应位置读到的值不是另一个物理地址,而是待再次转换的返回虚拟地址:

Recorded stack value
03E24EC81702465F07F8FFFF
  1. return_va0x3E24EC8–0x3E24ECF

第二次转换得到 0x3e9a217。其前两字节位于 0x3e9a215,记录为 FF D0,解码恰好是 call rax:

Recorded call site · physical view
0x3e9a215ff d0callrax

返回点、调用长度与实际字节相互支持,比“栈上有一个像内核地址的值”更有说服力。但完整页表条目没有保留,因此这些物理映射仍是记录结果,而非本机重新完成的页表遍历。

四级、48 位规范地址的拆分可以独立核算:

ium-address-model.pypython
def indices4(va):
    return tuple((va >> s) & 0x1ff for s in (39, 30, 21, 12))

rsp = 0xfffff80758344ec8
ret = 0xfffff8075f460217
assert indices4(rsp) == (496, 29, 193, 324)
assert indices4(ret) == (496, 29, 250, 96)
assert (rsp & 0xfff) == (0x3e24ec8 & 0xfff) == 0xec8
assert (ret & 0xfff) == (0x3e9a217 & 0xfff) == 0x217
assert (0x04c00002 & 0x000ffffffffff000) == 0x04c00000
assert int.from_bytes(bytes.fromhex("17 02 46 5f 07 f8 ff ff"),
                      "little") == ret

扩展测试验证了 4096 个规范地址的索引往返,并拒绝 4 个越界或非规范输入;同时核对了两个小端值、页内偏移和三个 VMCS 位置。真实遍历还应依据 CR4.LA57、物理地址宽度、CR4.PCIDE 等状态解释 CR3,检查 present 位,并处理 1 GiB / 2 MiB 大页叶子;上述模型没有声称完成这些运行时检查。

PE 身份与调试判定各自验证

从返回指针定位映像时,反向扫描物理页依赖布局偶然性。更稳定的分析顺序是沿映像虚拟页逐页回查、翻译,再检查 MZ、e_lfanew、PE\0\0、机器类型、映像大小和节边界。虚拟连续不等于物理连续,单独命中 MZ 也不证明是 Secure Kernel。Microsoft PE 格式

样本随后针对 SkpsIsProcessDebuggingEnabled 的一个尾部返回路径改变布尔结果。这里把两条指令的编码作为对照,而不是给出另一环境可直接使用的地址:

返回值指令的局部差异2 行
状态 指令 编码 长度
记录中的原指令 mov al, bl 8A C3 2
记录中的调整 mov al, 1 B0 01 2

本机通过 objdump 独立核对了这两种编码及 FF D0,没有实际写入该调整。保持相同长度只证明局部指令尺寸兼容,不证明其他返回路径、调用者预期、完整性机制和并发执行状态都满足条件;该布尔判定也不等于关闭所有 IUM 隔离。

附加结果证明了什么

目标进程与模块的相关层级为:

Recorded process and modules
  • vmcompute.exe
    • vmwp.exe
      • vmsp.exe
        • TpmEngUM.dll
        • iumbase.dll
        • iumdll.dll
3 个进程,3 个模块

WinDbg 1.2210.3001.0 的附加记录包含 TpmEngUM.dll 加载与初始断点。以下只保留两项决定性输出:

Recorded WinDbg attachlog
ModLoad: 00007ffc`423a0000 00007ffc`43292000 C:\Windows\System32\TpmEngUM.dll
ntdll!DbgBreakPoint:
00007ffc`50270bb0 cc int 3

这支持该环境中的调试可达性,不是 TPM 漏洞的证明,也不说明读取到的每个地址都处于 VTL1。后续仍须以断点命中、寄存器、内存视图及代码路径确认实际观察对象。

复查顺序应保持收敛:确认映像身份,选择有含义的事件,沿数据流取出正确来宾上下文,再用返回地址和代码字节闭合证据链。任何版本、偏移或地址视图不一致,都从最早出现分歧的位置重新核验,而不是继续向后推导内核修改。

官方参考

NORMAL~/posts/binary/hyperv-ium-guest-context-debugging.md§--
0%zh-CN