EAC Linux Devirtualization Research (plx)
Important
[!] Independent Personal Project & Hex-Rays Non-Liability Notice
Although this research was conducted by a Hex-Rays engineer and achieved in part using internal Hex-Rays software (including an internal development build of IDA and Hex-Rays IDA Assist), this repository is strictly an independent, personal project.
Hex-Rays SA does not endorse, sponsor, or affiliate itself with this work, makes no representations or warranties of any kind, and assumes NO responsibility or liability whatsoever for this project, its methodology, its findings, or any direct or indirect consequences arising from its publication or use. All research, code, artifacts, and conclusions expressed herein are solely those of the author acting in a purely personal capacity.
Caution
[!] Strict Research-Only Scope & Prohibition of Misuse
All materials, scripts, documentation, and binary analysis artifacts in this repository are published exclusively for educational, academic, and defensive security research purposes—specifically to study virtual machine protection architectures and advance automated program-recovery methodologies.
Under NO circumstances may any part of this work be misused for unauthorized tampering, circumvention of security or anti-cheat mechanisms in live or production environments, software piracy, cheating, or any other unlawful, unethical, or malicious activity. By accessing this repository, you agree that the author bears zero liability for any misuse of the information or artifacts provided herein.
This repository analyzes plx.elf, an Easy Anti-Cheat Linux x86-64 ELF containing native and VM-protected code. It preserves the input, decoded string catalogs, candidate lifts, compiled analysis libraries, and combined ELF/IDA images.
Open validated_systematic/plx_devirt_systematic.i64 for the checked combined image. The matching ELF and address map accompany it. The bundle contains the exact source/data snapshot used for validation; the parent checkout can be rebuilt independently. An already loaded IDA database does not update when a library is rebuilt.
What the outputs mean
| Profile | Files | Meaning |
|---|---|---|
| Checked | devirtualized_pie.c, libeac_devirt.{so,dylib}, devirtualization_manifest.json |
12 models with named executable fixtures; 96 other entries forward to original functions |
| Candidate | devirtualized_candidates.c, libeac_devirt_candidates.{so,dylib}, devirtualization_candidates_manifest.json |
All 108 candidate bodies retained for inspection; incomplete lifting remains explicit |
| Combined checked image | plx_devirt_systematic.{elf,i64,json} |
Original code/data and checked entries in one address space, with imports bound to original .text/PLT targets |
An original fallback is not a recovered implementation. Its public entry transfers to original_sub_*, preserving the tested incoming register/stack state. The original VM still executes there. IDA comments and the JSON map distinguish fallbacks from fixture models. Six-register C signatures are analysis interfaces, not proof of original source-level arities.
The fixture models are sub_21748, sub_2C930, sub_44D10, sub_5F195, sub_6F0D0, sub_73810, sub_73C80, sub_74180, sub_74700, sub_74920, sub_74B80, and sub_74DE0. Their scopes differ: the secondary checks have entry-boundary fixtures, while the main check/caller models have complete-function fixtures under controlled bindings. Whole-program equivalence remains unknown. These are static analysis images, not certified runtime replacements or ASLR-independent guest loaders.
Catalog metadata retains 189 distinct .rodata addresses and 167 unique decoded texts in both profiles. Text retention does not establish executable reachability or runtime encoding. Encrypted-pointer helpers keep original storage addresses; decoded UTF-8 metadata does not replace encrypted or UTF-32LE storage.
Current evidence
The frozen bundle passed 56 tests, with source and binary hashes unchanged during validation. It includes 1,008 emitted-instruction comparisons at -O0/-O2, arithmetic-condition comparisons, original-VM check/caller fixtures, native-boundary comparisons, and expanded mount-reader cases.
All 24 zeroing helpers are compared with original instructions. Three preserve incoming RAX; their models no longer invent a byte-count return. All 96 original fallbacks are checked up to the original entry for 16 GPRs, RFLAGS, 16 XMM registers and 512 stack bytes. Both profiles pass the measured sub_48A6A0(0,0,0,0,0,0) == 0 counterexample.
The combined ELF/IDA checks verify 120 native bindings, 108 generated entries, 190 patched branches and 506 relocation results, while preserving original code/data bytes. These checks establish address/linkage properties separately from fixture behavior.
The candidate inventory gate remains failed: the current candidate audit contains 29 closed cycles in 26 functions and witness literals in four functions. Closed cycles alone do not prove a defect or feasibility. Literal predicates, partial joins, unsupported instructions, indirect exits, ABI context and uncovered paths still require recovery or rejection. A passing fixture suite does not certify every candidate body.
| Evidence | Scope |
|---|---|
| SYSTEMATIC_RECOVERY.txt | Method, assumption register, falsification probes, complexity/bounds, findings and limits |
| systematic_validation.json | Exact source/build hashes, all seven suites and their results |
| systematic_cfg_audit.json | Every candidate function, recovery stage, closed cycles, witnesses and literal predicates |
| systematic_artifact_validation.json | Catalog retention, compiled calls and static readonly-store scan |
| systematic_merged_elf_validation.json | Original-byte preservation, bindings, relocations and actual branch targets |
| systematic_merged_ida_validation.json | Actual IDA function starts, branch references and fixture-model return inference |
| systematic_decompilation.json | Raw Hex-Rays exports; display evidence is separate from execution evidence |
The shared checkout was rebuilt concurrently during an earlier validation. Its hash guard rejected that run. Final evidence therefore applies to the isolated validated_systematic/ bundle, not later parent-worktree changes.
Layout
plx/
|-- plx.elf Original capture artifact
|-- extracted/ ELF extent, capture tail, protected slices
|-- vm_*_catalog.json Handler and entry inventories
|-- elf_symbols.json Names and unwind ranges
|-- decrypted_*strings*.json Decoded catalogs
|-- devirtualized_all_stubs.json Per-entry trace records
|-- devirtualized_functions.json Function grouping/recovery metadata
|-- devirtualized_pie.c Checked model/forwarder source
|-- devirtualized_candidates.c Candidate source
|-- libeac_devirt* Compiled checked/candidate libraries
|-- devirtualization*manifest.json Profile-specific bindings and implementation kinds
|-- validated_systematic/ Frozen source, data, libraries, ELF/IDA image and evidence
|-- devirt_quality_audit/ Active and historical audit outputs
`-- plx_devirt_{merged,helpers,paths,stack,flags}.* Earlier snapshots
Rebuild and validate
Run from the intended source snapshot. Requires Python, Capstone, Unicorn, Clang/LLD and, for Mach-O, the macOS SDK. IDA 9.5 is used for the database checks.
cd validated_systematic
python3 build_compilable_pie.py --profile candidate --build
python3 build_compilable_pie.py --profile checked --build
python3 devirt_quality_audit/validate_systematic.py
python3 merge_devirt_elf.py --output plx_devirt_systematic.elf
python3 devirt_quality_audit/validate_merged_elf.py plx_devirt_systematic.elf \
--result devirt_quality_audit/systematic_merged_elf_validation.jsonThe merger accepts a generated ELF hash recorded by a passing fixture validation. It does not turn a failed candidate inventory gate into a full-recovery claim. Original ELF section addresses and bytes remain intact; generated code is rebased by 0x1000000, relocations are applied, and resolved branches target original implementations. Original system-library imports remain original PLT imports. Mach-O models using Linux FS/GS require an appropriate guest TLS binding when executed.
prepare_merged_idb.py --batch --result=... runs inside IDA against the merged ELF, verifies its input hash, annotates implementation kinds, checks actual references/function starts and saves the database. The frozen IDB uses an isolated user profile without user plugins or startup scripts; parent IDA settings and databases are preserved.
Continue recovery
python3 recover_vm_cfgs.py --all --workers 3 --timeout 60 --apply python3 devirt_quality_audit/audit_cfgs.py --require-recovered
The recovery driver discovers actual native entry JMPs, checkpoints complete regions and compares original instructions with production-lowered C before application. Source-invalid fixtures, incomplete CFGs, dangling continuations, witnesses, unsupported/indirect edges and timeouts remain explicit. Partial regions do not establish complete functions. The current implementation uses bounded path contexts; unresolved runtime loops require state merging or rejection.
The second command audits the candidate profile and exits nonzero while legacy, partial, prior-schema or witness-containing bodies remain. This expected failure records unresolved work. Checked fallbacks preserve original execution without resolving that lifting work.
Trace a compiled symbol to the original
python3 devirt_quality_audit/call_boundaries.py python3 trace_original_functions.py _original_sub_32360 python3 devirt_quality_audit/call_boundaries.py --profile candidate python3 trace_original_functions.py --profile candidate _sub_2180E
Reports separate compiled addresses/stubs, source RVAs, unwind ranges, bounded native disassembly, compiled call sites and modeled VM-call origins. Use original_EA = original_image_base + source_rva; Mach-O preferred addresses are not original ELF RVAs. An unresolved_native record locates original code, but that code may itself enter another VM region. Mapping an address does not recover its implementation.
Historical evidence
The plx_devirt_merged.*, plx_devirt_helpers.*, plx_devirt_paths.*, plx_devirt_stack.* and plx_devirt_flags.* images preserve earlier stages. The matching REPORT.txt, HELPER_RESOLUTION.txt, VM_CONTINUATIONS.txt, STACK_STATE.txt, FLAGS_RECOVERY.txt, JSON reports and before_*/ directories retain their original validation scope. Their hashes and earlier counts must not be applied to the current profiles.
The latest systematic pass repairs shared flag/shift/rotate semantics, sign extension, overlapping memory state, ordered native leaf effects, unaligned C accesses, CPUID outputs, native helper transfer state and conditional CALL continuations. It removes plaintext materialization and forced decryption-branch shortcuts. Those repairs do not retroactively certify cached traces; the two-profile design makes that distinction explicit.