Skip to content

feat(vm): [Phase 1 - 4] Dalvik VM interpreter — Interpreter Basic, Core Instruction Set, Method Dispatch & Android API Stubs#2

Open
haeter525 wants to merge 20 commits intoev-flow:mainfrom
haeter525:android_emulator_enhance
Open

feat(vm): [Phase 1 - 4] Dalvik VM interpreter — Interpreter Basic, Core Instruction Set, Method Dispatch & Android API Stubs#2
haeter525 wants to merge 20 commits intoev-flow:mainfrom
haeter525:android_emulator_enhance

Conversation

@haeter525
Copy link
Copy Markdown
Member

@haeter525 haeter525 commented Apr 12, 2026

Key Changes

Phase 1 - Interpreter Basic

Execution Engine

  • engine.py: DalvikVM main loop with call-frame stack and invoke handling
  • errors.py: DalvikVM runtime error types
  • cmd_run.py: new dextrace run subcommand to run the Dalvik VM interpreter
  • _io.py: shared DEX/APK loader; handles APK with missing classes.dex and malformed DEX input

Register File

Call Stack & Frame

  • call_frame.py: per-call register snapshot/restore for callee isolation
  • state.py: VMState dataclass with pending_result lifecycle for invoke/move-result

Phase 2 - Core Instruction Set Support

  • handlers/: arithmetic, array, branch, compare, field, move, type_conv opcode handlers

Phase 3 - Method Dispatch

Vtable Dispatch

  • class_hierarchy.py: ClassHierarchy — builds vtable per class via DFS topological sort; provides O(1) resolve_virtual(class_desc, name, proto) → code_off
  • engine.py: invoke-virtual handler reads runtime class from heap, dispatches through vtable; new-instance handler allocates object on heap
  • dex_class_iter.py: iter_class_defs / iter_class_data_methods — low-level class_def and encoded_method iteration used by vtable builder

Object Heap

  • heap.py: ObjectHeap — integer handle → class descriptor; handle 0 reserved as null; reset between run() calls for isolation

Verbose Trace

  • cmd_run.py: --verbose flag wires trace_sink into DalvikVM; emits [INFO] messages for new-instance, invoke-virtual dispatch, and entry execution
  • cmd_run.py: --dump-regs flag prints non-zero register values after execution

Phase 4 - Android API Stubs & IoC Capture

Stub Registry

  • vm/android_stubs/__init__.py: REGISTRY keyed by static callee signature; return-type sentinels (VOID, Value, Wide, ObjectRef); StubCallable protocol so each stub gets (args, heap, api_calls) and records its own trace entry
  • vm/android_stubs/sms.py: Landroid/telephony/SmsManager;->getDefault() and sendTextMessage(...) — first concrete IoC-capturing stubs

Engine Wiring

  • engine.py: stub dispatch sits in front of vtable resolution and the external-miss path; Policy C external-miss (void → silent no-op, non-void → DexTraceNotImplementedError with full sig + pc); --strict-stubs escalates void misses to errors; vm.run() accepts str args and materializes them as Ljava/lang/String; heap handles
  • heap.py: per-handle value slot so stubs can read back analyst-supplied strings via heap.get_value(handle)
  • class_hierarchy.py: has_class() probe lets invoke-virtual distinguish in-DEX classes from external ones (external falls through to stub/external-miss instead of vtable miss)

