R8 / ProGuard¶
R8 and ProGuard are code transformation tools that ship with the Android build toolchain. They are technically obfuscators, not packers -- they do not encrypt DEX files, wrap native loaders, or perform any runtime self-protection. However, they are the most commonly encountered code transformation in Android reverse engineering, and understanding their output is a prerequisite for analyzing virtually any production Android application.
Overview¶
| Attribute | ProGuard | R8 |
|---|---|---|
| Developer | Guardsquare (Eric Lafortune) | |
| Type | Open-source obfuscator/optimizer | Built into Android Gradle Plugin |
| Status | Legacy (still functional) | Default since AGP 3.4.0 (2019) |
| Rule format | -keep rules in proguard-rules.pro |
Same rule format as ProGuard |
| Implementation | Separate JAR, processes .class files | Integrated into D8 dexer, processes directly to DEX |
R8 replaced ProGuard as the default code shrinker and obfuscator in the Android Gradle Plugin. Both use the same configuration file format (proguard-rules.pro), so from a rule-writing perspective they are interchangeable. The key difference is implementation: R8 operates directly on DEX bytecode as part of the D8 compilation pipeline, while ProGuard operated on Java bytecode before dexing.
For reverse engineers, this distinction rarely matters. The output of both tools produces the same general patterns in decompiled code. R8 tends to be more aggressive with certain optimizations (inlining, class merging), which can make decompiled output slightly different.
What They Do¶
Name Obfuscation¶
The most visible transformation. Classes, methods, and fields are renamed to short, meaningless identifiers:
com.example.myapp.network.ApiClient -> a.b.c
com.example.myapp.network.ApiClient.fetchUserProfile() -> a.b.c.a()
com.example.myapp.model.UserProfile -> a.b.d
com.example.myapp.model.UserProfile.displayName -> a.b.d.a
com.example.myapp.model.UserProfile.emailAddress -> a.b.d.b
Names are assigned alphabetically within each scope. The first class in a package becomes a, the second b, and so on. After z, naming continues with aa, ab, etc. This scheme is deterministic per build but changes between releases as code is added or removed.
Code Shrinking (Tree Shaking)¶
Removes unreachable code. Starting from entry points (activities, services, content providers, broadcast receivers declared in the manifest), R8/ProGuard traces all reachable code paths and discards everything else. This eliminates:
- Unused classes and interfaces
- Unused methods and fields
- Unused code branches within methods
- Unused library code pulled in as dependencies
For analysts, this means the APK only contains code that is actually reachable. Dead library code and unused features are stripped, reducing noise in the decompiled output.
Optimization¶
R8 performs several bytecode-level optimizations:
| Optimization | Effect on Decompiled Code |
|---|---|
| Method inlining | Small methods disappear; their code appears at call sites |
| Devirtualization | Virtual calls replaced with direct calls when only one implementation exists |
| Constant propagation | Computed constants replaced with literal values |
| Dead branch removal | Unreachable if/else branches eliminated |
| Enum unboxing | Enum classes replaced with int constants (R8 only, with full mode) |
| Class merging | Separate classes merged into one when possible (R8 only) |
| Outlining | Repeated code sequences extracted into shared methods (R8 only) |
What They Do NOT Do¶
R8 and ProGuard provide zero runtime protection:
- No string encryption -- all string literals remain as plaintext in the DEX
- No class/DEX encryption -- the DEX file is fully readable
- No anti-debugging -- no detection of debuggers, Frida, or Xposed
- No anti-tampering -- no signature verification or integrity checks
- No root/emulator detection -- no environmental checks
- No native code protection -- JNI libraries are untouched
- No control flow obfuscation -- code logic remains structurally intact
This is the fundamental distinction between R8/ProGuard and tools like DexGuard, Virbox, or Chinese packers. If an APK only uses R8/ProGuard, all strings, API calls, URLs, and logic are visible in static analysis. The only challenge is navigating renamed identifiers.
Reverse Engineering R8/ProGuard Output¶
Reading Obfuscated Code¶
Typical R8/ProGuard output in jadx:
package a.b;
public class c {
private final d a;
private String b;
public c(d dVar) {
this.a = dVar;
}
public void a(String str) {
this.b = str;
this.a.a("https://api.example.com/user", str, new a.b.e() {
@Override
public void a(String str2) {
f.a(str2);
}
@Override
public void b(Exception exc) {
Log.e("NetClient", exc.getMessage());
}
});
}
}
The class and method names are meaningless, but the string literals ("https://api.example.com/user", "NetClient") and Android framework calls (Log.e) are fully visible. This is the key advantage for analysts: R8/ProGuard cannot hide what the code actually does.
Using mapping.txt¶
When a build produces an R8/ProGuard-obfuscated APK, it also generates a mapping.txt file that maps obfuscated names back to original names. This file is used for crash report deobfuscation (uploaded to Google Play Console or Crashlytics).
Format:
com.example.myapp.network.ApiClient -> a.b.c:
okhttp3.OkHttpClient httpClient -> a
java.lang.String baseUrl -> b
void fetchUserProfile(java.lang.String) -> a
void handleResponse(okhttp3.Response) -> b
com.example.myapp.model.UserProfile -> a.b.d:
java.lang.String displayName -> a
java.lang.String emailAddress -> b
Analysts occasionally obtain mapping.txt through:
- Leaked build artifacts (CI/CD misconfigurations, exposed storage buckets)
- Google Play Console access (internal assessments)
- Bundled accidentally in the APK itself (rare but happens)
- Firebase Crashlytics storage (if accessible)
jadx can apply mapping files directly via File > Load mappings.
jadx Deobfuscation Features¶
jadx provides built-in deobfuscation that renames classes and methods based on usage patterns, even without mapping.txt:
- Auto-rename (
--deobfflag orPreferences > Deobfuscation): assigns readable names based on heuristics - Type-based renaming: when a field is assigned from
getSharedPreferences(), jadx can infer the field likely holds preferences - Interface method propagation: if an obfuscated class implements
View.OnClickListener, jadx knows thea()method is actuallyonClick()
Recovering Original Names¶
Even without mapping files, many original names survive obfuscation or can be inferred:
String References
Log tags, exception messages, and debug strings often reveal the original purpose:
The log tag "PaymentProcessor" reveals the class purpose despite its a name.
Android Framework and Library Calls
Method signatures of Android APIs and common libraries are never obfuscated:
public class a extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if ("android.provider.Telephony.SMS_RECEIVED".equals(action)) {
...
}
}
}
The superclass, overridden method name, and intent action string immediately identify this as an SMS interceptor.
Reflection Usage
Code that uses reflection must reference original class and method names as strings:
Class cls = Class.forName("android.telephony.TelephonyManager");
Method m = cls.getMethod("getDeviceId");
These strings survive obfuscation because they are runtime values, not compile-time identifiers.
Serialized Field Names
JSON serialization libraries preserve field names as strings:
public class d {
@SerializedName("account_number")
public String a;
@SerializedName("routing_number")
public String b;
@SerializedName("balance")
public double c;
}
The @SerializedName annotations (or equivalent JSON keys) reveal the real purpose of each field.
AIDL Interfaces
AIDL-generated code retains transaction names and descriptor strings:
Manifest-Declared Components
Activities, services, receivers, and providers declared in AndroidManifest.xml keep their full class names because the Android runtime needs to find and instantiate them.
Keep Rules as Intelligence¶
The proguard-rules.pro file (or consumer-rules.pro for libraries) defines which classes, methods, and fields must not be renamed or removed. These rules are a goldmine for analysts because they reveal architectural decisions and dependencies.
Reading Keep Rules¶
-keep class com.example.myapp.api.** { *; }
-keep class com.example.myapp.model.** { *; }
-keepclassmembers class * implements android.os.Parcelable {
static ** CREATOR;
}
-keepnames class * extends com.example.myapp.plugin.PluginBase
These rules tell an analyst:
- The
apiandmodelpackages contain classes used via reflection or serialization (likely API request/response models) - The app uses
Parcelablefor IPC - There is a plugin system with a
PluginBaseclass and dynamically loaded implementations
What Keep Rules Reveal¶
| Rule Pattern | Implies |
|---|---|
-keep class **.model.** { *; } |
JSON/XML serialization models -- field names map to API schema |
-keep class ** extends android.app.Service |
Services loaded by name (possibly from config) |
-keepclassmembers class * { @com.google.gson.annotations.* <fields>; } |
Uses Gson for JSON parsing |
-keep class **.BuildConfig { *; } |
Build configuration exposed at runtime |
-keep class * implements java.io.Serializable |
IPC or persistence via Java serialization |
-keep class **.js.** { *; } |
JavaScript bridge interfaces for WebView |
-keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; } |
WebView JS bridge -- potential attack surface |
Finding Keep Rules in APKs¶
R8 embeds a processed version of the keep rules into the build. Some APKs include the original proguard-rules.pro in the root of the APK or inside META-INF/. Additionally, library AARs bundle their own proguard.txt consumer rules that get merged into the final configuration.
Common Patterns¶
Enum Names Survive¶
Enum constants are almost always preserved because Enum.valueOf(String) requires the original name at runtime:
The class name a is obfuscated, but the constant names reveal intent. This is one of the most reliable sources of plaintext names in obfuscated APKs.
Parcelable Classes¶
Classes implementing Parcelable require a public CREATOR field and are often referenced by name in intents. The default ProGuard rules keep the CREATOR field, and the class itself is typically kept because it crosses process boundaries.
Manifest-Declared Components¶
All components declared in AndroidManifest.xml retain their original fully qualified class names:
<activity android:name="com.example.myapp.ui.LoginActivity" />
<service android:name="com.example.myapp.service.DataExfiltrationService" />
<receiver android:name="com.example.myapp.receiver.BootReceiver" />
These names survive R8/ProGuard because the Android framework instantiates them by name. The manifest is the first place to look for meaningful class names in an obfuscated APK.
Serialization Models¶
GSON, Moshi, Jackson, and similar libraries require field names to match JSON keys. These classes are either kept entirely or annotated with @SerializedName / @Json:
public class c {
@SerializedName("device_id")
String a;
@SerializedName("installed_apps")
List<String> b;
@SerializedName("sms_messages")
List<d> c;
}
The annotations expose the data model regardless of field renaming.
Native Method Declarations¶
JNI method names must match between Java and native code. If using static registration (not RegisterNatives), the native method names survive obfuscation:
However, if the class containing the native method is renamed, the corresponding JNI function name in the .so must also match the obfuscated name (e.g., Java_a_b_c_decryptPayload). Developers often keep native classes unobfuscated to avoid this complexity.
R8 vs ProGuard Differences¶
Class Merging (R8 Only)¶
R8 can merge classes that have a single implementation or are only used in one place. A class and its only subclass may be collapsed into one:
Before R8:
abstract class BaseRepository { void save(Data d); }
class UserRepository extends BaseRepository { void save(Data d) { ... } }
After R8:
class a { void a(b bVar) { ... } }
The inheritance relationship disappears entirely. This makes reconstructing the original architecture harder.
More Aggressive Inlining¶
R8 inlines more aggressively than ProGuard. Short methods (getters, setters, simple wrappers) are absorbed into their callers. The decompiled output may show inline code where the original had clean method boundaries:
This single line might represent three separate method calls in the original source.
Enum Unboxing (R8 Full Mode)¶
With R8 full mode (android.enableR8.fullMode=true), enums can be replaced with integer constants. The enum class disappears and all switch statements use raw ints. This removes the enum name survival pattern described above.
Kotlin Metadata Stripping¶
R8 strips Kotlin metadata annotations by default. ProGuard preserved them unless explicitly told to remove them. The presence or absence of kotlin.Metadata annotations on classes can hint at which tool was used.
Identifying Which Tool Was Used¶
| Indicator | ProGuard | R8 |
|---|---|---|
| Class merging observed | No | Possible |
| Kotlin metadata present | Often preserved | Stripped |
$$Lambda$ synthetic classes |
Present | Desugared differently |
| Enum constants as ints | No | Possible (full mode) |
| Build metadata comment in mapping.txt | # ProGuard, version X.Y.Z |
# compiler: R8 |
In practice, distinguishing the tool rarely matters for analysis. The deobfuscation approach is the same regardless.
Malware Usage¶
ProGuard/R8-Only Malware¶
Many Android malware families ship with only R8 or ProGuard obfuscation and no additional packing. This is the lowest tier of protection:
- All strings (C2 URLs, API keys, target app lists) are plaintext in the DEX
- All API calls are visible to static analysis tools
- Behavioral analysis is possible without any unpacking or decryption
- Automated scanners (VirusTotal, Google Play Protect) can pattern-match directly
Families that historically relied on R8/ProGuard alone include early variants of SpyNote, Cerberus-lineage builders, and many low-sophistication SMS stealers and banking trojans.
Distinguishing from DexGuard¶
DexGuard is built by Guardsquare, the same company that maintains ProGuard. DexGuard extends R8/ProGuard with encryption and runtime protection. Key differences in decompiled output:
| Feature | R8/ProGuard | DexGuard |
|---|---|---|
| String literals | Plaintext | Encrypted (method calls returning strings) |
| String access | "https://c2.example.com" |
o.oo("\\x4a\\x2f...") |
| Class loading | Standard | Custom class loader for encrypted classes |
| Native libraries | None added | libdexguard.so or obfuscated stubs |
| Asset files | Normal | Encrypted DEX payloads in assets/ |
| APKiD detection | No special flags | packer: DexGuard, anti_disassembly: DexGuard |
| Environmental checks | None | Root, emulator, debugger, Frida, Xposed detection |
The fastest way to distinguish: open the APK in jadx and search for string literals. If C2 URLs, package names, and configuration data appear as readable strings, it is R8/ProGuard. If strings are replaced by method calls to single-letter classes that take byte arrays, it is likely DexGuard or another string-encrypting protector.
Layered Protection¶
More sophisticated malware operations use R8/ProGuard as a base layer and add protection on top:
R8/ProGuard (name obfuscation, shrinking)
+ Custom string encryption (XOR/AES of sensitive strings)
+ Dynamic class loading (second-stage DEX from assets or network)
+ Native code for critical logic (C2 communication, credential theft)
This layered approach is cheaper than licensing DexGuard and gives operators more control. Analysts should not assume that R8/ProGuard-level obfuscation is the only protection present -- always check for custom encryption methods and dynamic loading patterns.
Analyst Workflow¶
1. Open APK in jadx
2. Check AndroidManifest.xml for component names (Activities, Services, Receivers)
3. Search strings for URLs, IPs, package names, API endpoints
4. If all strings are plaintext -> R8/ProGuard only, proceed with static analysis
5. If strings are encrypted -> additional protection present (DexGuard, custom)
6. Use jadx deobfuscation (--deobf) to auto-rename classes
7. Start from manifest-declared components and trace call graphs
8. Use enum names, log tags, and serialization annotations to reconstruct meaning
9. Cross-reference with mapping.txt if available