
Executive Summary
Tencent’s kernel anti-cheat stack, shipped as a family of ACE-*.sys drivers, protects selected functions by rewriting them into a custom virtual machine. The original entry point becomes a trampoline into a .tvm section. Guest registers and flags are saved into a VM context, a shared dispatcher walks encrypted bytecode, and anything the VM does not model is executed as a boxed native instruction. On paper this looks like a serious barrier: the analyst no longer sees ADD and Jcc, they see an interpreter.
Aftermath Labs’ 31 July 2026 note argues the opposite. Classic virtual-machine obfuscators of this shape are weak against a lifting and recompilation framework that is allowed to know a handful of VM-specific facts. Guided symbolic execution inlines the dispatcher, promotes bytecode fetches to constants, folds a closed list of mixed Boolean-arithmetic identities, rewrites virtualized branches back into native Jcc, and lowers the result with a recovered stack frame. Across four ACE kernel drivers they report 815 of 865 virtualized functions recovered to native code — 94.2 percent — with ACE-GAME.sys at 99.3 percent. The output is clean enough that a third party (Daax of secret club / revers.engineering) was given the files to confirm coverage independently. This draft walks the architecture, the CET and SEH compatibility tricks, the IR rewrite rules published in the source, and the hunting implications for anyone who has to live with VM-protected kernel modules.

How to read this piece
Two audiences share the same article. Green boxes keep the mechanics in ordinary language. Blue boxes keep the operator-grade facts: offsets, unwind codes, instruction encodings, coverage numbers, and the exact IR the source published. Original screenshots, looping videos, the coverage table and the three code listings appear in the same reading order as Aftermath Labs placed them. Extra diagrams sit next to those originals; they do not replace them.
- If you do not reverse binaries for a living: read the green boxes, the architecture animation, the CET receipt-tape metaphor, the coverage table, and the defensive checklist. You will leave knowing what a VM protector is, why Tencent had to talk to Intel CET and Windows SEH, and why “it is virtualized” is no longer a conversation-ending claim.
- If you lift obfuscators: the blue boxes plus the verbatim VJCC patterns, VJCC template and MBA identity list are the payload. Pair them with the source’s boxed-instruction sink trick and the VIP-in-context heuristic.
- If you hunt kernel anti-cheat or VM-packed malware: jump to the coverage statistics, the trampoline census (E9 + int3), .tvm / .tvm0 sections, RDSSPQ/INCSSPQ in kernel code, and the detection notes. The same shapes show up in Themida and VMProtect families.
What a virtual-machine obfuscator actually is
A virtual-machine obfuscator does not put the program inside Hyper-V or VMware. It replaces selected native functions with an interpreter. The original x86-64 is encoded as bytecode for a private ISA. A dispatcher fetches the next virtual instruction, a handler implements it against a virtual register file, and control returns to the dispatcher. From the outside you see a small cluster of native handlers and a large ocean of data. From the inside the program still adds, compares and branches.
That design has been the workhorse of commercial protectors for two decades. VMProtect, Code Virtualizer / Themida, and a long tail of in-house VMs all share the same silhouette: save GPRs, enter a dispatch loop, hide the real control flow in bytecode. Tencent VM sits in that family. Aftermath Labs say they have believed for a long time that this classic style is not strong against an attacker who already owns a flexible lifter and recompiler. The Tencent work is the latest public demonstration of that belief, after their Themida write-up earlier in 2026.

