Obfuscation (Proguard & R8) and deobfuscation

    This article introduces the obfuscation and anti-obfuscation of Android. When it comes to obfuscation, everyone will naturally think of Proguard, in addition to R8. In fact, after AGP3.3, the official default is to use R8 for code optimization, obfuscation and compression. ProGuard and R8 are often used to obfuscate the final Android project, making it more difficult for the project to be decompiled.

Table of contents

1. ProGuard

2. R8

3. Comparison between Proguard and R8

4. Confusion

5. Anti-obfuscation

1. Mapping file

2、proguardgui.sh


1. ProGuard

ProGuard is a free tool to     compress, optimize and obfuscate Java bytecode files. Confusion is just one of the functions of ProGuard. ProGuard can compress (Shrink), optimize (Optimize), confuse (Obfuscate), and pre-check (Preveirfy) the code in Java classes. The following is the workflow of Proguard:

1. Compression (Shrink)

Remove unused classes, fields, methods and properties.

2. Optimize

Optimize the bytecode and remove useless instructions.

3. Obfuscate

Rename classes, fields and methods with meaningless names such as a, b, c, etc.

4、preveirfy

Preflights the processed code on the Java platform.

2. R8

    R8 is a tool to convert java bytecode to optimized dex. It walks through the entire application and then optimizes it, such as removing unused classes, methods, etc. It helps us reduce the size of our builds and make our applications more secure. R8 uses the rules of Proguard, R8 is faster and stronger than Proguard.

1. Code compression

Safely remove unused classes, fields, methods and properties from App and its library dependencies.

2. Resource compression

Remove unused resources from packaged apps, including unused resources in app library dependencies. It is used in conjunction with code shrinking so that once unused code is removed, resources that are no longer referenced can also be safely removed.

3. Code obfuscation

Reduce DEX file size by renaming classes, fields and methods in your code with short and meaningless names.

4. Code optimization

Remove unused code or rewrite code to be more concise.

3. Comparison between Proguard and R8

    When using Proguard, the application code is converted by the Java compiler into Java bytecode .class files. Then Proguard uses the rules we wrote to optimize it to produce an optimized .class file, and finally convert it into executable Dalvik bytecode. The process of compiling and packaging is as follows:

 After the introduction of R8, the obfuscation and conversion of the code into dex is completed in one step through R8. The process of compiling and packaging is as follows:

1. R8 effectively inlines container classes and removes unused classes, fields, and methods, making the package smaller.

2. Compared with Proguard, R8 has more support for Kotlin.

3. R8 is faster than Proguard, thus reducing the overall build time.

4. Confusion

Although Android Studio already uses R8 as the compiler by default, we still need to configure whether to enable code and resource compression in build.gradle(app):

    buildTypes {
        release {
            // 是否开启代码压缩
            minifyEnabled true
            // 是否开启资源压缩
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
            android.applicationVariants.all { variant ->
                variant.outputs.all {
                    outputFileName = "demo_" + defaultConfig.versionName + "_release.apk"
                }
            }
        }
    }

 In this article, we use the following demo. The code creates a null pointer exception, which is prepared for later de-obfuscation:

package com.example.proguarddemo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = findViewById(R.id.crash);
        mTextView.setOnClickListener(v -> {
            mTextView = null;
            mTextView.setText("11111");
        });
    }
}

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/crash"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

    Now that AS has used R8 as the compiler by default, we no longer compare the package volume and compilation speed of Proguard and R8. We use the demo of this blog to compare the volume of the release package with R8 and without R8.

(1) Do not open R8:

(2) Enable R8, but do not enable resource compression:

(3) Enable R8, enable resource compression:

 By comparing the package volume above, we can see that after enabling R8 + resource compression, the package volume is reduced by 55.6%. Next, let's see if the code is really confused. Install the packaged release package, click the textview after running, and a crash is triggered. Let's see if the source code can be seen on the stack:

2023-04-24 16:46:45.163 6037-6037/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.proguarddemo, PID: 6037
    java.lang.NullPointerException: throw with null exception
        at a1.a.onClick(SourceFile:5)
        at android.view.View.performClick(View.java:7281)
        at android.view.View.performClickInternal(View.java:7255)
        at android.view.View.access$3600(View.java:828)
        at android.view.View$PerformClick.run(View.java:27925)
        at android.os.Handler.handleCallback(Handler.java:900)
        at android.os.Handler.dispatchMessage(Handler.java:103)
        at android.os.Looper.loop(Looper.java:219)
        at android.app.ActivityThread.main(ActivityThread.java:8393)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)

It can be seen that the crashed stack is obfuscated, a1.a.onClick, the code is indeed obfuscated, which enhances the security of the code and makes it less easy for outsiders to decompile and obtain key codes.

5. Anti-obfuscation

    Following what was mentioned above, the security of the code is enhanced after the code is obfuscated. After the official package release, the crash stacks collected for online crashes are also obfuscated, which increases the difficulty of troubleshooting and locating crash problems. So, how to deobfuscate it?

    The principle of obfuscation is to change some class names, method names, attribute names, etc. to meaningless letters, etc., to reduce the readability of the code while compressing the code size. While obfuscating, a mapping file will be generated, which records the mapping relationship. Through the mapping relationship, the class name and method name before confusion can be obtained.

     

1. Mapping file

After launching the release package, the generated mapping file path: app/build/outputs/mapping/release

2、proguardgui.sh

There is an anti-obfuscation tool in the sdk: proguardgui.sh, which can help us analyze the crash stack through the mapping file. Go to Android/sdk/tools/proguard/bin and run directly

./proguardgui.sh

A gui interface will open, select the mapping file, paste the crash stack, and click Retrace!:

 As you can see, a null pointer occurs in the third line of the onClick method of MainActivity:

    This article systematically introduces Proguard and R8 of Android, summarizes the execution process and comparison of the two, and uses the actual demo to verify the optimized package size and confusion results of R8. At the same time, it also introduces how to use the mapping tool proguardgui.sh to assist us in parsing the obfuscated crash stack, so as to locate online problems more quickly. If it helps or inspires you, please pay attention and like it.

Guess you like

Origin blog.csdn.net/qq_21154101/article/details/130686892