August 8, 2023

    Android Security and Obfuscation Realities of R8

    Protecting Android applications is a top priority for developers. A common approach we see is the use of R8 or ProGuard to protect and secure an application, but its value in this regard is often overestimated. R8 is integrated in Android Studio and like ProGuard, offers two main functionalities: shrinking and optimization. R8 performs code shrinking and optimizations by removing unused code & resources and applying various code transformations, resulting in a reduced app size for faster downloads and an improved user experience. These features were designed for optimization and not for security and hardening of your application. In this blog post, we'll delve into the realities and misconceptions of relying on R8 for security and obfuscation purposes.

    The original purpose of (name) obfuscation:

    While name obfuscation is a commonly employed technique in Android app development, it's important to note that its introduction in R8 is not primarily intended for obfuscation purposes. Instead, R8 incorporates name obfuscation as part of its code shrinking functionality. The primary goal of name obfuscation in R8 is to reduce the size of the application by transforming human-readable identifiers, such as variable names, method names, and class names, into shorter and more compact representations. By doing so, R8 removes unnecessary metadata and reduces the overall footprint of the app, making it smaller and faster to download.

    Example:
    // A Decompiled Method Before Name Obfuscation
    java
        public static void validateLicense(String key) {
            if (key.equals("SECRET_KEY")) {
                grantPremiumAccess();
            } else {
                denyAccess();
            }
        }
    
    
    // A Decompiled Method After Name Obfuscation:
    
    java
        public static void a(String b) {
            if (b.equals("SECRET_KEY")) {
                c();
            } else {
                d();
            }
        }

    When implementing name obfuscation, R8 utilizes techniques that transform human-readable identifiers into more obscure and abbreviated forms. As demonstrated in the provided example, the method "validateLicense" was converted to a seemingly arbitrary and enigmatic name "a". While this undoubtedly makes the code less readable to humans, it's crucial to understand that the fundamental logic and algorithms of the application remain unchanged. This means that the core functionalities of the code and the SECRET_KEY are still exposed to potential theft, making it imperative for developers to exercise caution and implement additional layers of security.

    String encryption can be used to safeguard sensitive data, such as API keys, access tokens, or other critical information stored within the app. By encrypting data, even if an attacker manages to reverse engineer and understand the code, they would face the additional barrier of decrypting the secured information, adding an extra layer of defense.

    If you are looking for a resilient approach to preventing reverse engineering of Android applications, then name obfuscation can be considered a cornerstone, but only when combined with additional code obfuscation techniques.

    Method inlining:

    Inlining, a technique offered by R8, involves the direct insertion of method code into the calling code, thereby potentially improving application performance. The perception is that inlining reduces the named references to a method making it harder to identify specific logic of the application. While this is true, removing an explicit call to a method doesn’t do anything to hide the logic and behavior of the code, resulting in a very trivial benefit, as shown in the example below:

    // before method inlining 
    public boolean isValidKey(String key) {
        if (key.length() != 19 || key.charAt(4) != '-' || key.charAt(9) != '-' ||
            key.charAt(14) != '-') {
            return false;
        }
        int sum = 0;
        for (int i = 0; i < key.length(); i++) {
            if (Character.isLetterOrDigit(key.charAt(i))) {
                sum += key.charAt(i);
            }
        }
        return sum % 2 == 0;
    }
    
    // easy to see where isValid logic ends and granting premium access starts
    public static void validateLicense(String key) {
        if (isValidKey(key)) {
            grantPremiumAccess();
        } else {
            denyAccess();
        }
    }
    
    
    // after method inlining 
    public static void validateLicense(String key) {
        boolean a = false;
        if (key.length() != 19 || key.charAt(4) != '-' || key.charAt(9) != '-' || key.charAt(14) != '-') {
            a = false;
        } else {
            int sum = 0;
            for (int i = 0; i < key.length(); i++) {
                if (Character.isLetterOrDigit(key.charAt(i))) {
                    sum += key.charAt(i);
                }
            }
            a = sum % 2 == 0;
        }
    // harder to see where isValid logic ends and granting premium access starts
        if (a) {
            grantPremiumAccess();
        } else {
            denyAccess();
        }
    }
    

    To improve the protection of your methods and their logic, you should consider implementing more advanced code obfuscation features, including arithmetic obfuscation, and potentially control-flow obfuscation to make it significantly more difficult to understand through static analysis.

    Arithmetic obfuscation will transform arithmetic expressions into semantically equivalent but more complex expressions. The goal of this operation is to make it harder for an attacker to read and interpret these expressions.

    Control flow refers to the order in which statements and instructions are executed in a program. Obfuscating the control flow involves altering the sequence of instructions or introducing dummy code paths that do not affect the program's functionality.

    Class merging:

    Class merging, another optimization feature of R8, consolidates small classes with similar functionalities into a larger one, by doing so, the application's codebase is simplified. This consolidation may be perceived to strengthen security by minimizing the visibility of individual classes. As with the method inlining example, this optimization is doing little to increase the complexity of understanding the logic or behavior of the code.

    In order to further protect your classes, you should consider class encryption which prevents reverse engineering with decompiler tools like JADX by encrypting the contents of Java class files to make them unreadable and unusable without the correct decryption key.

    Conclusion

    R8 provides a lot of value to Android Developers by providing optimization, shrinking and name obfuscation. While name obfuscation and optimizations can result in some obfuscation for Android applications, it should be viewed as one element of a comprehensive security approach. Relying solely on these techniques may provide a false sense of security, as determined attackers can easily overcome the basic level of obfuscation to achieve their goal. A purpose built security-oriented obfuscation and protection strategy will incorporate additional important security features which focus on:

    • Renaming classes, fields, methods, libraries etc..
    • Altering the structure of the code
    • Transforming arithmetic and logical expressions
    • Encryption of strings, classes, etc..
    • Removing certain metadata
    • Hiding calls to sensitive APIs

    To learn more about additional levels of code obfuscation check out our blog post on advanced code obfuscation techniques.

    Guardsquare

    Learn more about advanced Android protection
    with a free DexGuard demo

    Request a demo >

    Other posts you might be interested in