CLI

  • cmd_run.py: --arg auto-parses ints (decimal or 0x-hex) and falls back to string (+-prefixed values stay as strings so phone-number IoCs aren't silently downgraded); --args '[...]' for explicit JSON typing of mixed lists; --trace prints captured api_calls as JSON; --strict-stubs flips the engine's external-miss policy

Tests + Sample Fetcher

  • tests/test_vm_run_p4.py: runs Ahmyth's SMSManager.sendSMS(String,String)Z end-to-end and asserts the captured trace contains the analyst-supplied phone + message text
  • tests/fixtures/p4_ahmyth_fetcher.py: SHA256-locked lazy fetcher ($DEXTRACE_AHMYTH_APK~/.cache/dextrace/p4_ahmyth.apk → MalwareBazaar); pytest.skip on FetcherError so offline runs stay green

Docs

  • DESIGN.md: adds the P4 [ERROR] unknown Android API: <sig> (pc=...) format and a decision-log entry explaining why analysts need full sig + pc
  • TODOS.md: tracks scope intentionally cut from P4/P5/P6 MVPs (StringBuilder/Uri/static fields, IOSocket URL demo, try/catch dispatch, const-string heap migration, invoke-interface, taint tracking, --explain)

How to use

Installation

git clone -b android_emulator_enhance https://github.com/haeter525/DexTrace.git
cd DexTrace
pip install -e .

Phase 1 — execute const/16 v0, #42 → return-void

$ dextrace run tests/fixtures/samples/p1_const_return.dex --entry 'Lp1;->main()V' --dump-regs
return: void
registers: v0=42
  • the register value v0 should be 42
  • the return value should be void

Phase 2 — execute recursive Fibonacci

$ dextrace run tests/fixtures/samples/p2_fib_recursive.dex --entry 'Lp2/Fib;->fib(I)I' --arg 10
return: 55
  • the return value should be 55

Phase 3 — vtable dispatch through inheritance

$ dextrace run tests/fixtures/samples/p3_inheritance.dex --entry 'Lp3/Main;->entry()I'
return: 2
  • Lp3/Main;->entry() allocates a Lp3/Mid; object and calls foo() via invoke-virtual
  • vtable dispatch resolves Lp3/Base;->foo()ILp3/Mid;->foo()I (override), returning 2
$ dextrace run tests/fixtures/samples/p3_inheritance.dex --entry 'Lp3/Main;->entry()I' --verbose
[INFO]  executing Lp3/Main;->entry()I with args=[]
[INFO]  new-instance: Lp3/Mid; → handle #1
[INFO]  invoke-virtual: Lp3/Base;->foo()I → Lp3/Mid;->foo()I
return: 2

Phase 4 — capture an Android RAT's SMS exfil call

$ dextrace run /path/to/Ahmyth.apk \
    --entry 'Lahmyth/mine/king/ahmyth/SMSManager;->sendSMS(Ljava/lang/String;Ljava/lang/String;)Z' \
    --arg 'hello from dextrace p4'
return: 1

Tests

  • All tests pass: pytest tests
  • Phase 1, Phase 2, Phase 3, and Phase 4 commands above produce the expected output (Phase 4 requires an Ahmyth sample via $DEXTRACE_AHMYTH_APK; otherwise the P4 integration test skips)

haeter525 and others added 11 commits April 11, 2026 20:45
- vm/decoder.py: walk_method() static walk adapter over DalvikDisassembler
  DecoderTables type alias, MethodNotFound exception
- cli/cmd_trace.py: dextrace trace subcommand with JSON envelope output
  matching cmd_disasm schema (version/format/source/method/instructions)
  stderr-only errors, exit codes 0/1/3
- cli/main.py: wire trace subcommand
- tests/fixtures/samples/p1_const_return.dex: Lp1;->main()I fixture
  (const/16 v0, 42; return v0) built by tools/gen_p1_fixture.py
- tests/test_vm_trace_p1.py: 9 tests — unit + CLI integration

Verification: result['instructions'][0]['mnemonic'] == 'const/16'  ✓
59/59 tests pass, 0 regressions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wrap_method() now catches ValueError/struct.error from DexResolver and
DalvikDisassembler and re-raises as DexParseError.
cmd_trace.py catches DexParseError and prints [ERROR] message, exits 3.

Before: python traceback to stderr, exit 1
After:  [ERROR] malformed DEX: File too small to contain a valid DEX header, exit 3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
apk_reader.read_file() raises KeyError when the entry doesn't exist.
The old `if not dex_bytes` guard was dead code — the exception happens
before it. Fix: check list_entries() before reading.

Before: KeyError traceback to stderr, exit 1
After:  [ERROR] APK does not contain classes.dex, exit 3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ISSUE-002: malformed DEX must raise DexParseError (not raw ValueError traceback)
ISSUE-003: APK without classes.dex must exit 3 with [ERROR] (not KeyError)

Found by /qa on 2026-04-11
Report: .gstack/qa-reports/qa-report-dextrace-2026-04-11.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add dextrace run subcommand: executes a method in the Dalvik VM
  interpreter with static analysis semantics (no JNI, no heap)
- Add vm/engine.py: step-budget interpreter loop with opcode dispatch
- Add vm/handlers/: arithmetic, branch, compare, field, move, array,
  type_conv instruction handlers
- Add vm/state.py, call_frame.py, register_file.py, errors.py, int_ops.py
- Add p2 fixture (Fib recursive DEX) + gen_p2_fixture.py
- Add Phase 2 tests: test_vm_run_p1, test_vm_run_p2, tests/vm/ unit suite

Review fixes (from /review):
- decoder.py: widen DexParseError catch to include IndexError, KeyError,
  OverflowError — real malformed DEX triggers opcode table KeyError
- cmd_trace.py: add is_file() guard to prevent IsADirectoryError traceback
- cmd_trace.py: stage BadZipFile catch, flags filter, resolve() path fix,
  REVIEW-001 regression test (non-ZIP APK → exit 3)
- decoder.py: remove unused DecoderTables type alias + dead imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract load_dex_bytes() to shared cli/_io.py and use it from both
cmd_trace and cmd_run. dextrace run now accepts .apk files, extracts
classes.dex, and returns clean errors when the APK is malformed or
missing classes.dex. Previously gave "expected a .dex file, got: '.apk'".

Also fixes ISSUE-005: add is_file() guard to cmd_run so a directory
named *.dex gives exit 1 (user error) instead of exit 3 (parse error)
with an internal OSError traceback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ISSUE-004: APK files rejected with wrong error in dextrace run
ISSUE-005: directory-as-input gives wrong exit code (3 instead of 1)

Found by /qa on 2026-04-11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cmd_run.py: suppress too-many-return-statements/branches on run()
- move.py: suppress unused-argument on handle_nop (intentional no-op)
- register_file.py: suppress protected-access in snapshot() (same class)
- gen_p1/p2_fixture.py: rename HEADER_SIZE→header_size, _uleb128 out→buf
- test_handlers.py: move imports to module top-level, remove unused imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gen_p1 and gen_p2 intentionally share boilerplate structure as
standalone tools building different DEX test fixtures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@haeter525 haeter525 marked this pull request as draft April 12, 2026 04:17
@haeter525 haeter525 changed the title feat(vm): Dalvik VM interpreter — trace + run commands (Phase 1 & 2) feat(vm): [Phase 1 & 2] Dalvik VM interpreter — Interpreter Basic & Core Instruction Set Support Apr 12, 2026
- cmd_run.py: add --dump-regs flag to print non-zero register values
  after execution; add _print_registers() helper
- engine.py: save final VMState after run(); expose final_registers
  property for post-execution register inspection
- gen_p1_fixture.py: change method from ()I/return to ()V/return-void
  so Phase 1 demo exercises const/16 + return-void with --dump-regs
- Regenerate p1_const_return.dex with updated bytecode and proto
- Update all tests to use new entry sig Lp1;->main()V and void semantics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@haeter525 haeter525 marked this pull request as ready for review April 12, 2026 06:13
- ClassHierarchy: topological vtable construction from class_def_items
- ObjectHeap: handle-based new-instance allocation
- DalvikVM: invoke-virtual vtable dispatch, null receiver + vtable miss errors
- trace_sink callback for --verbose: new-instance and invoke-virtual [INFO] lines
- p3_inheritance.dex fixture: Base/Mid/Main hierarchy for vtable dispatch verification
- 3 new trace_sink tests: new-instance trace, invoke-virtual dispatch trace, silent default

Verification: dextrace run tests/fixtures/samples/p3_inheritance.dex --entry 'Lp3/Main;->entry()I'
  → return: 2
…OR] messages

