跳转到内容

虚拟机与代码虚拟化

VMP 风格保护把原生逻辑翻译成自定义字节码,再由 dispatcher 和 handlers 在运行时解释执行。Tenet 的 vm_abstractIDA/Ghidra 提供的静态结构与 Trace 中的真实执行连接起来,恢复 VM 步骤、opcode、上下文、虚拟寄存器和 handler 热度。

问题 Tenet 提供的证据
每条 VM 指令对应哪段原生执行? VM step 的 inst_id_start / inst_id_end
哪些 handlers 最常执行? handler 频率、body PC 和 opcode 集合
当前 VM-PC、context 与寄存器是什么? 每步的 VM-PC、context 字段、寄存器/栈快照
opcode 是否经过编码? 位域提取、固定变换、rolling key 去混淆
某个虚拟寄存器从哪里来? vm_reg_timelinevm_backward_slice

它不会自动恢复完整 VM ISA、枚举未执行 handler、生成原生代码或重新编译字节码。输出是后续 handler 语义分析和去虚拟化的动态证据层。

寻找每条 VM 指令恰好执行一次的位置:

  • while/switch dispatcher 的循环头;
  • direct-threaded VM 中各 handler 的 fetch/dispatch PC 集合;
  • 每步都写一次的 VM-PC 或 instruction-counter 内存字段。

只知道 dispatcher 时,可以先用行内 hint:

Terminal window
./tenet trace.bin --vm-abstract \
--vm-dispatcher 0x100005000 \
--vm-indirect-br 0x100005014

已知多个 handler 时可重复使用 --vm-handler。复杂 VM 推荐使用 JSON:

Terminal window
./tenet trace.bin --vm-abstract --vm-hints vm-hints.json

如有独立的字节码 decoder 定义,可追加 --vm-decoder decoder.json

当前 Tauri 前端的 VM Workspace 是一个 handler 摘要面板,也可弹出到独立窗口。它不支持拖放/选择 hints JSON;JSON hints 请通过 CLI --vm-hints <file.json> 提供。面板内可填写 dispatcher_pcindirect_branch_pcmem_anchor_addr,选择 dispatch type 和 opcode size,然后请求后端运行 vm_abstract

运行后表格展示每个已观察 handler 的 opcode、原生 handler PC、语义标签和命中次数。当前 Web 实现没有 VM Steps、Bytecode、Registers、Stack、Memory/Ctx 多标签视图,也没有 Linked 模式联动主指令时间线;需要步骤、虚拟寄存器时间线或反向切片时使用 MCP VM 工具。

下面是寄存器机的基础示例:

{
"dispatcher_pc": "0x100005000",
"indirect_branch_pc": "0x100005014",
"handler_entry_pcs": [
"0x100006000",
"0x100007000",
"0x100008000"
],
"handler_names": {
"0x100006000": "ADD",
"0x100007000": "XOR",
"0x100008000": "LOAD"
},
"dispatch_type": "WhileSwitch",
"ctx_base_reg": "x21",
"ctx_fields": [
{ "name": "ip", "offset": 0, "size": 8 },
{ "name": "sp", "offset": 8, "size": 8, "is_pointer": true }
],
"rf_base_reg": "x20",
"vm_registers": [
{ "name": "pc", "offset": 0, "size": 8 },
{ "name": "R0", "offset": 32, "size": 8 },
{ "name": "R1", "offset": 40, "size": 8 }
],
"opcode_size": 4,
"opcode_mask": "0xFF",
"opcode_shift": 0
}

vm_registers 中名为 pcip 的字段会被用作 VM-PC。rf_base_reg 未设置时,寄存器文件读取会使用 ctx_base_reg

模式 JSON 字段 适用结构
单 PC dispatcher_pc 中央 while/switch、间接分支或 trampoline dispatcher
PC 集合 anchor_pcs handler 内联 fetch/dispatch 的 direct-threaded VM
内存写 mem_anchor_addr + mem_anchor_size 控制流被 CFF 打散,但 VM-PC/context 字段仍每步写一次

PC anchor 必须每条 VM 指令恰好命中一次;命中缺失或重复都会造成错误切分,Pass 无法自动证明 anchor 是否满足这个约束。

Direct-threaded VM 可以给出多个稳定位置:

