Android 逆向系列:Android APK 代碼混淆


Android 逆向系列:Android APK 代碼混淆

  • 發佈文章









Android 逆向系列:Android APK 代碼混淆

同步滾動:

你能攻擊我的 app,那我肯定有防守的策略,接下來我們介紹一下 Android 中的混淆技術

注意:下面演示均是在 mac 下進行

Github Demo 地址:github.com/sweetying52…

一、jadx 介紹

在此之前,我想介紹另外一款反編譯工具:jadx,它相當於是 APKtool + dex2JAR + jd-gui 的結合體,既能反編譯代碼也能反編譯資源,關鍵使用起來還特別簡單,你只需要將文件拖進來即可,一定程度上提高了我們的開發效率

Github 地址:github.com/skylot/jadx

1.1、jadx 特點

1、能將 APK,AAR,JAR,DEX,AAB,ZIP 等文件中的代碼反編譯為 Java

2、能反編譯 APK,AAR,AAB,ZIP 中的資源

1.2、jadx 安裝

1、安裝 jadx,推薦使用 brew 去安裝,執行如下命令:

brew install jadx

使用 brew 安裝的好處就是 mac 會給你自動配置好環境變量,你只需要專註軟件的使用即可,等待安裝完成在驗證一下

2、jadx 驗證

在 Terminal 輸入如下命令:

jadx --version

如果打印出了版本號就證明安裝成功了:

1.3、jadx 使用

這裡我們直接使用 jadx 提供的可視化界面進行操作

1、在 Terminal 輸入如下命令:

jadx-gui

此時就會打開 jadx 的可視化界面了:

2、將你需要反編譯的文件拖入即可查看反編譯的代碼和資源了,如下圖:

二、混淆 APK 代碼

2.1、準備工作

首先我們先做一些準備工作

1、添加一些類:

//1、新建 Utils.java 文件,創建 Utils 類
public class Utils {

    public void methodNormal(){
        String logMessage = "this is normal method";
        logMessage = logMessage.toLowerCase();
        System.out.println(logMessage);
    }

    public void methodUnused(){
        String logMessage = "this is unused method";
        logMessage = logMessage.toLowerCase();
        System.out.println(logMessage);
    }
}

//2、新建 NativeUtils.java 文件,創建 NativeUtils 類
public class NativeUtils {

    public static native void methodNative();

    public static void methodNotNative(){
        String logMessage = "this is not native method";
        logMessage = logMessage.toLowerCase();
        System.out.println(logMessage);
    }
}

//3、新建 MyFragment.java 文件,創建 MyFragment 類
public class MyFragment extends Fragment {

    private String toastTips = "toast in MyFragment";

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_layout,container,false);
        methodWithGlobalVariable();
        methodWithLocalVariable();
        return rootView;
    }

    private void methodWithGlobalVariable() {
        Toast.makeText(getActivity(), toastTips, Toast.LENGTH_SHORT).show();
    }

    private void methodWithLocalVariable() {
        String logMessage = "log in MyFragment";
        logMessage = logMessage.toLowerCase();
        System.out.println(logMessage);
    }
}

2、接着在 MainActivity 中進行引用

public class MainActivity extends AppCompatActivity {

    String toastTips = "toast in MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getSupportFragmentManager().beginTransaction().add(R.id.flFragmentContainer,new MyFragment()).commit();
        //1、Utils 下的方法調用
        Utils utils = new Utils();
        utils.methodNormal();
        //2、NativeUtils 下的方法調用
        try {
            NativeUtils.methodNative();
            NativeUtils.methodNotNative();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        //3、第三方庫下工具類的方法調用
        int result = StringUtils.getLength("erdai666");
        System.out.println(result);
        //4、MainActivity 下的 methodWithGlobalVariable 方法調用
        methodWithGlobalVariable();
        //5、MainActivity 下的 methodWithLocalVariable 方法調用
        methodWithLocalVariable();
    }

    private void methodWithGlobalVariable() {
        Toast.makeText(this, toastTips, Toast.LENGTH_SHORT).show();
    }

