- 22 Jan 2025
- 7 Minutes à lire
- SombreLumière
- PDF
Obfuscation
- Mis à jour le 22 Jan 2025
- 7 Minutes à lire
- SombreLumière
- PDF
When an app is compiled from Java source code into the dex format, there is a number of debug data that is leaked through. This data will reveal much about the structure and organization of your application, and makes it much easier to reverse-engineer your application from compiled form.
Consider the following example class:
package com.example;
import android.util.Log;
@MyAnnotation
public class MyExampleClass {
private static final String TAG = MyExampleClass.class.toString();
private static int example(int paramOne, String paramTwo, boolean paramThree) {
if (paramThree) {
String logMessage = paramTwo + paramOne;
Log.d(TAG, logMessage);
}
int returnValue = paramOne + 50;
return returnValue;
}
}
After compiling to dex the same class looks like the following in smali:
.class public Lcom/example/MyExampleClass;
.super Ljava/lang/Object;
.source "MyExampleClass.java"
# annotations
.annotation build Lcom/example/MyAnnotation;
.end annotation
# static fields
.field private static final TAG:Ljava/lang/String;
# direct methods
.method static constructor <clinit>()V
.registers 1
.line 7
const-class v0, Lcom/example/MyExampleClass;
invoke-virtual {v0}, Ljava/lang/Class;->toString()Ljava/lang/String;
move-result-object v0
sput-object v0, Lcom/example/MyExampleClass;->TAG:Ljava/lang/String;
return-void
.end method
.method public constructor <init>()V
.registers 1
.line 6
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method private static example(ILjava/lang/String;Z)I
.registers 5
.param p0, "paramOne" # I
.param p1, "paramTwo" # Ljava/lang/String;
.param p2, "paramThree" # Z
.line 10
if-eqz p2, :cond_17
.line 11
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
.line 12
.local v0, "logMessage":Ljava/lang/String;
sget-object v1, Lcom/example/MyExampleClass;->TAG:Ljava/lang/String;
invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_18
.line 10
.end local v0 # "logMessage":Ljava/lang/String;
:cond_17
nop
.line 15
:goto_18
add-int/lit8 v0, p0, 0x32
.line 17
.local v0, "returnValue":I
return v0
.end method
There are a number of items in the preceding compiled code that can be omitted, removed or obfuscated:
① Class names including package names can be obfuscated.
.source is used in when generating stack traces and does not have any practical use elsewhere. It is safe and recommended to remove these with removeSourceFile *;.
Annotations are not obfuscated by default.
④ Fields can be obfuscated, as long as they are not accessed by reflection.
<clinit> (class constructor, initialization of static fields etc) and <init> may not be obfuscated.
Line numbers (.line) are only used when generating stack traces, and do not have any practical use elsewhere. It is safe and recommended to remove these with removeLines *;
⑦ Method name can be obfuscated, as long as they are not accessed by reflection.
Param information such as .param p0, "paramOne" # I is only used during debugging, and does not have any practical use elsewhere. They cannot be removed fully due to potential annotations being attached to params here. However, their name and type can be removed to make them less valuable. This is done with the rule removeDebug *;
Variable debug information such as .local v0, "logMessage":Ljava/lang/String; is only used for debugging, and it is safe and recommended to remove these with the option removeDebug *;
Java is a dynamic language which is used extensively with reflection by Android itself as well as by many libraries. The Shielding Tool can provide full app obfuscation where class names, method names and field names are given new randomly generated names. There are in general two scenarios where renaming a class, method or field name will cause problems and should be avoided:
Methods that reference/override from external SDKs and APIs (frameworks). E.g, method onCreate(Bundle) from Activity
Classes, methods and fields accessed through reflection.
The Shielding Tool will avoid obfuscating a class and its members if it is missing any ancestor classes. This is because the Shielding Tool cannot safely determine which members are overridden or not.
The use of reflection is commonly causing problems for obfuscation tools like the Shielding Tool and other third party tools like ProGuard. A declarative rule language exists to inform the Shielding Tool not to obfuscate specific parts of the application. These rules are required for parts of the application where the Shielding Tool’s static code analyzer fails to determine how or when a specific class or method is being used.
Obfuscation is not a fully automatic process, and will typically require some manual configuration of the Shielding Tool rules in the rules file in addition to, or replacement of, the built-in default-unobfuscate rules.
To enable obfuscation, pass a rules file to the Shielding Tool (with the command line option --rules file) with the following contents:
# Obfuscate and remove debug from all classes
obfuscate removeDebug removeLines removeSourceFile *;
# Include default exceptions
include builtin:default-unobfuscate.cfg;
See Configuring Shielding Tool rules for more information about the syntax for this file.
Of course this can be customized further. For instance, instead of obfuscate *; a more narrow set can be chosen in order to only obfuscate selected parts of the app.
removeDebug, removeLines, and removeSourceFile are all optional but do improve obfuscation.
The Shielding Tool generates a mapping file as output in the output folder (--output). This file contains mappings of original names to obfuscated names. This file is useful for de-obfuscating back traces from the app, and is therefore important to preserve.
Excluding framework jars from obfuscation
In order for the Shielding Tool to know which classes and methods are safe to modify during obfuscation, you must configure the Shielding Tool by pointing to framework Jar files. This can be done using the frameworkjar directive where you point to one or multiple such JAR files.
The most important framework jar is the Android SDK itself. Generally, pointing to the same version as used in targetSdk is the recommended option.
If no frameworkjar directives are provided, then the Shielding Tool will by default fall back to an internal framework file covering the current Android.jar interface.
Example: Using library .jar files:
frameworkjar /home/android/platforms/android-23/android.jar;
Default rules
The Shielding Tool comes with a few rule files bundled in. These are loaded automatically depending on the options used. For instance, to enable obfuscation, you can pass a rules file to the Shielding Tool (with the comman dline option --rules file) with the following contents:
include "builtin:obfuscate-on.cfg";
This loads the builtin rules file builtin:obfuscate-on.cfg, which enables class name obfuscation for all classes, and then loads another built-in rules file builtin:default-unobfuscate.cfg, which includes rules to not obfuscate some well known exceptions.
It is possible to load these files from your own rules file using the following syntax:
# Obfuscate all classes in my package:
obfuscate class com.example.*;
# but ensure that all well known exceptions are applied:
include "builtin:default-unobfuscate.cfg";
Contents of builtin:default-unobfuscate.cfg:
# The static Enum#values() method is generated on all Enums, and accessed through reflection.
# These methods should not be obfuscated
match enum * {
preserve public static values();
preserve public static valueOf(...);
}
# android.os.Parcelable.
preserve class * implements android.os.Parcelable {
public static field * CREATOR;
}
# No obfuscation on annotations
preserve interface * implements java.lang.annotation.Annotation;
# Kotlin
match class *$WhenMappings {
preserve <fields>;
}
# android.arch lifecycle.
match enum android.arch.lifecycle.Lifecycle$Event {
preserve <fields>;
}
preserve class * implements android.arch.lifecycle.LifecycleObserver;
preserve class * implements android.arch.lifecycle.GeneratedAdapter;
match class * {
preserve @android.arch.lifecycle.OnLifecycleEvent <members>;
}
# Preserve classes and method names when there's native methods
preserve class * {
native *(...);
}
# Preserve annotated javascript interface methods
match class * {
preserve @android.webkit.JavascriptInterface <methods>;
}
# Firebase
match com.google.firebase.iid.FirebaseInstanceId {
preserve com.google.firebase.iid.FirebaseInstanceId getInstance(...);
}
# Understand the @Keep support annotation.
preserve interface android.support.annotation.Keep;
preserve interface androidx.annotation.Keep;
preserve @android.support.annotation.Keep class *;
preserve @androidx.annotation.Keep class *;
match class * {
preserve @android.support.annotation.Keep <members>;
preserve @androidx.annotation.Keep <members>;
}
# View onClick handlers
match class * {
preserve public *(android.view.View);
}
# Temporarily necessary until processing of resources.asrc (strings.xml) is added.
preserve android.support.design.widget.AppBarLayout$ScrollingViewBehavior {}
preserve com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior {}
preserve android.support.design.widget.BottomSheetBehavior {}
preserve com.google.android.material.bottomsheet.BottomSheetBehavior {}
preserve android.support.design.transformation.FabTransformationScrimBehavior {}
preserve com.google.android.material.transformation.FabTransformationScrimBehavior {}
preserve android.support.design.transformation.FabTransformationSheetBehavior {}
preserve com.google.android.material.transformation.FabTransformationSheetBehavior {}
preserve android.support.design.behavior.HideBottomViewOnScrollBehavior {}
preserve com.google.android.material.behavior.HideBottomViewOnScrollBehavior {}
## Dynamite (Google gms)
# enforceInterface() which specifically checks if classNames was changed
preserve com.google.android.gms.dynamite.* {}
match com.google.android.gms.dynamite.DynamiteModule$DynamiteLoaderClassLoader {
preserve field sClassLoader;
}
## Cordova related
# Google maps plugin for cordova. Detailed in SHAND-2043
match plugin.google.maps.PluginEnvironment {
preserve isAvailable(org.json.JSONArray, org.apache.cordova.CallbackContext);
}
# Cordova webview reflection. Detailed in SHAND-2043
match org.apache.cordova.CordovaWebViewImpl {
preserve getView();
}
# Preserve any pluginManager
match org.apache.cordova.* {
preserve org.apache.cordova.PluginManager pluginManager;
}
# The annotations used for these classes uses Google's gson to parse a json file.
# In order to do that, all the methods and fields in that class needs to be
# excluded for a correct parsing.
preserve * {
@com.google.gson.* <members>;
preserve <members>;
}
Other obfuscation options
Other obfuscation options include:
removeDebug|keepDebug
Removes or keeps debug information such as variable and parameter names from matched classes/members
removeDebug *;
com.example.MyExampleClass {
keepDebug debugMethod(...);
}
removeLines|keepLines
Removes or keeps line numbers from the compiled code
removeLines *;
removeLines class com.example.MyExampleClass;
keepLines class com.example.MyExampleClass;
match com.example.MyExampleClass {
keepLines debugMethod(...);
}
removeSourceFile|keepSourceFile
Removes or keeps source file, e.g MyExampleClass.java. This line of text shows up in stack traces.
removeSourceFile *;
removeSourceFile com.example.my_app_classes.*;
keepSourceFile class @com.example.MyAnnotation * { }