Scope, ACE, and the interoperability frame
The target is Tencent’s ACE kernel anti-cheat, observed as ACE-GAME.sys, ACE-BASE.sys, ACE-BOOT.sys and ACE-CORE.sys. Aftermath Labs frame the work as reverse engineering for interoperability with ACE-protected software on Linux and Proton, under the exceptions that recognize reverse engineering for interoperability. They state that sharing of recovered artefacts was limited to trusted third parties for independent technical validation, and that the original protected software remains under copyright and the DMCA.
That frame matters for how this draft treats the material. The source publishes architecture, unwind tricks, IR patterns and coverage numbers. It does not publish a ready-to-run ACE unpacker, a cheat, or a bypass of a live anti-cheat service. This draft stays inside that envelope. Public IR and MBA identities are reproduced verbatim because they are already on the page. A working driver rewriter is not.
This work constitutes reverse engineering, decompilation, and de-virtualization performed solely for the purpose of achieving interoperability with ACE-protected software on Linux and Proton environments.
Aftermath Labs, legal disclaimer on the 31 July 2026 post
Prerequisite material the source points at
Aftermath Labs list three pointers before the introduction. They are not reproduced here; they are the reading list the authors assume.
- k0mkc, 29 July 2026 — a public Tencent VM note posted two days before this article. The Aftermath Labs piece reads as a more complete static-devirtualization treatment sitting on top of a conversation that was already moving.
- arXiv:2603.18355 — listed as prerequisite theory. Treat it as the authors’ pointer into the current lifting / MBA literature rather than as a substitute for the Tencent-specific heuristics below.
- arXiv:1909.01752 — a 2019 reference in the same list. Mixed Boolean-arithmetic identities of the kind Tencent uses have a long academic trail (MBA-Blast at USENIX Security 2021 is the paper most operators already know). The Tencent list is interesting because it is small and closed.
A later public toolchain, jz0/tvm-devirt, credits k0mkc and Back Engineering Labs and implements a static TVM devirtualizer in Rust. That repository is a third-party artefact. This draft does not treat it as Aftermath Labs’ official release, and it does not re-host recovered ACE drivers.
Introduction
Aftermath Labs write that interest in Tencent VM obfuscation has been climbing for months, that they have had complete static devirtualization of it for some time, and that others have reached similar deobfuscation results. They chose to publish once that fact was already in circulation. The article’s job is to explain the VM in detail: how it is built, how it stays compatible with Control-flow Enforcement Technology and Structured Exception Handling, and why it loses to guided symbolic evaluation.
A reader of their earlier Themida article had asked for coverage statistics and versioning. They do not have version information for Tencent VM. They do publish a per-driver census of virtualized versus recovered functions, and they state that Daax from secret club was given the files so the cleanliness of the output can be confirmed independently.
Virtual machine architecture
A virtualized function does not begin with its original prolog. The entry point jumps into the .tvm section. Stack space is allocated for a VM context. Every general-purpose register and EFLAGS is saved into that context — first onto the stack, then moved into the context area. Aftermath Labs note the resemblance to VMProtect, which pushes all GPRs and then has the first handlers pop them into the VM’s own storage. Execution then threads in and out of a shared dispatch loop. The loop is left in order to perform calls and boxed instructions.

Boxed instructions
Tencent VM models only a subset of AMD64. Anything outside that subset is a boxed instruction. The VM restores native context, executes the original opcode on the real CPU, then captures register state back into the VM context and resumes dispatch. At the moment the boxed instruction runs, machine state is indistinguishable from what the unvirtualized function would have produced, so the instruction does not need to be modelled at all.
That is a practical necessity, not a flourish. CPUID cannot be faked inside a software VM if you want the real leaf values. RDMSR, WRMSR and other architectural instructions are in the same bucket. More usefully for a lifter, every boxed instruction is a point where guest state is forced to materialize. Aftermath Labs use exactly that property to recover the original stack-frame size.
CET compatibility
Entering the dispatcher with CALL is a problem on hardware that implements Control-flow Enforcement Technology. A CET CALL pushes the return address onto a shadow stack as well as onto the data stack. A RET pops both and compares them. The VM’s calls into the dispatcher never return. Left alone, the shadow stack would grow for the lifetime of the virtualized function and eventually fault.
Tencent VM handles this at runtime rather than at protection time. It executes RDSSPQ to read the current shadow-stack pointer. RDSSPQ lives in the hint-NOP encoding space: on a processor or in a process without shadow stacks it retires as a NOP and leaves the destination register untouched. Zeroing the register beforehand and testing it afterward is therefore a feature check that is correct on old and new hardware and costs nothing on either.

When the check says CET is active, the VM issues INCSSPQ with a register holding 2. That advances the shadow-stack pointer by two entries and discards the two return addresses that no matching RET will ever consume.