    private void methodWithLocalVariable() {
        String logMessage = "log in MainActivity";
        logMessage = logMessage.toLowerCase();
        System.out.println(logMessage);
    }
}

好的,到這裡準備工作已經基本完成,接下來我們對 APK 中的代碼進行混淆

2.2、開啟混淆打 APK 包

1、在 app 的 build.Gradle 文件中的 android 閉包下 的 release 閉包中開啟代碼混淆:

android {
    buildTypes {
        release {
            //開啟代碼混淆
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

上述我們僅是把 minifyEnabled 改為 true 即開啟了代碼混淆,非常的簡單

另外需要注意: 這裡是在 release 閉包內進行配置的,因此只有打出正式版的 APK 才會進行混淆,debug 版的 APK 是不會混淆的。當然這也是非常合理的,因為 debug 版的 APK 文件我們只會用來內部測試,不用擔心被人破解。

2、接下來打一個正式的 APK 包

1、在 Android Studio 導航欄中點擊 Build -> Generate Signed Bundle or APK,選擇 APK

2、然後選擇簽名文件並輸入密碼,如果沒有簽名文件就創建一個

3、點擊 next 選擇打 release 包,最終點擊 Finish 完成打包

4、生成的 APK 會自動存放在 app/release/ 目錄下

Tips: 我們可以在 app 的 build.gradle 文件中添加簽名文件配置,後續就可以直接通過 ./gradlew assembleRelease 命令或者 AndroidStudio 右側的 Gradle 可視化界面去操作:

android {
    //1、聲明簽名文件
    signingConfigs{
        release{
            storeFile file('../Certificate')
            storePassword 'erdai666'
            keyAlias 'key0'
            keyPassword 'erdai666'
        }
    }

    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            //2、配置簽名文件
            signingConfig signingConfigs.release
        }
    }
}

需要注意

1、使用 AndroidStudio 導航欄 Generate Signed Bundle or APK 方式生成的 APK 在:app/release/ 目錄下

2、使用 ./gradlew assembleRelease 命令或者 AndroidStudio 右側的 Gradle 可視化界生成的 APK 在:app/build/outputs/apk/ 目錄下

3、接着使用 jadx 打開當前 APK,如下圖所示:

很明顯我們代碼混淆的功能已經生效了。

2.3、混淆文件介紹

下面我們嘗試來閱讀一下混淆後之前準備的那些類:

MainActivity

可以看到:

1、MyFragment 被混淆了

2、Utils 下的方法調用:直接是把方法裏面的內容拷貝到了方法的調用處

3、NativeUtils 下的方法調用:1、Native 方法還是正常的調用 2、非 Native 方法則是把方法裏面的內容拷貝到了方法的調用處

4、第三方庫下工具類的方法調用:直接是將方法的結果填充到了調用處

5、MainActivity 中的成員方法:直接是把成員方法裏面的內容拷貝到了方法的調用處

6、MainActivity 類名是沒有混淆的,onCreate 方法也沒有被混淆,但定義的成員變量,局部變量被混淆了

Utils

Utils 類直接沒有了

NativeUtils

可以看到:

1、NativeUtils 類名沒有被混淆,其中聲明成 native 的方法也沒有被混淆

2、非 Native 方法直接沒有了,方法的內容拷貝到了方法的調用處

MyFragment

可以看到:

1、所有的方法名,成員變量,局部變量都被混淆了

2、MyFragment 中的成員方法:直接是把成員方法裏面的內容拷貝到了方法的調用處

接下來在分析一下上面的混淆結果

1、Utils 直接沒有了,因為它被調用的方法內容直接拷貝到了方法的調用處。另外一個方法沒有被調用,會被認為是多餘的代碼,在打包的時候就給移除掉了,不僅僅是方法,沒有調用的資源同樣會被移除,這樣的好處是可以減少 APK 的體積

2、NativeUtils 類名沒有被混淆,這是由於它有一個聲明成 native 的方法。只要一個類中有存在 native 方法,它的類名就不會被混淆,native 方法的方法名也不會被混淆,因為 C 或 C++ 代碼要通過包名+類名+方法名來進行交互。 但是類中別的代碼還是會被混淆,它的非 Native 方法直接沒有了,因為方法裏面的內容拷貝到了方法的調用處

3、MyFragment 是混淆的比較徹底的,基本沒有任何保留,連生命周期方法也被混淆了,Fragment 怎麼說也算是一個系統組件吧,搞的一點面子都沒有

4、MainActivity 的保留程度就比 MyFragment 好多了,至少像類名,生命周期方法都沒有被混淆,這是因為:凡是需要在 AndroidManifest.xml 中註冊的所有類的類名以及從父類重寫的方法名都不會被混淆。 因此,除了 Activity 之外,這份規則同樣適用於:Service,BroadcastReceiver 和 ContentProvider

5、引入的第三方庫也被混淆了,上述可以看到直接是把方法調用的結果給填充了進來

2.4、默認混淆規則介紹

那麼這些混淆規則是在哪裡定義的呢?其實就是剛才在 build.gradle 的 release 閉包下配置的 proguard-android-optimize.txt 文件,這個文件存放於Android SDK/tools/proguard/目錄下:

看一眼它的具體內容:

# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# This file is no longer maintained and is not used by new (2.2+) versions of the
# Android plugin for Gradle. Instead, the Android plugin for Gradle generates the
# default rules at build time and stores them in the build directory.

# Optimizations: If you don't want to optimize, use the
# proguard-android.txt configuration file instead of this one, which
# turns off the optimization flags.  Adding optimization introduces
# certain risks, since for example not all optimizations performed by
# ProGuard works on all versions of Dalvik.  The following flags turn
# off various optimizations known to have issues, but the list may not
# be complete or up to date. (The "arithmetic" optimization can be
# used if you are only targeting Android 2.0 or later.)  Make sure you
# test thoroughly if you go this route.
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify

# The remainder of this file is identical to the non-optimized version
# of the Proguard configuration file (except that the other file has
# flags to turn off optimization).

-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose

-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
    native <methods>;
}

# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}

-keepclassmembers class **.R$* {
    public static <fields>;
}

# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version.  We know about them, and they are safe.
-dontwarn android.support.**

# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep

-keep @android.support.annotation.Keep class * {*;}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}

這個就是默認的混淆配置文件了,我們來逐行解釋一下:

# 啟動優化相關的一些配置
# 指定更精細級別的優化
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 表示對代碼優化的次數,一般為 5
-optimizationpasses 5
# 允許改變作用域
-allowaccessmodification
# 關閉預驗證
-dontpreverify

# 表示混淆時不使用大小寫混合類名
-dontusemixedcaseclassnames

# 表示不跳過 library 中的非 public 類
-dontskipnonpubliclibraryclasses

# 表示打印混淆的詳細信息
-verbose

#表示對註解中的參數進行保留
-keepattributes *Annotation*

# 表示不混淆如下聲明的兩個類,這兩個類基本上也用不上,是接入 Google 原生的一些服務時使用的
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# 表示不混淆任何包含 native 方法的類名以及 native 方法名,這個和剛才驗證的結果是一致的
-keepclasseswithmembernames class * {
    native <methods>;
}

# 表示不混淆 View 中的 setXXX() 和 getXXX() 方法,因為屬性動畫需要有相應的 setter 和 getter 方法實現
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# 表示不混淆 Activity 中參數是 View 的方法,因為有這麼一種用法,在 XML 中配置 android:onClick="btnClick" 屬性,混淆就找
# 不到了
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# 表示不混淆枚舉的 values() 和 valueOf() 方法
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 表示不混淆 Parcelable 實現類中的 CREATOR 字段,毫無疑問,CREATOR 字段是絕對不能改變的,包括大小寫都不能變,不然整個
# Parcelable 工作機制都會失效
-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}

# 表示不混淆 R 文件中的所有靜態字段,我們都知道 R 文件是通過字段來記錄每個資源 id ,字段名如果被混淆,id 就找不到了
-keepclassmembers class **.R$* {
    public static <fields>;
}

# 表示對 android.support 包下的代碼不警告,因為 support 包中的所有代碼都在兼容性上做了足夠的判斷,因此不用擔心代碼會出問題
# 所以直接忽略警告就可以了
-dontwarn android.support.**

# 表示不混淆 android.support.annotation.Keep 這個註解類的所有東西
-keep class android.support.annotation.Keep

# 表示不混淆使用了 class android.support.Keep 註解的類的所有東西
-keep @android.support.annotation.Keep class * {*;}

# 表示不混淆類名和類中使用了 class android.support.Keep 註解的方法
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}

# 表示不混淆類名和類中使用了 class android.support.Keep 註解的屬性
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}

# 表示不混淆類名和類中使用了 class android.support.Keep 註解的構造方法
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}

2.4.1、proguard-android-optimize.txt 和 proguard-android.txt 區別

之前一些 AGP 老版本,我們新建工程默認使用的是:proguard-android.txt,那麼它和 proguard-android-optimize.txt 有啥區別呢?

從字面的維度看,就多了一個 optimize(優化)這個單詞,實際就是多了優化這一部分,proguard-android-optimize.txt 相對於 proguard-android.txt 開啟了優化相關的配置:

# proguard-android-optimize.txt 新增了以下優化規則
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify

# proguard-android-optimize.txt 刪除了關閉優化指令的配置
# -dontoptimize

好了,上述就是 proguard-android-optimize.txt 文件中所有的默認配置,而我們混淆代碼也是按照這些配置的規則來進行混淆的。經過上面的講解,相信大家對這些配置的內容基本都能理解了。不過 Proguard 語法中還真有幾處非常難理解的地方,下面和大家分享一下這些難懂的語法部分

2.5、Proguard 疑難語法介紹

Proguard 中一共有三組六個 keep 關鍵字,很多人搞不清楚他們的區別,我們通過一個表格直觀的來看一下:

關鍵字

描述

keep

保留類和類中的成員不被混淆或移除

keepnames

在 keep 的基礎上,如果成員沒有被引用,則會被移除

keepclassmembers

保留類成員不被混淆或移除

keepclassmembernames

在 keepclassmembers 基礎上,如果成員沒有被引用,則會被移除

keepclasseswithmembers

保留類和類中的成員不被混淆或移除,前提是類中的成員必須存在,否則還是會被混淆

keepclasseswithmembernames

在 keepclasseswithmembers 基礎上,如果成員沒有被引用,則會被移除

除此之外,Proguard 的通配符也比較讓人難懂,proguard-android-optimize.txt 中就使用到了很多通配符,我們來看一下它們之間的區別:

通配符

描述

<field>

匹配類中所有的字段

<method>

匹配類中所有的方法

init

匹配類中所有的構造方法

*

匹配任意長度字符,但不包含分隔符.,例如我們完成類名是:com.dream.androidreversedemo.MainActivity,使用 com.* 或者 com.dream.* 是無法匹配的,因為 * 無法匹配報名中的分隔符,正確的匹配方式是com.dream.*.*或者com.dream.androidreversedemo.*

**

匹配任意長度字符,包含分隔符.,上面匹配規則我們可以使用com.**或者com.dream.**來進行匹配

***

匹配任意參數類型。例如void set*(***)就能匹配傳入任意的參數類型,***get(*)就能匹配任意返回值的類型

...

匹配任意長度的任意類型參數,例如void test(...)就能匹配void test(String str)或者void test(int a,double b)這些方法

ok,學習了疑難語法,下面我們來一道練習題:保留實現了 com.dream.test.BaseJsonData 接口的類的所有信息不被混淆?

一個清晰的思路很重要,仔細分析一下:

1、首先我們要保證 com.dream.test.BaseJsonData 接口不被混淆

2、然後保證實現 com.dream.test.BaseJsonData 接口的類不被混淆

3、最後就是匹配類中所有的成員不被混淆,可以使用通配符 *

我們可以這麼寫:

# 保證  com.dream.test.BaseJsonData 接口不被混淆
-keep class com.dream.test.BaseJsonData
# 保證實現 com.dream.test.BaseJsonData 接口的類不被混淆
# 匹配類中所有的成員不被混淆,可以使用通配符 *
-keep class * implements com.dream.test.BaseJsonData{
    *;
}

