
Devirtualisiert Tencent-VM-.tvm0-Abschnitte: hebt VM-Handler auf Gast-Ebene in Listings und LLVM IR an, rekonstruiert CFG und Dispatch und patcht den wiederhergestellten Code zurück in die PE.
Zugehöriger Zenodo-Datensatz: 10.5281/zenodo.23474767.
⚠️ Haftungsausschluss: Dieses Projekt ist ausschließlich für autorisierte Sicherheitsforschung, Interoperabilitätsanalyse und Schulungszwecke gedacht. Es enthält oder bietet keinerlei Umgehungsfunktionalität für kommerzielle Anti-Cheat-Systeme. Die Nutzer sind allein dafür verantwortlich, sicherzustellen, dass sie das rechtliche Recht besitzen, die Zielbinärdateien zu analysieren.
Ein Devirtualizer für Tencent VM. Stellt den virtualisierten Code in einem .tvm0-Abschnitt wieder her, hebt hervor, was jeder VM-Handler mit den Registern und dem Speicher des Gasts macht, und kann die wiederhergestellten Funktionen als normalen Lese- und Ausführungsabschnitt in die Binärdatei zurückschreiben.
Es ist ein Werkzeug: Richte es auf ein Modul, erhalte Struktur und Code zurück.
.tvm0 section -> VM semantics -> guest-level listing -> LLVM IR -> patched PE
Devirtualisierte Auflistung eines VM-Roots, mit dem Gast-Registerfile über Blöcke hinweg gebunden.

python -m venv .venv
source .venv/Scripts/activate # Windows: .venv\Scripts\Activate.ps1
pip install -r requirements.txt
Python 3.13, capstone. Für das Backend: llvmlite==0.50.0 und unter Windows MSVC
link.exe/cl.exe. z3-solver ist optional — ohne es bleibt die Pfadkonditionierung erhalten und nur der
begrenzte Dispatch-Beweis wird übersprungen.
Kopieren Sie .env.example nach .env und tragen Sie die Pfade für diese Maschine ein. .env wird von Git ignoriert, und
jede Einstellung hat auch ein Kommandozeilen-Flag, sodass die Datei nur Standardwerte liefert.
| Schlüssel | Bedeutung |
|---|---|
TVM_MODULE | das zu analysierende Modul für Probes, die ohne Positionsargument ausgeführt werden |
TVM_LISTINGS | Listing-Korpus, den das Backend liest |
TVM_OUT | Wurzelverzeichnis für alle Ausgaben (Standard out/) |
TVM_STEPS | Schritte pro Block, bevor ein Block abgeschnitten wird (Standard 900000) |
TVM_BLOCKS | Blockbudget pro Wurzel (Standard 512) |
TVM_PAYLOAD_OPT | Payload-Codegen-Stufe 0–3 |
TVM_PAYLOAD_MEM | sealed | absolute Gast-Speichermodus |
TVM_MSVC_BIN | Verzeichnis mit link.exe/cl.exe, statt sich auf PATH zu verlassen |
Priorität: Kommandozeilen-Flag → .env → Umgebung → integrierter Standardwert.
Engine-Befehle werden aus engine/ ausgeführt; Backend-Befehle aus dem Repository-Stammverzeichnis.
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
Eine 0x<root>.txt-Auflistung und eine 0x<root>.guest-state.json pro Funktion. Optionen:
| Flag | Standard | Bedeutung |
|---|---|---|
--out=DIR | stdout | Auflistungen + Guest-State schreiben |
--root=0x… | alle entdeckten | auf einen VM-Eintrag beschränken |
--show[=N] | aus | die ersten N Zeilen ausgeben |
--steps=N | 900000 | Schritte pro Block vor der Trunkierung |
--blocks=N | 512 | Block-Budget pro Root |
--caller-entry=0x… --call-site=0x… | aus | den Eintrag caller-spezialisieren |
Der Header jeder Auflistung gibt an, ob der Lauf trunkiert wurde; eine trunkierte Auflistung ist eine untere Schranke, nicht die Funktion.
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
In einen bestimmten Wert oder Speicher eintauchen:
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'
Führt Payload → Inject → Load → Differential aus und stoppt bei der ersten fehlschlagenden Stufe. Optionen:
-Listings, -Out, -Opt 0..3, -Mem sealed|absolute, -SkipDifferential, -Force.
Die einzelnen Stufen:
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 ist die entscheidende Prüfung: Es führt den Code innerhalb des gepatchten Images aus und
vergleicht ihn Byte für Byte mit dem Interpreter über dasselbe Listing. miniload.py beweist nur,
dass das Image lädt und sein Einstiegspunkt ausgeführt wird.
ACE ersetzt eine native Funktion durch ein Trampolin in einen VM-Dispatcher. Der Dispatcher führt native x86-Handler aus, aber der architektonische Zustand der geschützten Funktion lebt im VM-Kontext.
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
Einige Operationen werden außerhalb der VM ausgeführt. Eine boxed instruction führt ihre ursprüngliche x86-Kodierung aus, nachdem die VM die benötigten Register wiederhergestellt hat, und gibt dann ihre Effekte an den Gastkontext zurück. Ein guest call ruft ein natives Ziel auf und setzt mit dem Rückgabewert und den im Gastzustand repräsentierten Windows-x64-ABI-Clobbers fort. Guest-Returns und Tail-Calls verlassen den Dispatcher vollständig.
Der Kontext enthält alle sechzehn General-Purpose-Register des Gasts, einschließlich des Gast-RSP, sowie gepackte Gast-Flags. Sein Layout variiert je nach Protector-Build, daher wird es durch Beobachtung des Save/Restore-Verhaltens der VM lokalisiert und nicht angenommen — probe.ctx_map meldet die an den Stores gemessene Zuordnung, und jedes ausgegebene Listing klammert einen Kontextzugriff mit seinem Slot-Offset ein.