{
"anchor_pcs": [
"0x100006034",
"0x100007034",
"0x100008034"
],
"dispatch_type": "ThreadedCode",
"handler_entry_pcs": [
"0x100006000",
"0x100007000",
"0x100008000"
]
}

重度 CFF 场景可改用内存写:

{
"dispatch_type": "FetchOnly",
"mem_anchor_addr": "0x13B3444F0",
"mem_anchor_size": 4,
"handler_entry_pcs": ["0x100006000", "0x100007000"],
"opcode_from_reg": "x0"
}

opcode_from_reg 表示 opcode 已在 host 寄存器中解码,Tenet 不再从 VM-PC 指向的内存读取。

  • dispatcher_pcindirect_branch_pchandler_entry_pcsanchor_pcs 使用 IDA/Ghidra 显示地址;Tenet 根据 Trace 的 module_slide 重定位。
  • AArch64 Mach-O 默认按 IDA image base 0x100000000 解释。
  • mem_anchor_addr 是例外:它必须是 Trace 中观察到的运行时内存地址,不会重基。

不要混用静态 PC、运行时 PC 和文件偏移。如果 hinted PC 没有执行记录,先确认 hint 来自同一版本二进制且落在录制范围内。

默认模式从 VM-PC 指向的内存读取 opcode_size 字节,然后应用 opcode_byte_offsetopcode_maskopcode_shift。还可声明去混淆与操作数字段:

{
"opcode_size": 2,
"opcode_byte_offset": 1,
"opcode_mask": "0xFF00",
"opcode_shift": 8,
"opcode_deobf_steps": [
{ "op": "xor", "imm": "0xE0" },
{ "op": "sub", "imm": "0x0F" },
{ "op": "xor_rolling", "imm": "0xDEAD" }
],
"rolling_key_update": "xor_opcode",
"instruction_size": 4,
"operand_fields": [
{ "name": "dst", "byte_offset": 2, "size": 1, "mask": "0x1F", "shift": 0 },
{ "name": "src", "byte_offset": 3, "size": 1 }
]
}

支持固定算术/位运算、rotate、byte-swap、取反以及 rolling-key 变换。若字节码来自未被 Trace 观察到的只读映射,opcode 会保持 unknown;此时应改用 opcode_from_reg,或确保录制覆盖字节码的生成/写入。

虚拟状态也可以直接映射到 host 寄存器:

{
"vm_registers": [
{ "name": "pc", "from_host_reg": "x19" },
{ "name": "sp", "from_host_reg": "x20" },
{ "name": "R0", "from_host_reg": "x21" }
]
}

这适用于把 VM 状态常驻在 AArch64 callee-saved 寄存器中的轻量解释器。

先用 vm_abstract 加载相同 hints,再使用:

  • vm_summary:dispatcher、架构和 handler 热度概览;
  • vm_steps_query:按 label、opcode、VM-PC 或寄存器变化过滤步骤;
  • vm_reg_timeline:追踪一个虚拟寄存器的值变化;
  • vm_backward_slice:从指定 step 的虚拟寄存器反查 producer。

MCP 返回的 PC 使用当前 image_base 重基后的显示地址;默认与 AArch64 Mach-O 的 IDA 地址一致。

主要结果包括 dispatcher 类型、迭代数、架构分类、handler 执行次数、VM steps、VM-PC、context、虚拟寄存器、操作数和推断的字节码范围。

  • 结果只覆盖当前 Trace 实际执行的路径;未执行 handler 不会出现。
  • handler 标签来自 hints,不是 Tenet 自动证明的语义。
  • Sampling、缺失 GPR diff 或内存访问会降低状态恢复精度。
  • vm_abstract 提供结构和动态状态,不等于完整去虚拟化。

requires at least one static-analysis hint

Section titled “requires at least one static-analysis hint”

传入 --vm-hints--vm-dispatcher 或至少一个 --vm-handler。不存在无 hint 自动检测模式。

确认地址来自当前版本二进制、使用 IDA/Ghidra 显示地址、位于 qbditrace 追踪范围内,并且当前输入确实执行了该 VM 路径。

至少需要三个 anchor 命中。步骤过少通常表示漏命中,过多通常表示每步命中多次。对 direct-threaded 或 CFF 结构改用 anchor_pcsmem_anchor_addr

字节码内存可能没有被 Trace 观察到。定位承载已解码 opcode 的 host 寄存器并使用 opcode_from_reg,或扩大录制范围覆盖字节码写入。