SEH compatibility
The VM is covered by a single large .pdata entry whose unwind information spans the entire VM range: entry, dispatcher and handlers. That unwind info carries a language-specific exception handler for the VM. A never-executed dummy function carries what Aftermath Labs call phantom unwind info. Its only purpose is to describe unwind operations.
VM entry reserves a 0x68-byte local area. A 0x48-byte block of that is used exclusively during unwinding and holds all nine non-volatile registers. The phantom unwind info describes those saves. The address of the dummy function is kept in [RBP+0] for the entire lifetime of VM execution. The entry’s unwind info carries UWOP_SET_FPREG with RBP as the frame register, so unwinding is performed relative to RBP. RBP is saved at entry and is never used as scratch during VM execution, so the unwinder can recover a frame at any point.
When an exception is raised inside the VM, the VM’s exception handler copies every guest non-volatile register into the save area reserved at entry, overwrites the unwind target slot located eight bytes below the guest RSP with the guest RIP, and returns ExceptionContinueSearch.

The unwinder, following [RBP+0], picks up the dummy function as the next entry. Its .pdata is looked up. Through that phantom unwind info every guest non-volatile the handler just staged is written back into the native register of the CONTEXT record it belongs to. The unwinder then reads the unwind-target slot as RIP and advances RSP by eight, so CONTEXT->RIP holds the guest RIP and CONTEXT->RSP lands exactly on the guest RSP. The original function’s virtual unwind continues as guest state. Boxed instructions get chained unwind info as needed because they run outside the VM.

Guided symbolic execution
Guided symbolic execution, as used here, means lifting native AMD64 to a higher-level SSA intermediate representation that can be optimized and rewritten. The objective is to symbolically evaluate the entire virtualized function so that one IR function holds all of the routine’s semantics. The lifting loop needs guidance when it hits indirect control flow. Classical VM obfuscation uses bytecode to choose which handler runs next. In this VM, register R11 holds the address of the bytecode the interpreter consumes.
The guidance is obfuscation-specific. For Tencent VM the authors want to symbolically inline the call to the dispatcher loop. A blunt heuristic works: follow calls that have an int3 placed directly after them. In the symbolic-evaluation path, every one of those calls is a call into the dispatcher. The alternative is to declare the dispatcher function as a valid call target to follow or inline.
Indirect control flow inside the VM is solved by promoting bytecode to a constant inside the SSA IR, so later passes can fold the decryption arithmetic away. That promotion must be scoped. Original semantic loads in the guest program must not be frozen into constants. If they are, the recovered function will hallucinate a single memory image.
Virtualized loops must not be unrolled by relifting the same VIP forever. The lifter tracks VIP and, if the next VIP / handler has already been lifted, inserts a back-edge. VIP tracking is obfuscation-specific. For Tencent VM, VIP lives in the VM context. The offset is resolved dynamically by finding the last stored value into the context that is a pointer into .tvm0 (bytecode lives there). Aftermath Labs say the heuristic works well and reveals VIP automatically.

