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 是两条独立坐标轴
| 执行层 | 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 让调试器知道要尝试显示哪些范围。下面是相应界面示意图:

界面中从 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:
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 的公开调用约定。
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_pa0x00–0x07
按小端解码得到 0x109144000。调试监控端的 phys 命令切换到其物理地址视图;后续记录在该区域观察到:
| 状态 | 原始区域偏移 | 所见值 | 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 转换。
两次转换连接栈与调用点
记录中的第一条映射是:
CR3 0x0000000004c00002
page-table root 0x0000000004c00000
guest RSP 0xfffff80758344ec8
RSP physical view 0x0000000003e24ec8
stack value 0xfffff8075f460217
return physical view 0x0000000003e9a217在 RSP 对应位置读到的值不是另一个物理地址,而是待再次转换的返回虚拟地址:
return_va0x3E24EC8–0x3E24ECF
第二次转换得到 0x3e9a217。其前两字节位于 0x3e9a215,记录为 FF D0,解码恰好是 call rax:
返回点、调用长度与实际字节相互支持,比“栈上有一个像内核地址的值”更有说服力。但完整页表条目没有保留,因此这些物理映射仍是记录结果,而非本机重新完成的页表遍历。
四级、48 位规范地址的拆分可以独立核算:
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 的一个尾部返回路径改变布尔结果。这里把两条指令的编码作为对照,而不是给出另一环境可直接使用的地址:
| 状态 | 指令 | 编码 | 长度 |
|---|---|---|---|
| 记录中的原指令 | mov al, bl |
8A C3 |
2 |
| 记录中的调整 | mov al, 1 |
B0 01 |
2 |
本机通过 objdump 独立核对了这两种编码及 FF D0,没有实际写入该调整。保持相同长度只证明局部指令尺寸兼容,不证明其他返回路径、调用者预期、完整性机制和并发执行状态都满足条件;该布尔判定也不等于关闭所有 IUM 隔离。
附加结果证明了什么
目标进程与模块的相关层级为:
- vmcompute.exe
- vmwp.exe
- vmsp.exe
- TpmEngUM.dll
- iumbase.dll
- iumdll.dll
- vmsp.exe
- vmwp.exe
WinDbg 1.2210.3001.0 的附加记录包含 TpmEngUM.dll 加载与初始断点。以下只保留两项决定性输出:
ModLoad: 00007ffc`423a0000 00007ffc`43292000 C:\Windows\System32\TpmEngUM.dll
ntdll!DbgBreakPoint:
00007ffc`50270bb0 cc int 3这支持该环境中的调试可达性,不是 TPM 漏洞的证明,也不说明读取到的每个地址都处于 VTL1。后续仍须以断点命中、寄存器、内存视图及代码路径确认实际观察对象。
复查顺序应保持收敛:确认映像身份,选择有含义的事件,沿数据流取出正确来宾上下文,再用返回地址和代码字节闭合证据链。任何版本、偏移或地址视图不一致,都从最早出现分歧的位置重新核验,而不是继续向后推导内核修改。