DESIGN.md specifies: [ERROR] null receiver: ...
Previous output:      [ERROR] VM error: null receiver: ...

Engine error messages are already DESIGN.md-formatted. Drop the spurious
'VM error: ' and 'not implemented: ' infixes from cmd_run.py error handlers.
@haeter525 haeter525 changed the title feat(vm): [Phase 1 & 2] Dalvik VM interpreter — Interpreter Basic & Core Instruction Set Support feat(vm): [Phase 1, 2 & 3] Dalvik VM interpreter — Interpreter Basic, Core Instruction Set & Method Dispatch Apr 21, 2026
haeter525 and others added 6 commits April 26, 2026 11:56
ObjectHeap entries become (class_desc, value) tuples with a new
get_value() accessor so String handles can carry their Python str
payload — required for stubs that capture string args (e.g. the SMS
phone/message text).

ClassHierarchy gains has_class() so external Android types
(SmsManager, etc.) fall through to the external-miss policy instead
of raising vtable-miss for an unknown class.
Adds vm/android_stubs/ — a Dict[sig, StubCallable] registry keyed by
the full method signature, with a StubResult tagged union
(VOID / Value / Wide / ObjectRef) so stubs can return any Dalvik
result shape without leaking heap concerns.

SMS stub family covers Ahmyth's IoC capture path:
  - SmsManager.getDefault()  → ObjectRef to a fresh handle
  - SmsManager.sendTextMessage(...) → VOID; records destAddr/scAddr/text