Virtualized conditional control flow
Native Jcc is a single instruction that reads EFLAGS and picks a target. Tencent VM expands the flag comparison into multiple VM handlers. When symbolic evaluation stops on indirect control flow whose destination is still symbolic, two things are possible: you are sitting on a virtualized Jcc, or your optimizations are incomplete.
Virtualized Jcc logic is turned back into a native Jcc by matching pre-defined IR SSA DAGs. If the current indirect control flow matches a DAG, a rewrite fires. Branch targets come out of the same DAG, because the pattern also says where the destinations live in the IR.
The source publishes the pattern table first: one family per flag, four patterns each, covering whether the handler subtracted zero or subtracted the mask, and whether the subsequent test is equality or inequality on ZF of that subtraction. Then it publishes the generic template that those patterns instantiate.
; ── CF ── mask 0x1, idx 0
pattern vjcc.cf.ae.zero { body(0x1, 0x0) ; %r = R{e} %z } => { %r = R{ae} %cf }
pattern vjcc.cf.b .zero { body(0x1, 0x0) ; %r = R{ne} %z } => { %r = R{b} %cf }
pattern vjcc.cf.b .mask { body(0x1, 0x1) ; %r = R{e} %z } => { %r = R{b} %cf }
pattern vjcc.cf.ae.mask { body(0x1, 0x1) ; %r = R{ne} %z } => { %r = R{ae} %cf }
; ── PF ── mask 0x4, idx 1
pattern vjcc.pf.np.zero { body(0x4, 0x0) ; %r = R{e} %z } => { %r = R{np} %pf }
pattern vjcc.pf.p .zero { body(0x4, 0x0) ; %r = R{ne} %z } => { %r = R{p} %pf }
pattern vjcc.pf.p .mask { body(0x4, 0x4) ; %r = R{e} %z } => { %r = R{p} %pf }
pattern vjcc.pf.np.mask { body(0x4, 0x4) ; %r = R{ne} %z } => { %r = R{np} %pf }
; ── ZF ── mask 0x40, idx 3
pattern vjcc.zf.ne.zero { body(0x40, 0x0) ; %r = R{e} %z } => { %r = R{ne} %zf }
pattern vjcc.zf.e .zero { body(0x40, 0x0) ; %r = R{ne} %z } => { %r = R{e} %zf }
pattern vjcc.zf.e .mask { body(0x40, 0x40) ; %r = R{e} %z } => { %r = R{e} %zf }
pattern vjcc.zf.ne.mask { body(0x40, 0x40) ; %r = R{ne} %z } => { %r = R{ne} %zf }
; ── SF ── mask 0x80, idx 4
pattern vjcc.sf.ns.zero { body(0x80, 0x0) ; %r = R{e} %z } => { %r = R{ns} %sf }
pattern vjcc.sf.s .zero { body(0x80, 0x0) ; %r = R{ne} %z } => { %r = R{s} %sf }
pattern vjcc.sf.s .mask { body(0x80, 0x80) ; %r = R{e} %z } => { %r = R{s} %sf }
pattern vjcc.sf.ns.mask { body(0x80, 0x80) ; %r = R{ne} %z } => { %r = R{ns} %sf }
; ── OF ── mask 0x800, idx 5
pattern vjcc.of.no.zero { body(0x800, 0x0) ; %r = R{e} %z } => { %r = R{no} %of }
pattern vjcc.of.o .zero { body(0x800, 0x0) ; %r = R{ne} %z } => { %r = R{o} %of }
pattern vjcc.of.o .mask { body(0x800, 0x800) ; %r = R{e} %z } => { %r = R{o} %of }
pattern vjcc.of.no.mask { body(0x800, 0x800) ; %r = R{ne} %z } => { %r = R{no} %of }
template vjcc<FLAG, MASK, IDX, CC_SET, CC_CLEAR> {
%rf = X86ReadFlags %f[0..5]
%w = launder %rf
%m = And %w, imm MASK
%s = Sub %m, imm SUB where SUB ∈ { 0, MASK }
%z = X86Flag.ZF %s
%r = R{KIND} %z where KIND ∈ { e, ne }
} => {
%r = R{ (SUB == MASK) ⊕ (KIND == ne) ? CC_SET : CC_CLEAR } %f[IDX]
}
instantiate vjcc<CF, 0x1, 0, b, ae>
instantiate vjcc<PF, 0x4, 1, p, np>
instantiate vjcc<ZF, 0x40, 3, e, ne>
instantiate vjcc<SF, 0x80, 4, s, ns>
instantiate vjcc<OF, 0x800, 5, o, no>
A worked CF example, staying inside the published rules. body(0x1, 0x0) plus R{e} on ZF of the subtraction is jae/jnb (ae). The same body with R{ne} is jb/jc. Switching SUB to the mask 0x1 flips the sense. That is why four patterns exist per flag rather than two. An incomplete matcher that only handles the zero-SUB forms will invert half of the recovered branches.
Simple MBA identity rule reduction
Mixed Boolean-arithmetic obfuscation rewrites a simple operation such as A + B into a nest of ANDs, ORs, XORs, additions and negations that still compute A + B. Strong MBA is ugly: high-degree polynomials over bits, constants inside bitwise operands, and identities that need 1-bit folding (MBA-Blast, SiMBA) or synthesis. Tencent VM, in Aftermath Labs’ telling, uses trivial identities applied recursively. The list they publish is exhaustive for this VM. Put the identities in an inst-combine ruleset, run to a fixed point, and the MBA layer falls over.
(A|B) + (A&B) = A + B
(A^B) + (A&B) = A | B [from ((B^A)+(B&A)) → x|y]
(A|B) - (A&B) = A ^ B
((A|B)^A) + A = A | B [also A + ((A|B)^A)]
~((~A ^ ~B) | ~A) = A & B [De Morgan variant]
~(((A^B) & ~B) ^ ~A) = A & B
A - (A - (A&B)) = A & B
B - (((A&B)&B) ^ B) = A & B
((c&A) ^ A) | A = A [+ all operand orderings]
(A & ((A^c) | c)) = A
(((A&c) ^ A) | A) = A
(((c|A) ^ c) | A) = A
(((A^B)|B) & A) + B = A + B [((A^B)|B)=A|B, (A|B)&A=A]
(A&B) + (A|B) = A + B
~(A - c) = -A + (c-1) [reported as -A, const folded]
~( <A+c gadget> ) = -A + c

