
Tencent VM의 .tvm0 섹션을 디버추얼라이즈합니다. VM 핸들러를 게스트 수준 리스팅과 LLVM IR로 리프팅하고, CFG와 디스패치를 복구하며, 복구된 코드를 PE에 다시 패치합니다.
관련 Zenodo 레코드: 10.5281/zenodo.23474767.
⚠️ 면책 조항: 이 프로젝트는 승인된 보안 연구, 상호운용성 분석 및 교육 목적으로만 사용하도록 의도되었습니다. 상용 안티치트 시스템에 대한 우회 기능을 포함하거나 제공하지 않습니다. 대상 바이너리를 분석할 법적 권리가 있는지 확인하는 것은 전적으로 사용자의 책임입니다.
Tencent VM용 디버추얼라이저입니다. .tvm0 섹션에 있는 가상화된 코드를 복구하고, 각 VM 핸들러가 게스트의 레지스터와 메모리에 수행하는 작업을 리프팅하며, 복구된 함수를 일반적인 읽기+실행 섹션으로 바이너리에 다시 기록할 수 있습니다.
이것은 도구입니다: 모듈을 지정하면 구조와 코드를 돌려받습니다.
.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 | PATH에 의존하는 대신 link.exe/cl.exe가 있는 디렉터리 |
우선순위: 명령줄 플래그 → .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
함수마다 0x<root>.txt 목록 하나와 0x<root>.guest-state.json 하나가 생성됩니다. 옵션:
| 플래그 | 기본값 | 의미 |
|---|---|---|
--out=DIR | stdout | 목록 + guest state 기록 |
--root=0x… | 발견된 모든 항목 | 하나의 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 외부에서 실행된다. 박싱된 명령(boxed instruction) 은 VM이 필요한 레지스터를 복원한 후 원래의 x86 인코딩을 실행하고, 그 효과를 게스트 컨텍스트로 반환한다. 게스트 호출(guest call) 은 네이티브 대상을 호출하고, 반환값과 Windows x64 ABI 클로버(clobber)를 게스트 상태에 반영하여 재개한다. 게스트 반환과 테일 호출은 디스패처를 완전히 벗어난다.
컨텍스트는 게스트 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는 해당 호출자에 조건부인 결과를 생성하며, 페이로드는 이를 일반적인 복구로 승격하는 것을 거부합니다.