Best Practices

In this section, we describe recommended steps to harden code against reverse engineering. The idea is to apply different techniques to create multiple layers of obfuscation. Although no single technique is unbreakable, each layer further raises the bar for attackers. The different techniques protect the application code, but they also protect one another. We go over each of the techniques, explaining why they are useful, how you can apply them, and how you can verify their effects.

Core design

Discussion

The essential basis for a hardened application is a sound design and a robust implementation. Even if you will obfuscate your code, you should look at it as if it is open source and consider the worst-case scenarios. The details are application-specific and lie outside the scope of this guide, but you can find the a lot of basic principles in books, blogs, and online presentations.

More information

First layer: secure communication

Discussion

If your application communicates with a server, you should encrypt the communication with SSL (Secure Sockets Layer) or the more recent TLS (Transport Layer Security). These protocols have been developed for secure communication between a web browser and a server. In a web browser, a user can see the padlock icon when the communication is secure. In an Android application, the user doesn't see a padlock icon, so the developer shouldn't forget to use SSL when appropriate.

The server contains a chain of certificates with public keys and certified names. You can obtain a certificate from a Certificate Authority (CA). With the standard Android API, the Android runtime checks such a chain with its database of root certificates. Alternatively, you can create a self-signed certificate. You then need to check the certificate yourself in your application.

Occasionally, a Certificate Authority is compromised (as happened with Comodo and DigiNotar). New releases of Android then typically update the database of root certificates. However, if your application always communicates with the same server, it should pin the certificate or its public key, i.e. only accept a minimal, fixed list of trusted certificates or keys. This technique ensures that the application is always talking to the right server and blocks man-in-the-middle attacks. You should be aware that some large websites update their certificates and keys regularly.

Configuration

You can find the code to create connections with certificate pinning or public key pinning for different Android APIs in the sample samples/SSLPinning.

Verification

More information

Second layer: name obfuscation

Discussion

DexGuard can obfuscate names in the applications that it processes. This means that it renames classes, fields, and methods using meaningless names, whereever possible. By default, it uses UTF-8 characters that are difficult to distinguish. This obfuscation step makes the code base smaller and harder to reverse-engineer.

Configuration

In release builds, DexGuard automatically applies name obfuscation. For any names of classes, fields, and methods that are involved in reflection, this may not be suitable. DexGuard's default configuration already takes care of common entry points like activities, intentions, etc. If other classes, fields, or methods need to keep their original names, you should specify the proper -keep options.

Verification

You can find the map of original names to obfuscated names in bin/proguard/mapping.txt (with Ant), proguard/mapping.txt (with Eclipse), or build/outputs/proguard/release/mapping.txt (with Gradle).

Such a mapping file may look like this:

com.example.HelloWorldActivity -> com.example.HelloWorldActivity:
    39:42:void onCreate(android.os.Bundle) -> onCreate
com.example.Util -> o.・:
    byte[] values -> ・
    int doSomething(android.content.Context) -> ・
    java.lang.String compute(android.content.Context) -> ˊ
Note that names from the Android runtime are not obfuscated, since obfuscating them would break the application.

You can also see the obfuscated names if you disassemble the code with a tool like dexdump (build-tools/20.0.0/dexdump in the Android SDK), or baksmali (free and open-source: code.google.com/p/smali/).

In the debug version of the application, the names are still readable:

  Class descriptor  : 'Lcom/example/Util;
  ...
    #0              : (in Lcom/example/Util;)
      name          : 'values'
      ...
In the hardened release version of the application, the names are obfuscated:
  Class descriptor  : 'Lo/・;'
  ...
    #0              : (in Lo/・;)
      name          : '・'
      ...

More information

Third layer: string encryption

Discussion

String constants in the source code are still readable in the compiled application, with a disassembler like dexdump or baksmali, or a decompiler for Dalvik bytecode. You should let DexGuard encrypt sensitive string constants, so they become invisible to static analysis. Keys, tokens, communication-related strings, and log messages are all good candidates for encryption. Note that string encryption is actually a form of obfuscation, since the strings necessarily have to be decrypted at runtime.

Configuration

You can apply string encryption with the option -encryptstrings. It offers a number of ways to specify the strings to be encrypted. The most common ones: All options accept wildcards. Technically, it is possible to encrypt all strings in all classes in the code, but this is generally not advisable for performance reasons.

Verification

In the debug version of the application, you'll easily find the original strings with a disassembler. For instance:
... const-string v0, "Hello world!"
In the release version of the application, encrypted strings should no longer be visible.

More information

Fourth layer: reflection

Discussion

Name obfuscation can't change invocations of runtime classes, methods, and fields, since that would break the application's code. These invocations therefore remain conveniently readable in the disassembled or decompiled code. This provides attackers a lot of information about the structure and execution flow of the application. Especially for sensitive APIs, such as encryption and secure communication, you may want to make the code less readable by replacing the direct invocations by reflection.

You can let DexGuard replace invocations by reflection and then encrypt the resulting strings. They then become difficult to find with static analysis.

Configuration

For example, encrypt some invocations of the SecureRandom with the option -accessthroughreflection:
-accessthroughreflection class java.security.SecureRandom {
    <init>();
    int nextInt();
}
You should combine the reflection with string encryption. More specifically, you can enumerate all strings that are created for the reflection:
-encryptstrings "java.security.SecureRandom", "nextInt"
An easier approach may be to encrypt all strings in the class that invokes the cryptographic classes:
-encryptstrings class com.example.MySecretClass