13 unit tests cover registry contract, StubResult variants, arg
materialization, and the SMS happy + null-receiver paths.
Wire the Android stub REGISTRY into invoke dispatch (sits in front of
both vtable resolution and the external-miss path). Adds a Policy C
external-miss policy: void misses are silent no-ops, non-void misses
raise DexTraceNotImplementedError with full sig + pc. --strict-stubs
escalates void misses to errors as well.

Also accept str args to vm.run(): each is materialized on the heap as
a Ljava/lang/String; handle so stubs can read it back via
heap.get_value. Heap.reset() and api_calls.clear() at run() entry
keep consecutive runs isolated.

invoke-virtual on a class outside the parsed DEX (no-stub external)
now falls through to external-miss instead of raising vtable miss.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… (P4)

--arg now accepts strings and auto-parses ints (decimal or 0x-hex);
'+'-prefixed values stay as strings so phone-number IoCs aren't
silently downgraded. --args takes a JSON list for explicit mixed
typing. --trace prints captured stub-call entries as JSON (also
embedded under 'api_calls' in --json output). --strict-stubs flips
the engine's external-miss policy to error on void misses too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an integration test that runs Ahmyth's
SMSManager.sendSMS(String,String)Z and asserts:
  - returns 1 on the linear no-throw path
  - api_calls captures SmsManager.getDefault() then sendTextMessage(...)
  - the analyst-supplied phone + message text reach the stub args
  - heap+api_calls reset between consecutive vm.run() calls
  - --strict-stubs and default policy both raise on non-stubbed APIs

Sample is fetched lazily by tests/fixtures/p4_ahmyth_fetcher.py with
SHA256 lock: $DEXTRACE_AHMYTH_APK → ~/.cache/dextrace/p4_ahmyth.apk →
MalwareBazaar download. FetcherError → pytest.skip so offline runs
stay green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DESIGN.md gains the P4 [ERROR] format for unknown Android APIs (full
sig + pc) and the corresponding decision-log entry explaining why
analysts need that level of detail to either skip, file, or stub.

TODOS.md tracks scope intentionally cut from P4/P5/P6 MVPs:
StringBuilder/Uri/static fields (P4.1), IOSocket URL demo (P4.2),
try/catch dispatch (P5.1), const-string heap migration (P5.2), richer
ExecutionTrace (P5.3), invoke-interface/polymorphic/custom (P5.4),
coverage compare (P5.5), taint tracking (P6.1), --explain (P6.2).
Each entry names the unblocking sample and the smallest scope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@haeter525 haeter525 changed the title feat(vm): [Phase 1, 2 & 3] Dalvik VM interpreter — Interpreter Basic, Core Instruction Set & Method Dispatch feat(vm): [Phase 1, 2, 3 & 4] Dalvik VM interpreter — Interpreter Basic, Core Instruction Set, Method Dispatch & Android API Stubs Apr 26, 2026
@haeter525 haeter525 changed the title feat(vm): [Phase 1, 2, 3 & 4] Dalvik VM interpreter — Interpreter Basic, Core Instruction Set, Method Dispatch & Android API Stubs feat(vm): [Phase 1 - 4] Dalvik VM interpreter — Interpreter Basic, Core Instruction Set, Method Dispatch & Android API Stubs Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant