mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:54:31 +08:00
Batch 1 (full): Kotlin original-name recovery (new reference + zero-dep kotlin_name_recovery.py mining @DebugMetadata/@Metadata.d2/jadx rename comments, with -o/-p/--grep query modes); jadx advanced flags and a Fernflower/Vineflower second-engine tier in toolchain.md; BuildConfig sweep habit in recon.md; WebSocket/long-connection state-chain section in protocol-reverse.md. jsvmp-only (batches 2-3): A/B/C recovery ladder with black-box reuse boundary and a wire-parameter family index in jsvmp-recovery.md; new js-ast-deobfuscation.md pipeline layer (family detection, per-family pass order, safe-rewrite rules, minimal bootstrap eval, residue metrics) with js_obf_detect.py weighted detector. Wiring: routing.md/SKILL.md symptom+index rows, coverage-and-limits, route-inventory, evidence-summary, capability-matrix (+3 entries, labels kept honest: scripts observed on fixtures, absorbed methodology unverified). 12 new unit tests. Also: silence two environment-dependent mypy errors (yaml/frida sentinel assignments) and bump mypy python_version 3.9 -> 3.10 with the trade-off recorded. Gates: check_repo/check_refs/check_budget/check_commands/check_routing PASS; pytest 787 passed; flake8 clean; mypy clean.
68 lines
3.1 KiB
Python
68 lines
3.1 KiB
Python
"""Pinned-shape tests for the JS obfuscation family detector.
|
|
|
|
The detector (`skills/apk-reverse/scripts/js_obf_detect.py`) scores families by weighted
|
|
probes (hint token = 10, content regex = 3). These tests pin the routing decisions that
|
|
matter: each fixture selects its own family as the top pick, a hint outranks a single
|
|
content hit, plain code routes to the generic ladder, and the huge-literal threshold is
|
|
a parameter the caller can lower for small samples.
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'skills',
|
|
'apk-reverse', 'scripts'))
|
|
|
|
from js_obf_detect import ( # noqa: E402
|
|
FIXTURES, _jsvmp_fixture, pipeline_for, residue_metrics, score_families,
|
|
)
|
|
|
|
|
|
def test_each_fixture_wins_its_own_family():
|
|
for fam_id, body in FIXTURES.items():
|
|
detections = score_families(body, '', 'sample.js', 1500)
|
|
assert detections, 'no detection for %s' % fam_id
|
|
assert detections[0]['id'] == fam_id, (fam_id, detections)
|
|
|
|
|
|
def test_jsvmp_entry_needs_the_literal_threshold():
|
|
# The huge-literal probes only fire at the configured length: with the default 1500
|
|
# threshold a 210-char literal scores nothing for the literal itself, and the fixture
|
|
# is detected (if at all) through the dependency-array probe alone. Lowering
|
|
# --min-literal is the documented way to classify small samples.
|
|
body = _jsvmp_fixture(200)
|
|
default_detections = score_families(body, '', 's.js', 1500)
|
|
reasons = [r for d in default_detections if d['id'] == 'jsvmp_entry' for r in d['reasons']]
|
|
assert all('opcode literal' not in r for r in reasons), reasons
|
|
detections = score_families(body, '', 's.js', 200)
|
|
assert detections and detections[0]['id'] == 'jsvmp_entry', detections
|
|
assert any('opcode literal' in r for r in detections[0]['reasons'])
|
|
|
|
|
|
def test_hint_beats_a_single_content_hit():
|
|
# A human hint is worth 10; one content regex is worth 3. The routing consequence:
|
|
# a named vendor hint wins over incidental structural overlap.
|
|
detections = score_families('plain text', 'some obfuscator lineage', 'x.js', 1500)
|
|
assert detections[0]['id'] == 'ob_string_array'
|
|
assert detections[0]['score'] == 10
|
|
|
|
|
|
def test_plain_code_is_generic_ladder():
|
|
assert score_families('function add(a, b) { return a + b; }\n', '', 'x.js', 1500) == []
|
|
assert pipeline_for('generic')[0] == 'normalize' and pipeline_for('generic')[-1] == 'normalize'
|
|
|
|
|
|
def test_pipelines_differ_per_family():
|
|
# The whole point of detection: the string-array lineage decodes FIRST, the
|
|
# opcode-if-chain family goes straight to if-chain conversion.
|
|
assert pipeline_for('ob_string_array')[2] == 'string_array_pass'
|
|
assert 'if_chain_to_switch' in pipeline_for('opcode_if_chain')
|
|
assert pipeline_for('ob_string_array') != pipeline_for('opcode_if_chain')
|
|
|
|
|
|
def test_residue_metrics_are_textual_proxies():
|
|
r = residue_metrics(FIXTURES['ob_string_array'])
|
|
# The 3+ hex-char filter counts _0x4ab1 but not short names (_0x1, _0x9f).
|
|
assert r['mangled_identifiers'] == 1
|
|
assert r['eval_calls'] == 0
|
|
assert r['lines'] >= 3
|