Verification

In the debug version of the application, you'll easily find the original method invocation with a disassembler. For instance:
... new-instance v0, Ljava/security/SecureRandom;
... invoke-direct {v0}, Ljava/security/SecureRandom;.:()V
...
... invoke-virtual {v0}, Ljava/security/SecureRandom;.nextInt:()I
In the hardened release version of the application., you can check that any invocations that you have specified are no longer visible

More information

Fifth layer: removing logging code and stack traces

Discussion

Logging code provides attackers information about the structure and execution flow of the application. From the perspective of thwarting reverse engineering, you should not leave logging code in released applications. If your logging code (or the logging code in your external libraries) does not depend on compile-time flags like Build.DEBUG, you can let DexGuard remove logging calls for you.

Configuration

Remove all standard Android logging invocations:
-assumenosideeffects class android.util.Log {
    public static boolean isLoggable(java.lang.String, int);
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
    public static java.lang.String
                    getStackTraceString(java.lang.Throwable);
}

Remove all printing of stack traces:

-assumenosideeffects class java.lang.Exception {
    public void printStackTrace();
}

Verification

In the debug version of the application, you may find many logging invocations with a disassembler. For instance:
... invoke-static {v0, v1}, Landroid/util/Log;.d:(Ljava/lang/String;Ljava/lang/String;)I
In the release version of the application, all of these invocation should be gone.

More information

Sixth layer: tamper detection

Discussion

Attackers can extract information like encryption and decryption keys by injecting logging code into an application. They disassemble the application, insert logging code, and run the application again. Their logging code can print out decrypted strings, which include the keys.

You can fight this type of dynamic analysis by adding tamper detection to your application. Tamper detection actively checks the integrity of the application at runtime and acts suitably if the application has been modified.

Configuration

DexGuard offers tamper detection with simple methods that you can call from your application. You first need to add the library samples/TamperDetection/libs/dexguard_util.jar to your application. You can then invoke the tamper detection methods in the application and act based on their return values. Both methods accept an optional additional integer parameter, which they return instead of 0 if the checks are ok. You can pick such a parameter to make the code less predictable.

You should weave the tamper detection code into your regular code, making it more difficult to simply disable it. For instance, you could use the return value to affect the computation of a key, so the computed key is wrong if the application has been tampered with. A simplistic example:

int checkedKey = dexguard.util.TamperDetector.checkApk(context, key);

If the code detects tampering, the application should fail quietly, possibly with a delay, without giving away any further information.

Once you have added your tamper detection code, you should further harden it by encrypting its class, together with the tamper detection library (dexguard.util.**).

Verification

You can check if the tamper detection works by unpacking the application and repackaging it, for instance with unzip and zip. You can also tamper with the application simply by applying the standard tool zipalign:
zipalign -f 4 myapplication.apk
The tool repackages the application without fundamentally changing it. This will trigger the tamper detection at runtime.

You can check the certificate verification by re-signing the application with a different certificate:

zip -d MyApp.apk META-INF/*
jarsigner -keystore debug.keystore -storepass android -keypass android -signedjar \
    MyApp_signed.apk MyApp.apk AndroidDebugKey
This will trigger the certificate check at runtime.

More information

Seventh layer: environment checks

Discussion

Even if the application is unaltered, the underlying runtime may be subverted. The application calls into the Android runtime in good faith, but the runtime code may have been compromised. It may for instance intercept method calls that communicate or decrypt sensitive data, and pass the results to an attacker.

You may therefore want to check the environment in which the application is running. This is tricky, since the subverted environment may be constructed to look as ordinary and inconspicuous as possible. Nevertheless, you can perform some basic sanity checks:

Like tamper detection, these techniques actively check the integrity of the application and its environment at runtime. You can then act suitably if the environment is not the standard intended environment.

Configuration

DexGuard offers environment checks as simple methods that you can call from your application. You first need to add the library samples/EnvironmentCheck/libs/dexguard_util.jar to your application. You can then invoke the environment checking methods in the application and act based on their return values. All methods accept an optional additional integer parameter. which they return instead of 0 if the checks are ok. You can pick such a parameter to make the code less predictable.

You should again weave the tamper detection code into your regular code and further harden it by encrypting the class together with the library (dexguard.util.**).

Verification

If you have implemented these checks, you can try setting the debug flag, running the application in an emulator, or running the application on a rooted device.

More information

Eighth layer: class encryption

Discussion

Name obfuscation, string encryption, and reflection already help against static analysis, and tamper detection and environment checks help against dynamic analysis. Class encryption can provide another powerful layer over these techniques. It can completely hide the decryption code and reflection code, hardening it against static analysis and against tampering.

Encrypting all classes is not technically possible and would add an excessive overhead. You should identify a number of sensitive classes and encrypt those. Typically, these are the classes that you have already hardened with other techniques. For instance, you should include your tamper detection code and the associated library classes.

Configuration

You can specify the classes that you want to encrypt with the option -encryptclasses:
-encryptclasses com.example.MySecretClass,
                com.example.MySecretClass$*
The second name in the list matches all inner classes of MySecretClass, since inner classes often also contain sensitive code.

Verification

Applying a disassembler, you may find traces of string encryption, reflection, tamper detection, environment checks, and original code in the hardened release version of the application.

After having added class encryption for the class that contains such code, it will no longer be visible or modifiable.

More information


Copyright © 2002-2014 Saikoa BVBA.