Deobfuscation strategy
Deobfuscation is not one operation. Name minification, optimization, string encryption, reflection, control-flow transformation, and packing remove different kinds of information and need different evidence.
Identify what changed
| Symptom | Likely transformation | Response |
|---|---|---|
classes and methods named a, b, c | ProGuard/R8 name minification | use mapping if owned; otherwise infer roles structurally |
| methods apparently missing | optimization/inlining | inspect call sites and smali; compare the unoptimized build if available |
| unreadable constants wrapped by one method | string encryption | isolate/reimplement the decoder or observe authorized runtime returns |
| large switch dispatchers and opaque branches | control-flow obfuscation | compare engines; simplify cautiously; read smali |
| reflection around reconstructed names | reflection obfuscation | trace string construction and reflective calls |
| tiny loader DEX, real classes absent | packing/virtualization | move to loader/native/runtime analysis |
APKiD can identify known signatures, but manual structure still matters.
Use an owned mapping.txt
A mapping generated by the same release build is the strongest source of original names:
com.example.network.ApiClient -> a.b:
java.lang.String baseUrl -> a
42:57:void execute() -> b
Use Android's R8 retrace for obfuscated stack traces:
retrace mapping.txt obfuscated-stacktrace.txt
Important constraints:
- the mapping must match the exact build/version;
- retrace is primarily for stack traces, not wholesale source-tree rewriting;
- line mappings can be absent or altered by optimization;
- never seek or use a third party's private mapping without authorization.
For an owned decompiled tree, custom renaming can improve navigation. Parse exact class/member mappings, rename classes before members, and keep a reversible map. Blind text replacement can corrupt unrelated short names.
Work without a mapping
Recover semantic labels, not imaginary originals.
- Anchor classes named in the manifest.
- Use framework superclasses and implemented interfaces.
- Search constants: URLs, preference keys, intent actions, log tags, database tables.
- Follow call sites and data types.
- Rename in notes or a working copy (
a.b→probable NetworkClient) with confidence markers.
A useful naming ledger:
| Obfuscated symbol | Working label | Evidence | Confidence |
|---|---|---|---|
a.c.e | CheckoutActivity | manifest + extends Activity + layout strings | high |
x.y.a() | decryptString | called around constants; returns UTF-8 strings | medium |
Do not present working labels as recovered source names.
Handle encrypted strings
Find a repeated static method around opaque constants, especially in static initializers. Then choose the least invasive route:
Reimplement the routine
When the routine is self-contained, translate it into a small script, validate against one known value, and batch only the constants relevant to the task.
Record:
- encoding and byte order;
- key derivation and per-call parameters;
- integer overflow/rotation semantics;
- expected output charset;
- validation cases.
Observe authorized runtime values
If keys come from device state or native code, instrument the decoder's return value in a controlled lab. This is narrower than dumping every class and often answers the question faster. Follow the dynamic fallback authorization boundary.
Runtime strings can contain endpoints, tokens, user data, or anti-abuse signals. Minimize collection and redact reports.
Approach control-flow transformations
Before applying an automatic simplifier:
- preserve the original DEX and its hash;
- isolate one problematic method;
- compare jadx with CFR/Procyon or another engine;
- inspect the original smali;
- run the transformed output in a separate path;
- compare semantics, not only readability.
Tools such as simplify can symbolically execute methods and remove dead branches, but unsupported instructions, reflection, native calls, and environment-dependent predicates can limit or alter results. Never replace original evidence with transformed output.
Recognize the stopping point
Some information no longer exists after compilation and optimization: comments, exact formatting, many local variable names, generic abstractions, and inlined boundaries. A responsible result may be:
The branch can be explained at bytecode level, but the original method boundary and identifier are not recoverable from this build.
That is better than a confident but fabricated reconstruction.