twinflame 0.1.0b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- twinflame-0.1.0b1/PKG-INFO +274 -0
- twinflame-0.1.0b1/README.md +253 -0
- twinflame-0.1.0b1/pyproject.toml +52 -0
- twinflame-0.1.0b1/setup.cfg +4 -0
- twinflame-0.1.0b1/src/twinflame/__init__.py +46 -0
- twinflame-0.1.0b1/src/twinflame/_hot.py +41 -0
- twinflame-0.1.0b1/src/twinflame/accurate.py +409 -0
- twinflame-0.1.0b1/src/twinflame/anchor.py +146 -0
- twinflame-0.1.0b1/src/twinflame/api.py +232 -0
- twinflame-0.1.0b1/src/twinflame/boilerplate.py +39 -0
- twinflame-0.1.0b1/src/twinflame/changes.py +443 -0
- twinflame-0.1.0b1/src/twinflame/cli.py +288 -0
- twinflame-0.1.0b1/src/twinflame/cluster.py +73 -0
- twinflame-0.1.0b1/src/twinflame/components.py +91 -0
- twinflame-0.1.0b1/src/twinflame/deobf.py +331 -0
- twinflame-0.1.0b1/src/twinflame/features.py +333 -0
- twinflame-0.1.0b1/src/twinflame/loader.py +392 -0
- twinflame-0.1.0b1/src/twinflame/manifest.py +81 -0
- twinflame-0.1.0b1/src/twinflame/model.py +243 -0
- twinflame-0.1.0b1/src/twinflame/normalize.py +83 -0
- twinflame-0.1.0b1/src/twinflame/opcodes.py +156 -0
- twinflame-0.1.0b1/src/twinflame/parallel.py +28 -0
- twinflame-0.1.0b1/src/twinflame/prepare.py +260 -0
- twinflame-0.1.0b1/src/twinflame/propagate.py +265 -0
- twinflame-0.1.0b1/src/twinflame/provenance.py +119 -0
- twinflame-0.1.0b1/src/twinflame/report.py +68 -0
- twinflame-0.1.0b1/src/twinflame/signature.py +238 -0
- twinflame-0.1.0b1/src/twinflame.egg-info/PKG-INFO +274 -0
- twinflame-0.1.0b1/src/twinflame.egg-info/SOURCES.txt +31 -0
- twinflame-0.1.0b1/src/twinflame.egg-info/dependency_links.txt +1 -0
- twinflame-0.1.0b1/src/twinflame.egg-info/entry_points.txt +2 -0
- twinflame-0.1.0b1/src/twinflame.egg-info/requires.txt +8 -0
- twinflame-0.1.0b1/src/twinflame.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: twinflame
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: DEX-level Android APK class-diffing engine (SimHash + LSH + abstract-opcode comparison)
|
|
5
|
+
Author: twinflame contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ankorio/twinflame
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Topic :: Security
|
|
11
|
+
Classifier: Topic :: Software Development :: Disassemblers
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: androguard>=4.1
|
|
15
|
+
Requires-Dist: numpy>=1.26
|
|
16
|
+
Requires-Dist: rapidfuzz>=3.14
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
19
|
+
Requires-Dist: pytest-cov>=5; extra == "test"
|
|
20
|
+
Requires-Dist: hypothesis>=6; extra == "test"
|
|
21
|
+
|
|
22
|
+
# twinflame
|
|
23
|
+
|
|
24
|
+
DEX/APK **bytecode-level** diffing engine for Android reverse engineers. Matches classes and methods across two builds by *structure* — surviving R8/ProGuard renaming — and emits a ranked, method-localized **change report** (what was added / removed / modified) plus a ProGuard `mapping.txt` to carry recovered names into your decompiler. Implements the four-stage architecture from Quarkslab's _"Android Application Diffing: Engine Overview"_ (Czayka & Thomas, 2019).
|
|
25
|
+
|
|
26
|
+
Use cases:
|
|
27
|
+
|
|
28
|
+
- **Vulnerability patch identification** — find the class a vendor changed to fix a flaw.
|
|
29
|
+
- **Repackaging / mod analysis** — find injected code that replaces vendor logic with no-ops or hostile callbacks.
|
|
30
|
+
- **Malware triage / kinship** — find shared code across samples and flag when both implement the same permission-gated component (AccessibilityService, NotificationListener, DeviceAdmin).
|
|
31
|
+
- **Deobfuscation correlation** — pair classes across Proguard-renamed builds.
|
|
32
|
+
|
|
33
|
+
Inputs can be APKs, single `.dex` files, or directories of dumped `.dex` (memory dumps included). For pipelines that compare a sample against many, `twinflame prepare` fingerprints a sample once into a reusable record so later comparisons skip the expensive parse.
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
- **[docs/using-twinflame.md](docs/using-twinflame.md)** — task-oriented guide: the three use cases, reading the change report, feeding the mapping to jadx/retrace.
|
|
38
|
+
- **[docs/change-report-schema.md](docs/change-report-schema.md)** — the versioned JSON output format (for downstream tooling).
|
|
39
|
+
- **[docs/how-it-works.md](docs/how-it-works.md)** — how the pipeline works internally, with diagrams.
|
|
40
|
+
|
|
41
|
+
## Pipeline
|
|
42
|
+
|
|
43
|
+
1. **Multi-DEX merge** — every `classes*.dex` is unioned into one logical view (first-wins on descriptor, matching ART). The loader also records each method's call targets + incoming-xref count and each class's string constants, which feed anchoring.
|
|
44
|
+
2. **Stage 1 — clustering.** Classes group into pools keyed by package name; matching pools across the two APKs compare in parallel. Obfuscated packages (Shannon entropy + length heuristic) fall into a single fallback pool.
|
|
45
|
+
3. **Stage B — anchoring.** Within each pool, R8-invariant seeds are locked in *before* structural scoring: classes sharing a rare string constant (IDF-weighted) or a distinctive set of `android/*`/`androidx/*`/`java/*`/`kotlin/*` framework calls are paired with high confidence even when SimHash can't tell them apart. Disable with `--no-anchors`.
|
|
46
|
+
4. **Stage 2 — bulk SimHash + bucketed LSH.** Each remaining class gets a 128-bit signature built from four 32-bit partial hashes — `cls`, `fld`, `mth`, `code` — over **structure-only** features (no names, no strings). Nearest neighbors are found via Dullien-style bit-permutation LSH instead of all-pairs.
|
|
47
|
+
5. **Stage 3 — accurate, method-level comparison.** Top-k Stage-2 neighbors are re-scored at **method granularity**: methods are assigned 1-to-1 within a class pair (abstract-opcode Levenshtein over 13 semantic categories + prototype + xref), yielding per-method `matched / modified / added / deleted` verdicts. The class score is the instruction-weighted roll-up of those verdicts blended with structural features. Greedy 1-to-1 assignment resolves class-level ties.
|
|
48
|
+
|
|
49
|
+
Optional Redex pre-pass strips junk-instruction obfuscation (`LocalDcePass` + `RegAllocPass`) before loading.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### pip (recommended)
|
|
54
|
+
|
|
55
|
+
Requires Python ≥ 3.11. Runtime deps (androguard, numpy, rapidfuzz) install automatically.
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
git clone https://github.com/ankorio/twinflame && cd twinflame
|
|
59
|
+
python -m venv .venv && . .venv/bin/activate
|
|
60
|
+
pip install . # or: pip install -e '.[test]' for a dev checkout
|
|
61
|
+
twinflame --help # console entry point is installed
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That's everything for the core tool. Two features need external programs that aren't Python
|
|
65
|
+
packages: `--normalize` needs **Redex** on `PATH` (optional; only for junk-instruction
|
|
66
|
+
normalization), and applying a recovered `mapping.txt` needs your own decompiler (e.g. **JADX**).
|
|
67
|
+
|
|
68
|
+
### Nix flake (reproducible environment)
|
|
69
|
+
|
|
70
|
+
For a fully-pinned environment (Python + all deps + Redex built from source + JADX), use the
|
|
71
|
+
[Nix flake](https://nixos.wiki/wiki/Flakes) — everything is locked in [flake.lock](flake.lock):
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
# Install Nix with flakes (Determinate Systems installer is easiest):
|
|
75
|
+
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
|
|
76
|
+
git clone https://github.com/ankorio/twinflame && cd twinflame
|
|
77
|
+
nix develop # drops you in a shell with twinflame + Redex + JADX
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Running
|
|
81
|
+
|
|
82
|
+
### One-shot CLI via `nix run`
|
|
83
|
+
|
|
84
|
+
`nix run` builds the tool on demand and invokes it. The first run compiles Redex from source (~5 min); after that everything is cached.
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
# diff two APKs, scope to a vendor package, write a JSON report
|
|
88
|
+
nix run . -- old.apk new.apk \
|
|
89
|
+
--auto-package \
|
|
90
|
+
--threshold 0.8 \
|
|
91
|
+
-o report.json
|
|
92
|
+
|
|
93
|
+
# real-world example: vuln/patched pair, Redex-normalized to defeat
|
|
94
|
+
# junk-instruction obfuscation, with the obfuscated-package fallback
|
|
95
|
+
# so Proguard-renamed packages cluster together for comparison
|
|
96
|
+
nix run . -- vuln.apk patched.apk --normalize --find-obfuscated
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Inputs: APK, `.dex`, or dumped DEX (no APK)
|
|
100
|
+
|
|
101
|
+
Each side accepts an APK, a single `.dex`, or a directory of `.dex` files — so
|
|
102
|
+
you can diff **dumped/extracted DEX** (e.g. pulled from memory or an unpacked
|
|
103
|
+
payload) with no surrounding APK, with no flag: the input kind is detected.
|
|
104
|
+
Partial dumps are tolerated — an unparseable or corrupt `.dex` in a directory is
|
|
105
|
+
skipped (with a warning) rather than aborting the load. A DEX-only input has no
|
|
106
|
+
manifest, so `--auto-package` is unavailable — pass `--app-package <prefix>` for
|
|
107
|
+
app-vs-library ranking instead.
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
# directory of dumped classes*.dex on each side
|
|
111
|
+
twinflame dump_old/ dump_new/ --no-cluster
|
|
112
|
+
|
|
113
|
+
# write the machine report as CSV instead of the default JSON, only the
|
|
114
|
+
# classes that actually differ
|
|
115
|
+
twinflame a/ b/ --no-cluster --app-package com.target.app -s changed -f csv -o changes.csv
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
By default, stdout shows a ranked **change summary** (modified / added / removed /
|
|
119
|
+
cosmetic / unchanged, app code first) and any sensitive components both builds
|
|
120
|
+
share, while the full machine report is written to a file (`-f json`, default;
|
|
121
|
+
`csv`/`xml` also available). Pass `-m`/`--matches` for the raw per-class match
|
|
122
|
+
list instead:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
[+] com.acme.foo: Login - Login.java | com.acme.foo: Login - SourceFile -> 0.8523
|
|
126
|
+
~ validateToken (0.6100) # method modified
|
|
127
|
+
- legacyHash # method removed
|
|
128
|
+
+ verifyNonce # method added
|
|
129
|
+
[-] com.acme.foo: DeletedClass - DeletedClass.java # in lhs only
|
|
130
|
+
[*] com.acme.foo: AddedClass - SourceFile # in rhs only
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Each `[+]` paired line is followed by the methods that actually changed, so you
|
|
134
|
+
see *which* method moved the class score. The machine report carries the full
|
|
135
|
+
per-method verdict list, plus each paired class's superclass/interfaces and any
|
|
136
|
+
sensitive component it implements.
|
|
137
|
+
|
|
138
|
+
Timings go to stderr; the report to stdout (so you can `| less` or redirect freely).
|
|
139
|
+
|
|
140
|
+
#### Worked example
|
|
141
|
+
|
|
142
|
+
Diffing a vuln/patched APK pair (a real Android wallet app, ~8 k classes per side, the new build is Proguard'd):
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
$ nix run . -- old.apk new.apk --normalize --find-obfuscated
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Reading the output:
|
|
150
|
+
|
|
151
|
+
- The `load:` and `diff:` lines on **stderr** report timings and class counts so you can see what was actually compared after `--normalize` (Redex strips junk instructions on both sides) and post-filtering.
|
|
152
|
+
- `[+]` lines are paired matches with their similarity score (`distance < 1.0`). The two `androidx.activity` examples above pair clear-text class names on the left (lhs preserved source files) against the same logical classes on the right (rhs lost source files to `SourceFile` — Proguard's default). Source-file name was dropped, but the structure + bytecode similarity is high (~0.85–0.95).
|
|
153
|
+
- `[-]` is a class only present in lhs; `[*]` only in rhs. Counts at the top tell you the global picture (e.g. _11 042 matches: 602 paired below 1.0, 5 027 deleted, 5 664 added_ — the bulk being unmatched is the price of heavy obfuscation; tweak `--find-obfuscated` and `--threshold` to trade quality for coverage).
|
|
154
|
+
- Drop the `--normalize` and re-run to see what _uncleaned_ bytecode looks like — if the average paired similarity drops from ~0.9 to ~0.95+ everywhere, that's the fingerprint of junk-instruction obfuscation that Redex defeats.
|
|
155
|
+
|
|
156
|
+
### Build a standalone binary
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
nix build # produces ./result/bin/twinflame
|
|
160
|
+
./result/bin/twinflame --help
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Dev shell
|
|
164
|
+
|
|
165
|
+
For interactive use (REPL, hacking on the code, running tests):
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
nix develop # enter shell with python + redex + jadx on PATH
|
|
169
|
+
pytest tests/ # 86 tests, ~8s
|
|
170
|
+
python -m twinflame.cli --help
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Python API
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from twinflame import load, filter, diff
|
|
177
|
+
|
|
178
|
+
lhs_app = load("app-1.6.1.apk")
|
|
179
|
+
rhs_app = load("app-1.6.3.apk")
|
|
180
|
+
|
|
181
|
+
lhs = filter(lhs_app.classes, {"package_filtering": "com.vendor.app"})
|
|
182
|
+
rhs = filter(rhs_app.classes, {"package_filtering": "com.vendor.app"})
|
|
183
|
+
|
|
184
|
+
matches = diff(lhs, rhs, 0.8, {
|
|
185
|
+
"synthetic_skipping": True,
|
|
186
|
+
"min_inst_size_threshold": 5,
|
|
187
|
+
"top_match_threshold": 3,
|
|
188
|
+
})
|
|
189
|
+
for m in matches:
|
|
190
|
+
if m.is_paired and m.distance < 1.0:
|
|
191
|
+
print(f"[+] {m.lhs.info} | {m.rhs.info} -> {m.distance:.4f}")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Cross-version deobfuscation map
|
|
195
|
+
|
|
196
|
+
`--deobfuscation-map OUT` writes a ProGuard-format `mapping.txt` for **apk2** (the obfuscated/stripped build) by propagating class names recovered from the matched classes in **apk1** (the donor build that still carries the DEX `SourceFile` attribute). Load it into JADX/Ghidra and the rename propagates to every reference automatically — twinflame never rewrites the DEX.
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
# apk1 = older build that kept SourceFile; apk2 = the one you want to read
|
|
200
|
+
twinflame app-old.apk app-new.apk \
|
|
201
|
+
--find-obfuscated --deobfuscation-map app-new.map --map-min-confidence 0.8
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Load into JADX with `-Prename-mappings.invert=yes`:
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
jadx --mappings-path app-new.map -Prename-mappings.format=PROGUARD_FILE -Prename-mappings.invert=yes \
|
|
208
|
+
-d out_deobf app-new.apk
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`invert=yes` is required: our file follows the standard ProGuard convention (`<original> -> <obfuscated>:`, what `retrace` consumes), but jadx's `PROGUARD_FILE` reader treats the left side as the name *currently in the binary* — the opposite. Without the flag it silently renames nothing (no error). Same applies in jadx-gui: check "Invert" alongside the mapping path.
|
|
212
|
+
|
|
213
|
+
- Recovers the class **simple name** (`ContextCompat.java` → `x6.q` becomes `x6.ContextCompat`); the obfuscated **package** and inner-class structure are kept (source files carry no package).
|
|
214
|
+
- Anchored / exact (`distance == 1.0`) matches are trusted; lower-confidence ones are still emitted but flagged with a `# low-confidence` comment. Tune the floor with `--map-min-confidence`.
|
|
215
|
+
- This is the cross-version superpower over single-APK source-file deobfuscation: it names classes in a build that **stripped** `SourceFile`, using the build that didn't. Method/field name propagation is a planned next step.
|
|
216
|
+
|
|
217
|
+
### Workflow recipes
|
|
218
|
+
|
|
219
|
+
- **Patch identification.** Scope with `--package PREFIX` or `--auto-package` (drastically shrinks the comparison set), use `--threshold 0.8` since bug fixes are minor changes. Skip `distance == 1.0` matches.
|
|
220
|
+
- **Modded / repackaged app.** Drop `--package` (mods often live in bundled SDKs, not the vendor package). If you see a sea of ~0.95 "everything changed" matches, run with `--normalize` — that's the signature of junk-instruction obfuscation.
|
|
221
|
+
- **Heavily Proguard'd inputs.** Add `--find-obfuscated` so single-letter package names (`a.b.c`) collapse into a fallback pool rather than failing to pair with their clear-text counterparts.
|
|
222
|
+
- **Deobfuscating a stripped build.** Diff it against an older build that kept `SourceFile`, add `--find-obfuscated --deobfuscation-map out.map`, then load `out.map` in JADX.
|
|
223
|
+
|
|
224
|
+
### Evaluation harness (`eval/`, plan M3.2)
|
|
225
|
+
|
|
226
|
+
Grades twinflame's own class matches against real ground truth instead of eyeballing them. Build an OSS Android app **twice** — R8 off, then R8 on at full optimization — the `mapping.txt` R8 writes for the R8-on build is a free, perfect oracle for the renaming-only case (directly grades M1.1 + M1.2; inlining/outlining is a future difficulty-graded slice per M3.1).
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
python -m eval.cli app-r8-off.apk app-r8-on.apk app-r8-on/mapping.txt --package com.vendor.app
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Prints true/false positive & negative class-match counts plus precision/recall/F1. `eval/mapping.py` parses the ProGuard-format oracle (same format `deobf.py` emits); `eval/score.py` exposes `score_class_matches(matches, ground_truth) -> ScoreResult` for use as a library, independent of the CLI. Kept out of the `src/twinflame` package on purpose — it's tooling to grade the engine, not part of it.
|
|
233
|
+
|
|
234
|
+
## Layout
|
|
235
|
+
|
|
236
|
+
| Module | Role |
|
|
237
|
+
| ---------------------------------------------------- | -------------------------------------------------------------------- |
|
|
238
|
+
| [src/twinflame/model.py](src/twinflame/model.py) | Dataclasses: `App`, `Class`, `Method`, `Field`, `Signature`, `Match`, `MethodMatch` |
|
|
239
|
+
| [src/twinflame/loader.py](src/twinflame/loader.py) | androguard wrapping + multi-DEX merge; captures calls/strings/xrefs |
|
|
240
|
+
| [src/twinflame/manifest.py](src/twinflame/manifest.py) | `AndroidManifest.xml` → `ManifestInfo`; dev-package suggestion |
|
|
241
|
+
| [src/twinflame/normalize.py](src/twinflame/normalize.py) | Redex subprocess wrapper |
|
|
242
|
+
| [src/twinflame/cluster.py](src/twinflame/cluster.py) | Package-based pools + entropy/length obfuscation fallback |
|
|
243
|
+
| [src/twinflame/anchor.py](src/twinflame/anchor.py) | Stage B: string-IDF + framework-call seed matches |
|
|
244
|
+
| [src/twinflame/signature.py](src/twinflame/signature.py) | 128-bit SimHash + LSH bucket index |
|
|
245
|
+
| [src/twinflame/accurate.py](src/twinflame/accurate.py) | Abstract opcodes, method-level + class scoring, greedy 1-to-1 assignment |
|
|
246
|
+
| [src/twinflame/opcodes.py](src/twinflame/opcodes.py) | Dalvik opcode → 13-category lookup table |
|
|
247
|
+
| [src/twinflame/\_hot.py](src/twinflame/_hot.py) | Hot loops (popcount, Hamming, rapidfuzz-backed Levenshtein) |
|
|
248
|
+
| [src/twinflame/deobf.py](src/twinflame/deobf.py) | Cross-version deobfuscation: recovered names → ProGuard mapping.txt |
|
|
249
|
+
| [src/twinflame/api.py](src/twinflame/api.py) | Public `load / filter / diff` entry points |
|
|
250
|
+
| [src/twinflame/cli.py](src/twinflame/cli.py) | `twinflame` console script |
|
|
251
|
+
|
|
252
|
+
## Tests
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
nix develop --command pytest tests/
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Currently: **86 tests passing**, 0 skipped. Redex is built and on PATH inside the dev shell.
|
|
259
|
+
|
|
260
|
+
Test fixtures are **pure-synthetic**: `tests/fixtures/synthetic.py` builds `Class` objects directly via Python, no binaries committed to git. The integration tests exercise the full diff pipeline (cluster → anchor → signature → accurate → api) on these objects.
|
|
261
|
+
|
|
262
|
+
The on-disk DEX/APK round-trip (loader.py end-to-end) currently has a smoke-only test; a real binary DEX emitter under [tests/fixtures/build_dex.py](tests/fixtures/build_dex.py) is stubbed for future work.
|
|
263
|
+
|
|
264
|
+
## Known limitations
|
|
265
|
+
|
|
266
|
+
- **Identical-structure classes collide.** Pools where many classes share signatures (trivial getter/setter classes, generated stubs) may produce false pairings via greedy assignment. `--min-instr 5` filters trivial classes; raising `--neighbors` widens the candidate pool.
|
|
267
|
+
- **No inheritance context.** Matching is per-class (now with method-level detail *inside* a paired class), but ignores first-level parent/child signals (cf. LibPecker); these can be added later without disturbing the core.
|
|
268
|
+
- **No cross-boundary / optimization resilience yet.** Matching assumes a 1:1 class and method correspondence. R8 *optimizations* that break that assumption — inlining, outlining, class merging — are a planned future track (see `twinflame-next-dev-plan.md`, M3.1), not yet implemented.
|
|
269
|
+
- **JNI / native-code changes are invisible.** The diff is purely Dalvik-level — native library mutations require a complementary native-code diff.
|
|
270
|
+
- **LSH is approximate by design.** `--buckets N` tunes the accuracy/speed knob.
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# twinflame
|
|
2
|
+
|
|
3
|
+
DEX/APK **bytecode-level** diffing engine for Android reverse engineers. Matches classes and methods across two builds by *structure* — surviving R8/ProGuard renaming — and emits a ranked, method-localized **change report** (what was added / removed / modified) plus a ProGuard `mapping.txt` to carry recovered names into your decompiler. Implements the four-stage architecture from Quarkslab's _"Android Application Diffing: Engine Overview"_ (Czayka & Thomas, 2019).
|
|
4
|
+
|
|
5
|
+
Use cases:
|
|
6
|
+
|
|
7
|
+
- **Vulnerability patch identification** — find the class a vendor changed to fix a flaw.
|
|
8
|
+
- **Repackaging / mod analysis** — find injected code that replaces vendor logic with no-ops or hostile callbacks.
|
|
9
|
+
- **Malware triage / kinship** — find shared code across samples and flag when both implement the same permission-gated component (AccessibilityService, NotificationListener, DeviceAdmin).
|
|
10
|
+
- **Deobfuscation correlation** — pair classes across Proguard-renamed builds.
|
|
11
|
+
|
|
12
|
+
Inputs can be APKs, single `.dex` files, or directories of dumped `.dex` (memory dumps included). For pipelines that compare a sample against many, `twinflame prepare` fingerprints a sample once into a reusable record so later comparisons skip the expensive parse.
|
|
13
|
+
|
|
14
|
+
## Documentation
|
|
15
|
+
|
|
16
|
+
- **[docs/using-twinflame.md](docs/using-twinflame.md)** — task-oriented guide: the three use cases, reading the change report, feeding the mapping to jadx/retrace.
|
|
17
|
+
- **[docs/change-report-schema.md](docs/change-report-schema.md)** — the versioned JSON output format (for downstream tooling).
|
|
18
|
+
- **[docs/how-it-works.md](docs/how-it-works.md)** — how the pipeline works internally, with diagrams.
|
|
19
|
+
|
|
20
|
+
## Pipeline
|
|
21
|
+
|
|
22
|
+
1. **Multi-DEX merge** — every `classes*.dex` is unioned into one logical view (first-wins on descriptor, matching ART). The loader also records each method's call targets + incoming-xref count and each class's string constants, which feed anchoring.
|
|
23
|
+
2. **Stage 1 — clustering.** Classes group into pools keyed by package name; matching pools across the two APKs compare in parallel. Obfuscated packages (Shannon entropy + length heuristic) fall into a single fallback pool.
|
|
24
|
+
3. **Stage B — anchoring.** Within each pool, R8-invariant seeds are locked in *before* structural scoring: classes sharing a rare string constant (IDF-weighted) or a distinctive set of `android/*`/`androidx/*`/`java/*`/`kotlin/*` framework calls are paired with high confidence even when SimHash can't tell them apart. Disable with `--no-anchors`.
|
|
25
|
+
4. **Stage 2 — bulk SimHash + bucketed LSH.** Each remaining class gets a 128-bit signature built from four 32-bit partial hashes — `cls`, `fld`, `mth`, `code` — over **structure-only** features (no names, no strings). Nearest neighbors are found via Dullien-style bit-permutation LSH instead of all-pairs.
|
|
26
|
+
5. **Stage 3 — accurate, method-level comparison.** Top-k Stage-2 neighbors are re-scored at **method granularity**: methods are assigned 1-to-1 within a class pair (abstract-opcode Levenshtein over 13 semantic categories + prototype + xref), yielding per-method `matched / modified / added / deleted` verdicts. The class score is the instruction-weighted roll-up of those verdicts blended with structural features. Greedy 1-to-1 assignment resolves class-level ties.
|
|
27
|
+
|
|
28
|
+
Optional Redex pre-pass strips junk-instruction obfuscation (`LocalDcePass` + `RegAllocPass`) before loading.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
### pip (recommended)
|
|
33
|
+
|
|
34
|
+
Requires Python ≥ 3.11. Runtime deps (androguard, numpy, rapidfuzz) install automatically.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
git clone https://github.com/ankorio/twinflame && cd twinflame
|
|
38
|
+
python -m venv .venv && . .venv/bin/activate
|
|
39
|
+
pip install . # or: pip install -e '.[test]' for a dev checkout
|
|
40
|
+
twinflame --help # console entry point is installed
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's everything for the core tool. Two features need external programs that aren't Python
|
|
44
|
+
packages: `--normalize` needs **Redex** on `PATH` (optional; only for junk-instruction
|
|
45
|
+
normalization), and applying a recovered `mapping.txt` needs your own decompiler (e.g. **JADX**).
|
|
46
|
+
|
|
47
|
+
### Nix flake (reproducible environment)
|
|
48
|
+
|
|
49
|
+
For a fully-pinned environment (Python + all deps + Redex built from source + JADX), use the
|
|
50
|
+
[Nix flake](https://nixos.wiki/wiki/Flakes) — everything is locked in [flake.lock](flake.lock):
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
# Install Nix with flakes (Determinate Systems installer is easiest):
|
|
54
|
+
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
|
|
55
|
+
git clone https://github.com/ankorio/twinflame && cd twinflame
|
|
56
|
+
nix develop # drops you in a shell with twinflame + Redex + JADX
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Running
|
|
60
|
+
|
|
61
|
+
### One-shot CLI via `nix run`
|
|
62
|
+
|
|
63
|
+
`nix run` builds the tool on demand and invokes it. The first run compiles Redex from source (~5 min); after that everything is cached.
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
# diff two APKs, scope to a vendor package, write a JSON report
|
|
67
|
+
nix run . -- old.apk new.apk \
|
|
68
|
+
--auto-package \
|
|
69
|
+
--threshold 0.8 \
|
|
70
|
+
-o report.json
|
|
71
|
+
|
|
72
|
+
# real-world example: vuln/patched pair, Redex-normalized to defeat
|
|
73
|
+
# junk-instruction obfuscation, with the obfuscated-package fallback
|
|
74
|
+
# so Proguard-renamed packages cluster together for comparison
|
|
75
|
+
nix run . -- vuln.apk patched.apk --normalize --find-obfuscated
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Inputs: APK, `.dex`, or dumped DEX (no APK)
|
|
79
|
+
|
|
80
|
+
Each side accepts an APK, a single `.dex`, or a directory of `.dex` files — so
|
|
81
|
+
you can diff **dumped/extracted DEX** (e.g. pulled from memory or an unpacked
|
|
82
|
+
payload) with no surrounding APK, with no flag: the input kind is detected.
|
|
83
|
+
Partial dumps are tolerated — an unparseable or corrupt `.dex` in a directory is
|
|
84
|
+
skipped (with a warning) rather than aborting the load. A DEX-only input has no
|
|
85
|
+
manifest, so `--auto-package` is unavailable — pass `--app-package <prefix>` for
|
|
86
|
+
app-vs-library ranking instead.
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
# directory of dumped classes*.dex on each side
|
|
90
|
+
twinflame dump_old/ dump_new/ --no-cluster
|
|
91
|
+
|
|
92
|
+
# write the machine report as CSV instead of the default JSON, only the
|
|
93
|
+
# classes that actually differ
|
|
94
|
+
twinflame a/ b/ --no-cluster --app-package com.target.app -s changed -f csv -o changes.csv
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
By default, stdout shows a ranked **change summary** (modified / added / removed /
|
|
98
|
+
cosmetic / unchanged, app code first) and any sensitive components both builds
|
|
99
|
+
share, while the full machine report is written to a file (`-f json`, default;
|
|
100
|
+
`csv`/`xml` also available). Pass `-m`/`--matches` for the raw per-class match
|
|
101
|
+
list instead:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
[+] com.acme.foo: Login - Login.java | com.acme.foo: Login - SourceFile -> 0.8523
|
|
105
|
+
~ validateToken (0.6100) # method modified
|
|
106
|
+
- legacyHash # method removed
|
|
107
|
+
+ verifyNonce # method added
|
|
108
|
+
[-] com.acme.foo: DeletedClass - DeletedClass.java # in lhs only
|
|
109
|
+
[*] com.acme.foo: AddedClass - SourceFile # in rhs only
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each `[+]` paired line is followed by the methods that actually changed, so you
|
|
113
|
+
see *which* method moved the class score. The machine report carries the full
|
|
114
|
+
per-method verdict list, plus each paired class's superclass/interfaces and any
|
|
115
|
+
sensitive component it implements.
|
|
116
|
+
|
|
117
|
+
Timings go to stderr; the report to stdout (so you can `| less` or redirect freely).
|
|
118
|
+
|
|
119
|
+
#### Worked example
|
|
120
|
+
|
|
121
|
+
Diffing a vuln/patched APK pair (a real Android wallet app, ~8 k classes per side, the new build is Proguard'd):
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
$ nix run . -- old.apk new.apk --normalize --find-obfuscated
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Reading the output:
|
|
129
|
+
|
|
130
|
+
- The `load:` and `diff:` lines on **stderr** report timings and class counts so you can see what was actually compared after `--normalize` (Redex strips junk instructions on both sides) and post-filtering.
|
|
131
|
+
- `[+]` lines are paired matches with their similarity score (`distance < 1.0`). The two `androidx.activity` examples above pair clear-text class names on the left (lhs preserved source files) against the same logical classes on the right (rhs lost source files to `SourceFile` — Proguard's default). Source-file name was dropped, but the structure + bytecode similarity is high (~0.85–0.95).
|
|
132
|
+
- `[-]` is a class only present in lhs; `[*]` only in rhs. Counts at the top tell you the global picture (e.g. _11 042 matches: 602 paired below 1.0, 5 027 deleted, 5 664 added_ — the bulk being unmatched is the price of heavy obfuscation; tweak `--find-obfuscated` and `--threshold` to trade quality for coverage).
|
|
133
|
+
- Drop the `--normalize` and re-run to see what _uncleaned_ bytecode looks like — if the average paired similarity drops from ~0.9 to ~0.95+ everywhere, that's the fingerprint of junk-instruction obfuscation that Redex defeats.
|
|
134
|
+
|
|
135
|
+
### Build a standalone binary
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
nix build # produces ./result/bin/twinflame
|
|
139
|
+
./result/bin/twinflame --help
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Dev shell
|
|
143
|
+
|
|
144
|
+
For interactive use (REPL, hacking on the code, running tests):
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
nix develop # enter shell with python + redex + jadx on PATH
|
|
148
|
+
pytest tests/ # 86 tests, ~8s
|
|
149
|
+
python -m twinflame.cli --help
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Python API
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from twinflame import load, filter, diff
|
|
156
|
+
|
|
157
|
+
lhs_app = load("app-1.6.1.apk")
|
|
158
|
+
rhs_app = load("app-1.6.3.apk")
|
|
159
|
+
|
|
160
|
+
lhs = filter(lhs_app.classes, {"package_filtering": "com.vendor.app"})
|
|
161
|
+
rhs = filter(rhs_app.classes, {"package_filtering": "com.vendor.app"})
|
|
162
|
+
|
|
163
|
+
matches = diff(lhs, rhs, 0.8, {
|
|
164
|
+
"synthetic_skipping": True,
|
|
165
|
+
"min_inst_size_threshold": 5,
|
|
166
|
+
"top_match_threshold": 3,
|
|
167
|
+
})
|
|
168
|
+
for m in matches:
|
|
169
|
+
if m.is_paired and m.distance < 1.0:
|
|
170
|
+
print(f"[+] {m.lhs.info} | {m.rhs.info} -> {m.distance:.4f}")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Cross-version deobfuscation map
|
|
174
|
+
|
|
175
|
+
`--deobfuscation-map OUT` writes a ProGuard-format `mapping.txt` for **apk2** (the obfuscated/stripped build) by propagating class names recovered from the matched classes in **apk1** (the donor build that still carries the DEX `SourceFile` attribute). Load it into JADX/Ghidra and the rename propagates to every reference automatically — twinflame never rewrites the DEX.
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
# apk1 = older build that kept SourceFile; apk2 = the one you want to read
|
|
179
|
+
twinflame app-old.apk app-new.apk \
|
|
180
|
+
--find-obfuscated --deobfuscation-map app-new.map --map-min-confidence 0.8
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Load into JADX with `-Prename-mappings.invert=yes`:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
jadx --mappings-path app-new.map -Prename-mappings.format=PROGUARD_FILE -Prename-mappings.invert=yes \
|
|
187
|
+
-d out_deobf app-new.apk
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`invert=yes` is required: our file follows the standard ProGuard convention (`<original> -> <obfuscated>:`, what `retrace` consumes), but jadx's `PROGUARD_FILE` reader treats the left side as the name *currently in the binary* — the opposite. Without the flag it silently renames nothing (no error). Same applies in jadx-gui: check "Invert" alongside the mapping path.
|
|
191
|
+
|
|
192
|
+
- Recovers the class **simple name** (`ContextCompat.java` → `x6.q` becomes `x6.ContextCompat`); the obfuscated **package** and inner-class structure are kept (source files carry no package).
|
|
193
|
+
- Anchored / exact (`distance == 1.0`) matches are trusted; lower-confidence ones are still emitted but flagged with a `# low-confidence` comment. Tune the floor with `--map-min-confidence`.
|
|
194
|
+
- This is the cross-version superpower over single-APK source-file deobfuscation: it names classes in a build that **stripped** `SourceFile`, using the build that didn't. Method/field name propagation is a planned next step.
|
|
195
|
+
|
|
196
|
+
### Workflow recipes
|
|
197
|
+
|
|
198
|
+
- **Patch identification.** Scope with `--package PREFIX` or `--auto-package` (drastically shrinks the comparison set), use `--threshold 0.8` since bug fixes are minor changes. Skip `distance == 1.0` matches.
|
|
199
|
+
- **Modded / repackaged app.** Drop `--package` (mods often live in bundled SDKs, not the vendor package). If you see a sea of ~0.95 "everything changed" matches, run with `--normalize` — that's the signature of junk-instruction obfuscation.
|
|
200
|
+
- **Heavily Proguard'd inputs.** Add `--find-obfuscated` so single-letter package names (`a.b.c`) collapse into a fallback pool rather than failing to pair with their clear-text counterparts.
|
|
201
|
+
- **Deobfuscating a stripped build.** Diff it against an older build that kept `SourceFile`, add `--find-obfuscated --deobfuscation-map out.map`, then load `out.map` in JADX.
|
|
202
|
+
|
|
203
|
+
### Evaluation harness (`eval/`, plan M3.2)
|
|
204
|
+
|
|
205
|
+
Grades twinflame's own class matches against real ground truth instead of eyeballing them. Build an OSS Android app **twice** — R8 off, then R8 on at full optimization — the `mapping.txt` R8 writes for the R8-on build is a free, perfect oracle for the renaming-only case (directly grades M1.1 + M1.2; inlining/outlining is a future difficulty-graded slice per M3.1).
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
python -m eval.cli app-r8-off.apk app-r8-on.apk app-r8-on/mapping.txt --package com.vendor.app
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Prints true/false positive & negative class-match counts plus precision/recall/F1. `eval/mapping.py` parses the ProGuard-format oracle (same format `deobf.py` emits); `eval/score.py` exposes `score_class_matches(matches, ground_truth) -> ScoreResult` for use as a library, independent of the CLI. Kept out of the `src/twinflame` package on purpose — it's tooling to grade the engine, not part of it.
|
|
212
|
+
|
|
213
|
+
## Layout
|
|
214
|
+
|
|
215
|
+
| Module | Role |
|
|
216
|
+
| ---------------------------------------------------- | -------------------------------------------------------------------- |
|
|
217
|
+
| [src/twinflame/model.py](src/twinflame/model.py) | Dataclasses: `App`, `Class`, `Method`, `Field`, `Signature`, `Match`, `MethodMatch` |
|
|
218
|
+
| [src/twinflame/loader.py](src/twinflame/loader.py) | androguard wrapping + multi-DEX merge; captures calls/strings/xrefs |
|
|
219
|
+
| [src/twinflame/manifest.py](src/twinflame/manifest.py) | `AndroidManifest.xml` → `ManifestInfo`; dev-package suggestion |
|
|
220
|
+
| [src/twinflame/normalize.py](src/twinflame/normalize.py) | Redex subprocess wrapper |
|
|
221
|
+
| [src/twinflame/cluster.py](src/twinflame/cluster.py) | Package-based pools + entropy/length obfuscation fallback |
|
|
222
|
+
| [src/twinflame/anchor.py](src/twinflame/anchor.py) | Stage B: string-IDF + framework-call seed matches |
|
|
223
|
+
| [src/twinflame/signature.py](src/twinflame/signature.py) | 128-bit SimHash + LSH bucket index |
|
|
224
|
+
| [src/twinflame/accurate.py](src/twinflame/accurate.py) | Abstract opcodes, method-level + class scoring, greedy 1-to-1 assignment |
|
|
225
|
+
| [src/twinflame/opcodes.py](src/twinflame/opcodes.py) | Dalvik opcode → 13-category lookup table |
|
|
226
|
+
| [src/twinflame/\_hot.py](src/twinflame/_hot.py) | Hot loops (popcount, Hamming, rapidfuzz-backed Levenshtein) |
|
|
227
|
+
| [src/twinflame/deobf.py](src/twinflame/deobf.py) | Cross-version deobfuscation: recovered names → ProGuard mapping.txt |
|
|
228
|
+
| [src/twinflame/api.py](src/twinflame/api.py) | Public `load / filter / diff` entry points |
|
|
229
|
+
| [src/twinflame/cli.py](src/twinflame/cli.py) | `twinflame` console script |
|
|
230
|
+
|
|
231
|
+
## Tests
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
nix develop --command pytest tests/
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Currently: **86 tests passing**, 0 skipped. Redex is built and on PATH inside the dev shell.
|
|
238
|
+
|
|
239
|
+
Test fixtures are **pure-synthetic**: `tests/fixtures/synthetic.py` builds `Class` objects directly via Python, no binaries committed to git. The integration tests exercise the full diff pipeline (cluster → anchor → signature → accurate → api) on these objects.
|
|
240
|
+
|
|
241
|
+
The on-disk DEX/APK round-trip (loader.py end-to-end) currently has a smoke-only test; a real binary DEX emitter under [tests/fixtures/build_dex.py](tests/fixtures/build_dex.py) is stubbed for future work.
|
|
242
|
+
|
|
243
|
+
## Known limitations
|
|
244
|
+
|
|
245
|
+
- **Identical-structure classes collide.** Pools where many classes share signatures (trivial getter/setter classes, generated stubs) may produce false pairings via greedy assignment. `--min-instr 5` filters trivial classes; raising `--neighbors` widens the candidate pool.
|
|
246
|
+
- **No inheritance context.** Matching is per-class (now with method-level detail *inside* a paired class), but ignores first-level parent/child signals (cf. LibPecker); these can be added later without disturbing the core.
|
|
247
|
+
- **No cross-boundary / optimization resilience yet.** Matching assumes a 1:1 class and method correspondence. R8 *optimizations* that break that assumption — inlining, outlining, class merging — are a planned future track (see `twinflame-next-dev-plan.md`, M3.1), not yet implemented.
|
|
248
|
+
- **JNI / native-code changes are invisible.** The diff is purely Dalvik-level — native library mutations require a complementary native-code diff.
|
|
249
|
+
- **LSH is approximate by design.** `--buckets N` tunes the accuracy/speed knob.
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
MIT.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "twinflame"
|
|
7
|
+
version = "0.1.0b1"
|
|
8
|
+
description = "DEX-level Android APK class-diffing engine (SimHash + LSH + abstract-opcode comparison)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "twinflame contributors" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Topic :: Security",
|
|
17
|
+
"Topic :: Software Development :: Disassemblers",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"androguard>=4.1",
|
|
21
|
+
"numpy>=1.26",
|
|
22
|
+
"rapidfuzz>=3.14",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = [
|
|
27
|
+
"pytest>=8",
|
|
28
|
+
"pytest-cov>=5",
|
|
29
|
+
"hypothesis>=6",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
twinflame = "twinflame.cli:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/ankorio/twinflame"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
package-dir = { "" = "src" }
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
twinflame = ["py.typed"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
addopts = "-ra"
|
|
50
|
+
filterwarnings = [
|
|
51
|
+
"ignore::DeprecationWarning:androguard.*",
|
|
52
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""twinflame — DEX-level Android APK class-diffing engine."""
|
|
2
|
+
|
|
3
|
+
from .api import diff, filter, load
|
|
4
|
+
from .model import (
|
|
5
|
+
App,
|
|
6
|
+
Class,
|
|
7
|
+
DiffOptions,
|
|
8
|
+
Field,
|
|
9
|
+
Match,
|
|
10
|
+
Method,
|
|
11
|
+
MethodMatch,
|
|
12
|
+
Pool,
|
|
13
|
+
Signature,
|
|
14
|
+
)
|
|
15
|
+
from .prepare import PreparedRecord, load_record, prepare, save
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"App",
|
|
19
|
+
"Class",
|
|
20
|
+
"DiffOptions",
|
|
21
|
+
"Field",
|
|
22
|
+
"Match",
|
|
23
|
+
"Method",
|
|
24
|
+
"MethodMatch",
|
|
25
|
+
"Pool",
|
|
26
|
+
"PreparedRecord",
|
|
27
|
+
"Signature",
|
|
28
|
+
"diff",
|
|
29
|
+
"filter",
|
|
30
|
+
"load",
|
|
31
|
+
"load_record",
|
|
32
|
+
"prepare",
|
|
33
|
+
"save",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Single source of truth is the [project].version in pyproject.toml; read it
|
|
37
|
+
# back from the installed package metadata so the two never drift. Falls back
|
|
38
|
+
# when running from an uninstalled source tree.
|
|
39
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version # noqa: E402
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
__version__ = _pkg_version("twinflame")
|
|
43
|
+
except PackageNotFoundError: # source checkout without an install
|
|
44
|
+
__version__ = "0.0.0+unknown"
|
|
45
|
+
|
|
46
|
+
del _pkg_version, PackageNotFoundError
|