
Tencent VM の .tvm0 セクションを逆仮想化します。VM ハンドラをゲストレベルのリスティングと LLVM IR にリフトし、CFG とディスパッチを復元し、復元したコードを PE にパッチして戻します。
関連する Zenodo レコード: 10.5281/zenodo.23474767。
⚠️ 免責事項: 本プロジェクトは、許可されたセキュリティ研究、相互運用性分析、および教育のみを目的としています。商用アンチチートシステムのバイパス機能を含むものではなく、提供するものでもありません。対象バイナリを分析する法的権利を有していることを確認する責任は、ユーザー自身にあります。
Tencent VM 用の devirtualizer です。.tvm0 セクション内の仮想化されたコードを復元し、各 VM ハンドラがゲストのレジスタとメモリに対して行う処理をリフトし、復元された関数を通常の read+execute セクションとしてバイナリに書き戻すことができます。
これはツールです。モジュールを指定すれば、構造とコードを取り出せます。
.tvm0 section -> VM semantics -> guest-level listing -> LLVM IR -> patched PE
VMルートの逆仮想化されたリスティング。ゲストレジスタファイルがブロックをまたいでバインドされています。

python -m venv .venv
source .venv/Scripts/activate # Windows: .venv\Scripts\Activate.ps1
pip install -r requirements.txt
Python 3.13、capstone。バックエンドには llvmlite==0.50.0 と、Windows では MSVC の
link.exe/cl.exe が必要です。z3-solver は任意です — これがない場合、パス条件付けは維持され、
有界ディスパッチ証明のみがスキップされます。
.env.example を .env にコピーし、このマシンのパスを記入してください。.env は git で無視され、
すべての設定にはコマンドラインフラグもあるため、このファイルはデフォルト値を提供するだけです。
| キー | 意味 |
|---|---|
TVM_MODULE | 解析対象のモジュール。位置引数なしで実行されるプローブ用 |
TVM_LISTINGS | バックエンドが読み込むリスティングコーパス |
TVM_OUT | すべての出力のルート(デフォルト out/) |
TVM_STEPS | ブロックが切り詰められるまでのブロックあたりのステップ数(デフォルト 900000) |
TVM_BLOCKS | ルートあたりのブロック予算(デフォルト 512) |
TVM_PAYLOAD_OPT | ペイロードコード生成レベル 0–3 |
TVM_PAYLOAD_MEM | sealed | absolute ゲストメモリモード |
TVM_MSVC_BIN | link.exe/cl.exe を格納するディレクトリ。PATH に依存する代わりに使用 |
優先順位: コマンドラインフラグ → .env → 環境変数 → 組み込みデフォルト。
エンジンコマンドは engine/ から、バックエンドコマンドはリポジトリルートから実行します。
cd engine
python -m probe.scan_tvm0 /path/to/dir # which files carry a .tvm0, and its roots
python -m probe.trampoline_forms input.exe # what the entry trampolines look like
python -m probe.mnemonic_census input.exe # what x86 the VM contains
python -m core.devirt input.exe --out ../out/listings
関数ごとに1つの 0x<root>.txt リストと1つの 0x<root>.guest-state.json を出力します。オプション:
| フラグ | デフォルト | 意味 |
|---|---|---|
--out=DIR | stdout | リストとゲスト状態を書き出す |
--root=0x… | 検出されたすべて | 1つのVMエントリに限定する |
--show[=N] | オフ | 最初のN行を表示する |
--steps=N | 900000 | 切り捨て前のブロックあたりのステップ数 |
--blocks=N | 512 | ルートあたりのブロック予算 |
--caller-entry=0x… --call-site=0x… | オフ | エントリを呼び出し元に特化する |
各リストのヘッダーには、実行が切り捨てられたかどうかが示されます。切り捨てられたリストは下限であり、関数そのものではありません。
python -m probe.cfg_run input.exe 0x14015fc2e # walk it, print the CFG
python -m probe.guest_effect input.exe 0x14015fc2e # what handlers do to guest registers
python -m probe.ctx_map input.exe 0x14015fc2e # which context slot holds which register
python -m probe.why_unresolved input.exe 0x14015fc2e # why a dispatch did not resolve
python -m probe.block_exits input.exe 0x14015fc2e # why a block has no successor
python -m probe.stop_debug input.exe 0x14015fc2e # why evaluation stopped
python -m probe.unresolved_detail input.exe 0x14015fc2e # per-block detail
特定の値またはストアを掘り下げます:
python -m probe.trace_window input.exe 0x14015fc2e 0x14015fc40 0x14015fc90
python -m probe.reg_history input.exe 0x14015fc2e rax
python -m probe.who_wrote input.exe 0x14015fc2e 0x18
python -m probe.flags_at input.exe 0x14015fc2e 0x14015fc60
python -m probe.stack_at input.exe 0x14015fc2e
.\write-devirt.ps1 -Module 'C:\path\to\SGuardSvc64.exe'
ペイロードを実行 → 注入 → ロード → 差分検出の順に実行し、最初に失敗したステージで停止します。オプション:
-Listings、-Out、-Opt 0..3、-Mem sealed|absolute、-SkipDifferential、-Force。
各ステージの個別実行:
cd llvm
python payload.py --listings=../out/hard-cases-listings-final --out=../out/write-devirt --opt=0 --mem=sealed
python peinject.py --host=input.exe --payload=../out/write-devirt --out=../out/write-devirt/input.neko.exe
python miniload.py ../out/write-devirt/input.neko.exe
python diff_payload.py --payload=../out/write-devirt --patched=../out/write-devirt/input.neko.exe
diff_payload.py が重要なチェックです。これはパッチ適用済みイメージの内部でコードを実行し、同じリスティングに対してインタプリタとバイト単位で比較します。miniload.py はイメージがロードされ、そのエントリが実行されることだけを証明します。
ACE はネイティブ関数を VM ディスパッチャへのトランポリンに置き換えます。ディスパッチャはネイティブの x86 ハンドラを実行しますが、保護された関数のアーキテクチャ状態は VM コンテキスト内に存在します。
flowchart TB
subgraph TOP[ ]
direction LR
subgraph TEXT[.text]
ENTRY["<b>Function entry</b><br/>jump to VM entry"]
end
subgraph VM[.tvm0 section]
direction LR
CORE["<b>Shared dispatcher and handlers</b><br/>execute x86 - resolve next handler"]
CTX["<b>Guest context</b><br/>GPRs - RFLAGS - VIP"]
CORE <--> CTX
end
end
subgraph NATIVE[Native execution outside the VM]
direction LR
BOXED["<b>Boxed instruction</b><br/>execute original x86"]
CALL["<b>Guest call</b><br/>invoke native target"]
EXIT["<b>Guest return or tail call</b><br/>resume native execution"]
BOXED ~~~ CALL ~~~ EXIT
end
TEXT --> VM
VM <--> NATIVE
style TOP fill:transparent,stroke:transparent
一部の操作はVMの外で実行されます。ボックス化された命令は、VMが必要なレジスタを復元した後に元のx86エンコーディングを実行し、その効果をゲストコンテキストに返します。ゲストコールはネイティブターゲットを呼び出し、戻り値とWindows x64 ABIのクロバーをゲスト状態に反映して再開します。ゲストリターンとテールコールはディスパッチャを完全に離脱します。
コンテキストは、ゲストのRSPを含む16個すべてのゲスト汎用レジスタと、パックされたゲストフラグを保持します。そのレイアウトはプロテクタのビルドによって異なるため、想定するのではなくVMの保存/復元動作を観察することで特定されます — probe.ctx_mapはストア時に測定されたマッピングを報告し、出力される各リスティングはコンテキストアクセスをそのスロットオフセットで囲みます。
条件付き制御フローはディスパッチ演算です。ハンドラはゲストフラグまたは述語を抽出し、それを使って次の仮想プログラムカウンタを選択し、次のハンドラのアドレスを計算します。デバーチャライザはそれをシンボリックに評価し、選択が未解決のゲスト値に依存する場合にフォークします。ブロックの識別子は(handler, vip)のペアであり、ハンドラ単独ではありません。
リカバリは一つのルールに基づきます。値は、分析が与えた名前ではなく、マシンが行ったことによって識別する。 ブロック間のレジスタ伝播は、証拠としてではなく注釈としてリスティングに記録されます — リスティング内のすべてのレジスタバージョン番号を置換しても、解析された構造は変化してはなりません。
write-devirt.ps1 one command: payload -> patched PE -> differential
.env.example configuration template; copy to .env
envfile.py .env parser; bootenv.py loads it for llvm/, engine/envboot.py for probes
engine/core/ PE parsing, expression arena, x86-64 lifting, machine model,
guest context, CFG recovery, guest IR, guest-state programs
engine/probe/ 28 probes - each answers one question about the target
engine/tests/ 30 suites, all on synthetic fixtures or independent Python arithmetic
llvm/ listing grammar, IR -> LLVM IR, interpreter, differential,
payload builder, PE/PDB editor, injector, loader
llvm/runtime/ the code the payload calls: sealed memory, entry stub, __chkstk
out/ tool output (git-ignored)
ゲストコンテキストのレイアウトは仮定ではなく計測によって導出される。probe.ctx_map は、どのコンテキストスロットがどのゲストレジスタを保持しているかを、ストアの時点で計測して報告する。ブロックの同一性は (handler, vip) の組であり、ハンドラ単体ではない。
出力が主張しないこと: リスティングは VM が行ったことの記録であり、逆コンパイルではない。未解決のディスパッチは推測せず、未解決として報告される。切り詰められた実行はファイルヘッダーに明示される。--caller-entry はその呼び出し元を条件とした結果を生成し、ペイロードはそれを一般的なリカバリへ昇格させることを拒否する。