mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:54:31 +08:00
A new documented route for logic protected as a private JS opcode stream (jsvmp): recognition signals, the call-it-first route, stack- and register-machine reading, AST instruction extraction, the four JS evaluation traps, constant folding and control-flow recovery. Every procedure is inferred (absorbed from three public write-ups, anonymized; none run on a real target) and labelled so. The one shipped piece is the loader decoder (base64 -> varint -> ZigZag), measured against its own round trip (2,016 values exact, canonical varint/ZigZag layouts pinned, truncation raises) and it decodes the write-up's own sample literal end to end. Wired into the symptom index, routing tables, coverage ledgers and the capability matrix; RN/Hermes still not covered, with the jsvmp carve-out stated. Also clears two warnings: an E302 in the new script, and the long-standing check_commands warning where a tool-verification transcript quoted check_budget.py with its result inline (annotations moved to a continuation line, content intact). Ran: check_repo/check_refs/check_routing/check_commands/check_budget (all exit 0), flake8 clean, pytest 763 passed / 199 skipped / 25 xfailed.
79 lines
3.0 KiB
Python
79 lines
3.0 KiB
Python
"""Round-trip and known-vector tests for the jsvmp loader decoder.
|
|
|
|
The decoder implements the loader chain documented in
|
|
`skills/apk-reverse/references/jsvmp-recovery.md` (base64/hex -> varint -> ZigZag).
|
|
Its evidence is the round trip plus pinned byte layouts -- no real jsvmp target was
|
|
ever decoded in this repository, which is why these tests pin encodings that do not
|
|
depend on the encoder under test having written them.
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'skills',
|
|
'apk-reverse', 'scripts'))
|
|
|
|
from jsvmp_loader_decode import ( # noqa: E402
|
|
b64_to_bytes, decode_program, hex_to_bytes, main, varint_decode, varint_encode,
|
|
zigzag_decode, zigzag_encode,
|
|
)
|
|
|
|
|
|
def test_zigzag_known_vectors():
|
|
# The write-up's formula (n >> 1) ^ -(n & 1): neighbours interleave 0,1,2,3...
|
|
assert [zigzag_decode(u) for u in (0, 1, 2, 3)] == [0, -1, 1, -2]
|
|
assert [zigzag_encode(n) for n in (0, -1, 1, -2)] == [0, 1, 2, 3]
|
|
assert zigzag_encode(-1) == 1 and zigzag_decode(1) == -1
|
|
|
|
|
|
def test_varint_known_layout():
|
|
# Raw varint(300) = AC 02 is the canonical protobuf example; the decode-side layout
|
|
# must not depend on this repository's encoder having produced it.
|
|
assert varint_decode(b'\xac\x02') == [(0, 300)]
|
|
assert varint_decode(b'\x7f\x80\x01') == [(0, 127), (1, 128)]
|
|
# varint_encode is the full-chain encode side: it zigzags FIRST, so signed 300
|
|
# encodes as zigzag(300)=600 -> D8 04 (and raw 600 would be AC 04, not AC 02).
|
|
assert varint_encode([1, -1]) == b'\x02\x01'
|
|
assert varint_encode([300]) == b'\xd8\x04'
|
|
|
|
|
|
def test_varint_truncation_raises():
|
|
# A stream that ends mid-value is truncation (or not varint) -- it must raise
|
|
# rather than silently decode short.
|
|
for truncated in (b'\xac', b'\x80'):
|
|
try:
|
|
varint_decode(truncated)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
raise AssertionError('truncated varint %r decoded silently' % truncated)
|
|
|
|
|
|
def test_round_trip_random_and_extremes():
|
|
import random
|
|
rng = random.Random(7)
|
|
values = [0, 1, -1, 127, -127, 128, -128, 2 ** 31 - 1, -(2 ** 31)]
|
|
values += [rng.randrange(-(2 ** 31), 2 ** 31) for _ in range(500)]
|
|
decoded = [v for _, v in decode_program(varint_encode(values))]
|
|
assert decoded == values
|
|
|
|
|
|
def test_b64_and_hex_frontends():
|
|
blob = varint_encode([1, -1, 300])
|
|
import base64
|
|
assert b64_to_bytes(base64.b64encode(blob).decode()) == blob
|
|
# Stripped padding (the write-up's loader strips '=') must still decode.
|
|
text = base64.b64encode(blob).decode().rstrip('=')
|
|
assert b64_to_bytes(text) == blob
|
|
assert hex_to_bytes(blob.hex().upper()) == blob
|
|
|
|
|
|
def test_cli_selftest_and_decode(capsys):
|
|
assert main(['--selftest']) == 0
|
|
assert 'OK' in capsys.readouterr().out
|
|
encoded = varint_encode([5, -7])
|
|
import base64
|
|
rc = main(['--b64-string', base64.b64encode(encoded).decode(), '--json'])
|
|
out = capsys.readouterr()
|
|
assert rc == 0
|
|
assert '"values": [5, -7]' in out.out
|