A second worked identity, written as bits
Take A – (A – (A & B)). Inner A – (A & B) clears from A every bit that is set in both A and B, which is A & ~B. Subtracting that from A again yields A – (A & ~B) = A & B. The VM can nest this inside an OR-identity and an absorbing form and the SSA tree looks busy. After two rewrites it is a bitwise AND. That is the entire MBA story on this target: depth, not novelty.
Lowering
Before the SSA IR can become native code again, the authors concretize RSP to an arbitrary constant at the start of lifting. Existing optimizations then fold RSP modifications. Concrete RSP is also the heuristic for “are we at a VMEXIT?” They note you can keep RSP symbolic; this is simply the approach they already used on Themida, VMProtect, vxlang, Tencent VM and the Denuvo anti-cheat driver.
The original stack-frame size still has to be recovered. Dynamic stack allocation is rare in AMD64 PE functions, so at sink points — calls and boxed instructions — RSP reveals the original frame. Aftermath Labs call this the stack-frame high-water mark.
Once the high-water mark is known, the recompiler rebuilds a prolog and epilog that represent the original frame. If the recompiled code spills, the original frame is placed after the recompiler’s own frame. Any reference to RSP at or above the return address is adjusted by the new spill size. For functions without dynamic stack allocation, that is a correct devirtualized frame.
Deobfuscation coverage statistics
Coverage is measured per driver as the fraction of virtualized functions successfully recompiled to native code. A virtualized function is identified by its entry trampoline (E9 rel32 padded with int3). After devirtualization the trampoline retargets from the .tvm0 bytecode interpreter to the recompiled .devirt code. Functions the tool could not recover still point into .tvm0.
| Driver | Virtualized functions | Devirtualized | Coverage |
|---|---|---|---|
ACE-GAME.sys | 134 | 133 | 99.3 % |
ACE-BASE.sys | 329 | 313 | 95.1 % |
ACE-BOOT.sys | 235 | 219 | 93.2 % |
ACE-CORE.sys | 167 | 150 | 89.8 % |
| Total | 865 | 815 | 94.2 % |
Across the four kernel drivers, 815 of 865 virtualized functions (94.2 %) were fully recovered to native code.