2.6、自定義混淆規則

回到項目中,剛才打出的 APK 雖然已經成功混淆了,但是混淆的規則是按照 proguard-android-optimize.txt 中默認的規則來的,當然我們可以修改 proguard-android-optimize.txt 中的規則,但是這樣做會對本機上所有項目的混淆規則都生效,那麼有沒有什麼好的辦法只針對當前項目做混淆規則修改呢?

答:對 proguard-rules.pro 文件進行自定義混淆規則編寫

可以看到 android 閉包下 release 閉包的配置,實際上配置了兩個混淆文件,一個就是我們前面介紹的默認混淆規則,另外一個就是自定義混淆規則:

android {

    buildTypes {
        release {
            minifyEnabled true
            //proguard-android-optimize.txt:默認混淆規則 proguard-rules.pro:自定義混淆規則
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

proguard-rules.pro 文件位於 app 目錄下,接下來我們就使用剛才學習的 Proguard 相關知識對混淆規則做修改吧

這裡先列出我們要實現的目標:

1、對 MyFragment 類進行完全保留,不混淆其任何信息

2、對 MainActivity 類進行完全保留,不混淆其任何信息

3、對 Utils 中的方法進行保留,防止其被混淆或移除

4、對 NativeUtils 中的非 Native 方法進行保留,防止其被混淆或移除

5、對第三方庫進行保留,防止其被混淆或移除

實現如下:

# 對 MyFragment 類進行完全保留,不混淆其任何信息
-keep class com.dream.androidreversedemo.MyFragment{
    *;
}

# 對 MainActivity 類進行完全保留,不混淆其任何信息
-keep class com.dream.androidreversedemo.MainActivity{
    *;
}

# 對 Utils 中的方法進行保留,防止其被混淆或移除
-keep class com.dream.androidreversedemo.Utils{
    *;
}

# 對 NativeUtils 中的非 Native 方法進行保留,防止其被移除
-keepclassmembers class com.dream.androidreversedemo.NativeUtils{
    public static void methodNotNative();
}

# 對第三方庫進行保留,防止其被混淆或移除
-keep class com.dream.androidutils.*{
    *;
}

編寫好了自定義規則,現在我們重新打一個正式版的 APK 文件,然後在反編譯看效果:

可以看到我們自己編寫的類和引入的第三方庫中所有的的代碼都被保留了下來,不管是包名,類名都沒有被混淆

接着看一下具體的類:

MainActivity

Utils

NativeUtils

MyFragment

可以看到,上面的這些類基本上按照我們的要求保留了下來

ok,經過上面的例子,相信大家已經對 Proguard 的用法有相當不錯的理解了,那麼根據自己的業務需求去編寫混淆配置相信也不是什麼難事了吧?

關於混淆 APK 代碼就講這麼多,如果你還想了解更多關於 Proguard 的用法,可以參考這篇文章:juejin.cn/post/684490…

三、總結

本篇文章我們主要介紹了:

1、反編譯工具 jadx 的安裝與使用

jadx 相當於是 apktool + dex2jar + jd-gui 的結合體,既能反編譯代碼也能反編譯資源,一定程度上提高了我們的開發效率

2、混淆 APK 代碼

1、準備了一些類(自定義編寫的類,第三方庫的類)用於混淆後的效果驗證

2、在 app -> build.gradle -> android 閉包 -> release 閉包將 minifyEnabled 設為 true 開啟代碼混淆

3、使用 AndroidStudio 導航欄上 Generate Signed Bundle or APK 的方式打 release 包

4、在 app 的 build.gradle 文件中配置簽名文件,方便後續使用 gradle 命令或 gradle 可視化界面打包

5、逐行介紹了默認混淆規則文件 proguard-android-optimize.txt 中的配置

6、Proguard 疑難語法介紹

7、自定義混淆規則保留類(自定義編寫的類,第三方庫的類)不被混淆

好了,本篇文章到這裡就結束了,希望能給你帶來幫助

作者:sweetying
鏈接:https://juejin.cn/post/7168086915445424136
來源:稀土掘金