The recovered DriverEntry is ordinary WDM: it fills MajorFunction slots for create/close, device control and shutdown, sets DriverUnload, calls AceGameStartup, then RtlInitUnicodeString / IoCreateDevice / IoRegisterShutdownNotification on \Device\ACE-GAME. That is the point of the screenshot. After a 94 percent recovery you are no longer staring at a dispatcher. You are staring at a driver.
Limitations
Ranged for-loops collide with the aggressive indirect-control-flow optimizations. In a construct such as for (int i = 0; i < 10; i++), the virtualized i < 10 becomes a VJCC. Because i starts at 0, constant folding keeps only the loop-taken destination, which is the back-edge. The lifter has already lifted that back-edge, so it stops and the recovered function is an infinite loop. Aftermath Labs say relifting back-edges may be required, symbolizing everything except VIP.
Where this sits next to Themida, VMProtect and Denuvo
Aftermath Labs are explicit that the same lowering tricks were already used on Themida, VMProtect, vxlang, Tencent VM and the Denuvo anti-cheat driver. Their May 2026 Themida article argued the techniques apply to pretty much every virtual-machine obfuscator with minor modifications. The Tencent article is the kernel-anti-cheat data point in that series.
What repeats across the family is the silhouette: GPR save into a context, a shared dispatcher, bytecode-chosen handlers, boxed native escapes, MBA on the ALU, virtualized flags, a VIP that can be named. What changes is the compatibility skin. Tencent’s skin is CET (RDSSPQ / INCSSPQ 2) and a phantom-unwind SEH story. VMProtect’s skin is different handler shuffling and mutation. Themida’s skin is a different bytecode encryption and a different boxed set. A lifter that is built as a framework with per-VM guidance plugins will keep winning. A lifter that is a single hardcoded Tencent script will die on the next vendor.
Detection, hunting, and what this looks like on disk
Defenders do not need to recover 815 functions to benefit from the write-up. The published tells are already enough to build a hunting checklist for VM-protected kernel modules and for user-mode cousins that share the silhouette.
Static tells
- PE sections named
.tvm,.tvm0, or obvious siblings, with a high entropy data stream and a small native dispatcher. - Function entries that are a single E9 rel32 followed by int3 padding, targeting that section.
- A language-specific exception handler covering an unusually large .pdata range that includes a dispatcher.
- RDSSPQ (hint-NOP space) followed by a conditional INCSSPQ with immediate-class 2, inside kernel code that is not the Windows CET runtime itself.
- A dummy function that is never called, whose unwind codes describe nine non-volatile saves, and whose address is stored at [RBP+0] from a VM entry that uses UWOP_SET_FPREG.
Behavioral tells
- A kernel module that spends a large fraction of sampled IP in one dispatcher loop with a rolling bytecode pointer.
- Frequent native context restore / execute / recapture around CPUID, MSR, or similarly unmodelable instructions (the boxed path).
- CALL sites that never RET, paired with shadow-stack pointer adjustment on CET-enabled hosts.
ATT&CK and CWE, kept honest
| Lens | ID | How it applies here |
|---|---|---|
| Protector as defense evasion | T1027.002 Software Packing | Selected functions are replaced by an interpreter and bytecode. This is packing, even when the vendor is anti-cheat rather than malware. |
| Protector as obfuscated file | T1027 Obfuscated Files or Information | MBA identities and virtualized flags hide the original ALU and branches from linear disassembly. |
| Analyst technique, not attacker | T1127 / research | Guided symbolic execution is the analyst’s tool. Do not file it as an adversary technique just because the subject is anti-cheat. |
| If an ACE driver is later buggy | CWE-269 / CWE-787 etc. | Not claimed by this source. Keep LPE hunts in a different ticket from packing hunts. |
| Exception-path correctness | CWE-755 Improper Handling of Exceptional Conditions | The phantom-unwind design is how Tencent avoids this class inside the VM. A broken port of the same idea would land here. |
What this article does not give you
- A ready-to-run ACE unpacker, a recovered ACE-*.sys image, or a cheat.
- A filled-in Tencent bytecode decoder beyond the heuristics and IR already published by Aftermath Labs.
- A claim that every in-house VM is this weak. The authors’ point is that classic VMs of this silhouette are. Their CodeDefender pitch is that a protector can be built to fight this exact pipeline.
- A claim of a Microsoft-serviced vulnerability. CET and SEH compatibility here are engineering, not CVEs.
Key Takeaways
- Tencent VM is a classic GPR-save / shared-dispatcher / bytecode-handler virtual machine sitting in ACE kernel drivers, with boxed native escapes for the rest of AMD64.
- CET compatibility is a runtime feature check (RDSSPQ) plus INCSSPQ 2, because CALLs into a dispatcher that never returns would otherwise exhaust the shadow stack.
- SEH compatibility is a single large .pdata blanket, a frozen RBP frame pointer, nine staged non-volatiles, a dummy function with phantom unwind info at [RBP+0], and ExceptionContinueSearch.
- Guided symbolic execution inlines the dispatcher (CALL+int3 heuristic), promotes .tvm0 bytecode loads, tracks VIP from the last .tvm0 pointer stored into the VM context, and refuses to unroll seen VIPs.
- Virtual Jcc is a four-pattern-per-flag DAG on CF/PF/ZF/SF/OF. The published template is enough to rewrite it back to native Jcc and to extract both targets.
- The MBA layer is a closed list of trivial identities. Inst-combine to a fixed point is the published solvent; this is not a job that, on this VM, requires MBA-Blast as the first tool.
- Lowering reuses a concretized RSP, a stack-frame high-water mark at boxed/CALL sinks, and a spill-then-guest frame layout. Reported coverage is 815/865 functions (94.2 percent) across four ACE drivers, with ACE-GAME.sys at 99.3 percent.
- The remaining failure mode they highlight is ranged for-loops whose VJCC is over-folded into a single back-edge. Relift with everything except VIP symbolic is the stated way out.
Defensive Recommendations
- Inventory VM-protected kernel modules. Section names, E9+int3 trampolines into those sections, and oversized .pdata ranges are enough for a first census of ACE and of look-alikes.
- Do not treat “it is virtualized” as the end of an IR ticket. If a kernel driver in your estate is VM-protected, assume a capable shop can recover the majority of it statically, because that is what this paper just showed on ACE.
- Hunt RDSSPQ/INCSSPQ pairings in third-party kernel code. They are a CET-hygiene tell for interpreters that CALL and never RET. Baseline the Windows CET runtime so you do not page yourself.
- If you ship a protector, stop shipping this silhouette. A shared dispatcher, a context-resident VIP, boxed sinks, and recursive trivial MBA are a solved combination. Anti-lifting design (the authors’ CodeDefender claim, and the 2026 MBA-hardening literature) is the actual product conversation.
- If you ship an anti-cheat driver, budget for interoperability analysis. Aftermath Labs’ stated purpose is Linux/Proton interoperability. Closed kernel ACE on titles that players reasonably run under Proton will be reverse engineered. Plan a supported path instead of relying on the VM to be the path.
- Keep recovered artefacts inside the legal envelope the source used. Interoperability research and independent validation with a named third party is one thing. Redistributing unpacked ACE images or a cheat kit is another.
- For malware crews that reuse the same VM grammar: the VJCC DAGs and MBA identities in this article are reusable detections of the grammar, not of ACE specifically. YARA on RDSSPQ+INCSSPQ+dispatcher loops will overfit; prefer structural rules in your lifter and a trampoline census on disk.
- Watch ACE-CORE.sys-class tails. A 10 percent uncovered set is where interesting control flow hides. If you are doing a security review of a VM-protected driver, start with the functions that still point into .tvm0 after a recovery pass.
Conclusion
Aftermath Labs close where they opened. Classic virtual-machine obfuscation is not strong in the face of guided symbolic evaluation. Simple compiler passes — constant promotion, constant folding, and a short MBA identity list — were enough to simplify Tencent VM. They present the ACE numbers as evidence, the CET and SEH chapters as proof that even a VM which did its Windows-hygiene homework still lifts, and their CodeDefender product as the place they have tried to block this exact attack. Analyzing hardened targets, they write, is how a protector learns which pitfalls are obvious only after someone has driven a lifter through them. Consulting and product contact sit on the Aftermath Labs site; the post’s visible mailbox is Cloudflare-protected.
For the rest of us the lesson is plainer. A virtual machine that saves GPRs, dispatches on bytecode, boxes the leftover ISA, and obfuscates arithmetic with a pocket sheet of identities is a compiler problem. Compiler problems get solved. The interesting engineering in this paper is not a magic deobfuscator. It is the discipline of naming VIP, naming the boxed sinks, naming the Jcc DAGs, and then letting an ordinary optimizer finish the job.
Original text: “Static Devirtualization of Tencent VM” by author not clearly listed (site: Aftermath Labs) at Aftermath Labs.


