Android安全攻防戰,反編譯與混淆技術完全解析(下)

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/50451259
在上一篇文章當中,我們學習了Android程序反編譯方面的知識,包括反編譯代碼、反編譯資源、以及重新打包等內容。通過這些內容我們也能看出來,其實我們的程序並沒有那麼的安全。可能資源被反編譯影響還不是很大,重新打包又由於有簽名的保護導致很難被盜版,但代碼被反編譯就有可能會泄漏核心技術了,因此一款安全性高的程序最起碼要做到的一件事就是:對代碼進行混淆。

混淆代碼並不是讓代碼無法被反編譯,而是將代碼中的類、方法、變量等信息進行重命名,把它們改成一些毫無意義的名字。因爲對於我們而言可能Cellphone類的call()方法意味着很多信息,而A類的b()方法則沒有任何意義,但是對於計算機而言,它們都是平等的,計算機不會試圖去理解Cellphone是什麼意思,它只會按照設定好的邏輯來去執行這些代碼。所以說混淆代碼可以在不影響程序正常運行的前提下讓破解者很頭疼,從而大大提升了程序的安全性。

今天是我們Android安全攻防戰系列的下篇,本篇文章的內容建立在上篇的基礎之上,還沒有閱讀過的朋友可以先去參考 Android安全攻防戰,反編譯與混淆技術完全解析(上)


混淆

本篇文章中介紹的混淆技術都是基於Android Studio的,Eclipse的用法也基本類似,但是就不再爲Eclipse專門做講解了。

我們要建立一個Android Studio項目,並在項目中添加一些能夠幫助我們理解混淆知識的代碼。這裏我準備好了一些,我們將它們添加到Android Studio當中。
首先新建一個MyFragment類,代碼如下所示:

public class MyFragment extends Fragment {
    
    private String toastTip = "toast in MyFragment";

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

    public void methodWithGlobalVariable() {
        Toast.makeText(getActivity(), toastTip, Toast.LENGTH_SHORT).show();
    }

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

}

可以看到,MyFragment是繼承自Fragment的,並且MyFragment中有一個全局變量。onCreateView()方法是Fragment的生命週期函數,這個不用多說,在onCreateView()方法中又調用了methodWithGlobalVariable()和methodWithLocalVariable()方法,這兩個方法的內部分別引用了一個全局變量和一個局部變量。
接下來新建一個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);
    }

}

這是一個非常普通的工具類,沒有任何繼承關係。Utils中有兩個方法methodNormal()和methodUnused(),它們的內部邏輯都是一樣的,唯一的據別是稍後methodNormal()方法會被調用,而methodUnused()方法不會被調用。
下面再新建一個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);
    }

}

這個類中同樣有兩個方法,一個是native方法,一個是非native方法。

最後,修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity {

    private String toastTip = "toast in MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getSupportFragmentManager().beginTransaction().add(R.id.fragment, new MyFragment()).commit();
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                methodWithGlobalVariable();
                methodWithLocalVariable();
                Utils utils = new Utils();
                utils.methodNormal();
                NativeUtils.methodNative();
                NativeUtils.methodNotNative();
                Connector.getDatabase();
            }
        });
    }

    public void methodWithGlobalVariable() {
        Toast.makeText(MainActivity.this, toastTip, Toast.LENGTH_SHORT).show();
    }

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

}

可以看到,MainActivity和MyFragment類似,也是定義了methodWithGlobalVariable()和methodWithLocalVariable()這兩個方法,然後MainActivity對MyFragment進行了添加,並在Button的點擊事件裏面調用了自身的、Utils的、以及NativeUtils中的方法。注意調用native方法需要有相應的so庫實現,不然的話就會報UnsatisefiedLinkError,不過這裏其實我也並沒有真正的so庫實現,只是演示一下讓大家看看混淆結果。點擊事件的最後一行調用的是LitePal中的方法,因爲我們還要測試一下引用第三方Jar包的場景,到LitePal項目的主頁去下載最新的Jar包,然後放到libs目錄下即可。

完整的build.gradle內容如下所示:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.example.guolin.androidtest"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.2.0'
}

好的,到這裏準備工作就已經基本完成了,接下來我們就開始對代碼進行混淆吧。

混淆APK

在Android Studio當中混淆APK實在是太簡單了,藉助SDK中自帶的Proguard工具,只需要修改build.gradle中的一行配置即可。可以看到,現在build.gradle中minifyEnabled的值是false,這裏我們只需要把值改成true,打出來的APK包就會是混淆過的了。如下所示:

release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}

其中minifyEnabled用於設置是否啓用混淆,proguardFiles用於選定混淆配置文件。注意這裏是在release閉包內進行配置的,因此只有打出正式版的APK纔會進行混淆,Debug版的APK是不會混淆的。當然這也是非常合理的,因爲Debug版的APK文件我們只會用來內部測試,不用擔心被人破解。

那麼現在我們來打一個正式版的APK文件,在Android Studio導航欄中點擊Build->Generate Signed APK,然後選擇簽名文件並輸入密碼,如果沒有簽名文件就創建一個,最終點擊Finish完成打包,生成的APK文件會自動存放在app目錄下。除此之外也可以在build.gradle文件當中添加簽名文件配置,然後通過gradlew assembleRelease來打出一個正式版的APK文件,這種方式APK文件會自動存放在app/build/outputs/apk目錄下。

那麼現在已經得到了APK文件,接下來就用上篇文章中學到的反編譯知識來對這個文件進行反編譯吧,結果如下圖所示:

很明顯可以看出,我們的代碼混淆功能已經生效了。

下面我們嘗試來閱讀一下這個混淆過後的代碼,最頂層的包名結構主要分爲三部分,第一個a.a已經被混淆的面目全非了,但是可以猜測出這個包下是LitePal的所有代碼。第二個android.support可以猜測出是我們引用的android support庫的代碼,第三個com.example.guolin.androidtest則很明顯就是我們項目的主包名了,下面將裏面所有的類一個個打開看一下。

首先MainActivity中的代碼如下所示:

可以看到,MainActivity的類名是沒有混淆的,onCreate()方法也沒有被混淆,但是我們定義的方法、全局變量、局部變量都被混淆了。

再來打開下一個類NativeUtils,如下所示:

NativeUtils的類名沒有被混淆,其中聲明成native的方法也沒有被混淆,但是非native方法的方法名和局部變量都被混淆了。
接下來是a類的代碼,如下所示:

很明顯,這個是MainActivity中按鈕點擊事件的匿名類,在onClick()方法中的調用代碼雖然都被混淆了,但是調用順序是不會改變的,對照源代碼就可以看出哪一行是調用的什麼方法了。

再接下來是b類,代碼如下所示:

雖然被混淆的很嚴重,但是我們還是可以看出這個是MyFragment類。其中所有的方法名、全局變量、局部變量都被混淆了。

最後再來看下c類,代碼如下所示:

c類中只有一個a方法,從字符串的內容我們可以看出,這個是Utils類中的methodNormal()方法。
我爲什麼要創建這樣的一個項目呢?因爲從這幾個類當中很能看出一些問題,接下來我們就分析一下上面的混淆結果。

首先像Utils這樣的普通類肯定是會被混淆的,不管是類名、方法名還是變量都不會放過。除了混淆之外Utils類還說明了一個問題,就是minifyEnabled會對資源進行壓縮,因爲Utils類中我們明明定義了兩個方法,但是反編譯之後就只剩一個方法了,因爲另外一個方法沒有被調用,所以認爲是多餘的代碼,在打包的時候就給移除掉了。不僅僅是代碼,沒有被調用的資源同樣也會被移除掉,因此minifyEnabled除了混淆代碼之外,還可以起到壓縮APK包的作用。

接着看一下MyFragment,這個類也是混淆的比較徹底的,基本沒有任何保留。那有些朋友可能會有疑問,Fragment怎麼說也算是系統組件吧,就算普通方法名被混淆了,至少像onCreateView()這樣的生命週期方法不應該被混淆吧?其實生命週期方法會不會被混淆和我們使用Fragment的方式有關,比如在本項目中,我使用的是android.support.v4.app.Fragment,support-v4包下的,就連Fragment的源碼都被一起混淆了,因此生命週期方法當然也不例外了。但如果你使用的是android.app.Fragment,這就是調用手機系統中預編譯好的代碼了,很明顯我們的混淆無法影響到系統內置的代碼,因此這種情況下onCreateView()方法名就不會被混淆,但其它的方法以及變量仍然會被混淆。

接下來看一下MainActivity,同樣也是系統組件之一,但MainActivity的保留程度就比MyFragment好多了,至少像類名、生命週期方法名都沒有被混淆,這是爲什麼呢?根據我親身測試得出結論,凡是需要在AndroidManifest.xml中去註冊的所有類的類名以及從父類重寫的方法名都自動不會被混淆。因此,除了Activity之外,這份規則同樣也適用於Service、BroadcastReceiver和ContentProvider。

最後看一下NativeUtils類,這個類的類名也沒有被混淆,這是由於它有一個聲明成native的方法。只要一個類中有存在native方法,它的類名就不會被混淆,native方法的方法名也不會被混淆,因爲C++代碼要通過包名+類名+方法名來進行交互。 但是類中的別的代碼還是會被混淆的。

除此之外,第三方的Jar包都是會被混淆的,LitePal不管是包名還是類名還是方法名都被完完全全混淆掉了。
這些就是Android Studio打正式APK時默認的混淆規則。

那麼這些混淆規則是在哪裏定義的呢?其實就是剛纔在build.gradle的release閉包下配置的proguard-android.txt文件,這個文件存放於<Android SDK>/tools/proguard目錄下,我們打開來看一下:

# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html

-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose

# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
-dontoptimize
-dontpreverify
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.

-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.
# Dont 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.**

這個就是默認的混淆配置文件了,我們來一起逐行閱讀一下。

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

-dontskipnonpubliclibraryclasses 表示不跳過library中的非public的類。
-verbose 表示打印混淆的詳細信息。

-dontoptimize 表示不進行優化,建議使用此選項,因爲根據proguard-android-optimize.txt中的描述,優化可能會造成一些潛在風險,不能保證在所有版本的Dalvik上都正常運行。

-dontpreverify 表示不進行預校驗。這個預校驗是作用在Java平臺上的,Android平臺上不需要這項功能,去掉之後還可以加快混淆速度。

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

-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

表示不混淆上述聲明的兩個類,這兩個類我們基本也用不上,是接入Google原生的一些服務時使用的。

-keepclasseswithmembernames class * {
    native <methods>;
}

表示不混淆任何包含native方法的類的類名以及native方法名,這個和我們剛纔驗證的結果是一致的。

-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

表示不混淆任何一個View中的setXxx()和getXxx()方法,因爲屬性動畫需要有相應的setter和getter的方法實現,混淆了就無法工作了。

-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

表示不混淆Activity中參數是View的方法,因爲有這樣一種用法,在XML中配置android:onClick="buttonClick"屬性,當用戶點擊該按鈕時就會調用Activity中的buttonClick(View view)方法,如果這個方法被混淆的話就找不到了。

-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

表示不混淆枚舉中的values()和valueOf()方法,枚舉我用的非常少,這個就不評論了。

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

表示不混淆Parcelable實現類中的CREATOR字段,毫無疑問,CREATOR字段是絕對不能改變的,包括大小寫都不能變,不然整個Parcelable工作機制都會失敗。

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

表示不混淆R文件中的所有靜態字段,我們都知道R文件是通過字段來記錄每個資源的id的,字段名要是被混淆了,id也就找不着了。

-dontwarn android.support.** 表示對android.support包下的代碼不警告,因爲support包中有很多代碼都是在高版本中使用的,如果我們的項目指定的版本比較低在打包時就會給予警告。不過support包中所有的代碼都在版本兼容性上做足了判斷,因此不用擔心代碼會出問題,所以直接忽略警告就可以了。

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

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

關鍵字 描述
keep 保留類和類中的成員,防止它們被混淆或移除。
keepnames 保留類和類中的成員,防止它們被混淆,但當成員沒有被引用時會被移除。
keepclassmembers 只保留類中的成員,防止它們被混淆或移除。
keepclassmembernames 只保留類中的成員,防止它們被混淆,但當成員沒有被引用時會被移除。
keepclasseswithmembers 保留類和類中的成員,防止它們被混淆或移除,前提是指名的類中的成員必須存在,如果不存在則還是會混淆。
keepclasseswithmembernames 保留類和類中的成員,防止它們被混淆,但當成員沒有被引用時會被移除,前提是指名的類中的成員必須存在,如果不存在則還是會混淆。

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

通配符 描述
<field> 匹配類中的所有字段
<method> 匹配類中的所有方法
<init> 匹配類中的所有構造函數
* 匹配任意長度字符,但不含包名分隔符(.)。比如說我們的完整類名是com.example.test.MyActivity,使用com.*,或者com.exmaple.*都是無法匹配的,因爲*無法匹配包名中的分隔符,正確的匹配方式是com.exmaple.*.*,或者com.exmaple.test.*,這些都是可以的。但如果你不寫任何其它內容,只有一個*,那就表示匹配所有的東西。
** 匹配任意長度字符,並且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有內容,包括任意長度的子包。
*** 匹配任意參數類型。比如void set*(*)就能匹配任意傳入的參數類型,* get*()就能匹配任意返回值的類型。
匹配任意長度的任意類型參數。比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)這些方法。

雖說上面表格已經解釋的很詳細了,但是很多人對於keep和keepclasseswithmembers這兩個關鍵字的區別還是搞不懂。確實,它們之間用法有點太像了,我做了很多次試驗它們的結果都是相同的。其實唯一的區別就在於類中聲明的成員存不存在,我們還是通過一個例子來直接地看一下,先看keepclasseswithmember關鍵字:

-keepclasseswithmember class * {
    native <methods>;
}

這段代碼的意思其實很明顯,就是保留所有含有native方法的類的類名和native方法名,而如果某個類中沒有含有native方法,那就還是會被混淆。

但是如果改成keep關鍵字,結果會完全不一樣:

-keep class * {
    native <methods>;
}

使用keep關鍵字後,你會發現代碼中所有類的類名都不會被混淆了,因爲keep關鍵字看到class *就認爲應該將所有類名進行保留,而不會關心該類中是否含有native方法。當然這樣寫只會保證類名不會被混淆,類中的成員還是會被混淆的。

比較難懂的用法大概就這些吧,掌握了這些內容之後我們就能繼續前進了。

回到Android Studio項目當中,剛纔打出的APK雖然已經成功混淆了,但是混淆的規則都是按照proguard-android.txt中默認的規則來的,當然我們也可以修改proguard-android.txt中的規則,但是直接在proguard-android.txt中修改會對我們本機上所有項目的混淆規則都生效,那麼有沒有什麼辦法只針對當前項目的混淆規則做修改呢?當然是有辦法的了,你會發現任何一個Android Studio項目在app模塊目錄下都有一個proguard-rules.pro文件,這個文件就是用於讓我們編寫只適用於當前項目的混淆規則的,那麼接下來我們就利用剛纔學到的所有知識來對混淆規則做修改吧。

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

  • 對MyFragment類進行完全保留,不混淆其類名、方法名、以及變量名。
  • 對Utils類中的未調用方法進行保留,防止其被移除掉。
  • 對第三方庫進行保留,不混淆android-support庫,以及LitePal庫中的代碼。

下面我們就來逐一實現這些目標。

首先要對MyFragment類進行完全保留可以使用keep關鍵字,keep後聲明完整的類名,然後保留類中的所有內容可以使用*通配符實現,如下所示:

-keep class com.example.guolin.androidtest.MyFragment {
    *;
}

然後保留Utils類中的未調用方法可以使用keepclassmembers關鍵字,後跟Utils完整類名,然後在內部聲明未調用的方法,如下所示:

-keepclassmembers class com.example.guolin.androidtest.Utils {
    public void methodUnused();
}

最後不要混淆第三方庫,目前我們使用了兩種方式來引入第三方庫,一種是通過本地jar包引入的,一種是通過remote引入的,其實這兩種方式沒什麼區別,要保留代碼都可以使用**這種通配符來實現,如下所示:

-keep class org.litepal.** {
    *;
}

-keep class android.support.** {
    *;
}

所有內容都在這裏了,現在我們重新打一個正式版的APK文件,然後再反編譯看看效果:

可以看到,現在android-support包中所有代碼都被保留下來了,不管是包名、類名、還是方法名都沒有被混淆。LitePal中的代碼也是同樣的情況:

再來看下MyFragment中的代碼,如下所示:

可以看到,MyFragment中的代碼也沒有被混淆,按照我們的要求被完全保留下來了。
最後再來看一下Utils類中的代碼:

很明顯,Utils類並沒有被完全保留下來,類名還是被混淆了,methodNormal()方法也被混淆了,但是methodUnused()沒有被混淆,當然也沒有被移除,因爲我們的混淆配置生效了。

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

Progaurd的使用非常靈活,基本上能夠覆蓋你所能想到的所有業務邏輯。這裏再舉個例子,之前一直有人問我使用LitePal時的混淆配置怎麼寫,其實真的很簡單,LitePal作爲開源庫並不需要混淆,上面的配置已經演示瞭如何不混淆LitePal代碼,然後所有代碼中的Model是需要進行反射的,也不能混淆,那麼只需要這樣寫就行了:

-keep class * extends org.litepal.crud.DataSupport {
    *;
}

因爲LitePal中所有的Model都是應該繼承DataSupport類的,所以這裏我們將所有繼承自DataSupport的類都進行保留就可以了。

關於混淆APK的用法就講這麼多,如果你還想繼續瞭解關於Proguard的更多用法,可以參考官方文檔:

http://proguard.sourceforge.net/index.html#manual/usage.html

混淆Jar

在本篇文章的第二部分我想講一講混淆Jar包的內容,因爲APK不一定是我們交付的唯一產品。就比如說我自己,我在公司是負責寫SDK的,對於我來說交付出去的產品就是Jar包,而如果Jar包不混淆的話將會很容易就被別人反編譯出來,從而泄漏程序邏輯。

實際上Android對混淆Jar包的支持在很早之前就有了,不管你使用多老版本的SDK,都能在 <Android SDK>/tools目錄下找到proguard這個文件夾。然後打開裏面的bin目錄,你會看到如下文件:

其中proguardgui.bat文件是允許我們以圖形化的方式來對Jar包進行混淆的一個工具,今天我們就來講解一下這個工具的用法。

在開始講解這個工具之前,首先我們需要先準備一個Jar包,當然你從哪裏搞到一個Jar包都是可以的,不過這裏爲了和剛纔的混淆邏輯統一,我們就把本篇文章中的項目代碼打成一個Jar包吧。

Eclipse中導出Jar包的方法非常簡單,相信所有人都會,可是Android Studio當中就比較讓人頭疼了,因爲Android Studio並沒有提供一個專門用於導出Jar包的工具,因此我們只能自己動手了。

我們需要知道,任何一個Android Studio項目,只要編譯成功之後就會在項目模塊的build/intermediates/classes/debug目錄下生成代碼編譯過後的class文件,因此只需通過打包命令將這些class文件打包成Jar包就行了,打開cmd,切換到項目的根目錄,然後輸入如下命令:

jar -cvf androidtest.jar -C app/build/intermediates/classes/debug .

在項目的根目錄下就會生成androidtest.jar這個文件,這樣我們就把Jar包準備好了。
現在雙擊proguardgui.bat打開混淆工具,如果是Mac或Ubuntu系統則使用sh proguardgui.sh命令打開混淆工具,界面如下圖所示:

其實從主界面上我們就能看出,這個Proguard工具支持Shrinking、Optimization、Obfuscation、Preverification四項操作,在左側的側邊欄上也能看到相應的這些選項。Proguard的工作機制仍然還是要依賴於配置文件,當然我們也可以通過proguardgui工具來生成配置文件,不過由於配置選項太多了,每個都去一一設置太複雜,而且大多數還都是我們用不到的配置。因此最簡單的方式就是直接拿現有的配置文件,然後再做些修改就行了。

那麼我們從<Android SDK>/tools/proguard目錄下將proguard-android.txt文件複製一份出來,然後點擊主界面上的Load configuration按鈕來加載複製出來的這份proguard-android.txt文件,完成後點擊Next將進入Input/Output界面。

Input/Output界面是用於導入要混淆的Jar包、配置混淆後文件的輸出路徑、以及導入該Jar包所依賴的所有其它Jar包的。我們要混淆的當然就是androidtest.jar這個文件,那麼這個Jar包又依賴了哪些Jar包呢?這裏就需要整理一下了。

  • 首先我們寫的都是Java代碼,Java代碼的運行要基於Jre基礎之上,沒有Jre計算機將無法識別Java的語法,因此第一個要依賴的就是Jre的rt.jar。
  • 然後由於我們導出的Jar包中有Android相關的代碼,比如Activity、Fragment等,因此還需要添加Android的編譯庫,android.jar。
  • 除此之外,我們使用的AppCompatActivity和Fragment分別來自於appcompat-v7包和support-v4包,那麼這兩個Jar包也是需要引入的。
  • 最後就是代碼中還引入了litepal-1.3.1.jar。

整理清楚了之後我們就來一個個添加,Input/Output有上下兩個操作界面,上面是用於導入要混淆的Jar包和配置混淆後文件的輸出路徑的,下面則是導入該Jar包所依賴的所有其它Jar包的,全部導入後結果如下圖所示:

這些依賴的Jar包所存在的路徑每臺電腦都不一樣,你所需要做的就是在你自己的電腦上成功找到這些依賴的Jar包並導入即可。

不過細心的朋友可能會發現,我在上面整理出了五個依賴的Jar包,但是在圖中卻添加了六個。這是我在寫這篇文章時碰到的一個新的坑,也是定位了好久才解決的,我覺得有必要重點提一下。由於我平時混淆Jar包時裏面很少會有Activity,所以沒遇到過這個問題,但是本篇文章中的演示Jar包中不僅包含了Activty,還是繼承自AppCompatActivity的。而AppCompatActivity的繼承結構並不簡單,如下圖所示:

其中AppCompatActivity是在appcompat-v7包中的,它的父類FragmentActivity是在support-v4包中的,這兩個包我們都已經添加依賴了。但是FragmentActivity的父類就坑爹了,如果你去看BaseFragmentActivityHoneycomb和BaseFragmentActivityDonut這兩個類的源碼,你會發現它們都是在support-v4包中的:


可是如果你去support-v4的Jar包中找一下,你會發現壓根就沒有這兩個類,所以我當時一直混淆報錯就是因爲這兩個類不存在,繼承結構在這裏斷掉了。而這兩個類其實被規整到了另外一個internal的Jar包中,所以當你要混淆的Jar包中有Activity,並且還是繼承自AppCompatActivity或FragmentActivity的話,那麼就一定要記得導入這個internal Jar包的依賴,如下圖所示:

接下來點擊Next進入Shrink界面,這個界面沒什麼需要配置的東西,但記得要將Shrink選項鉤掉,因爲我們這個Jar包是獨立存在的,沒有任何項目引用,如果鉤中Shrink選項的話就會認爲我們所有的代碼都是無用的,從而把所有代碼全壓縮掉,導出一個空的Jar包。

繼續點擊Next進入Obfuscation界面,在這裏可以添加一些混淆的邏輯,和混淆APK時不同的是,這裏並不會自動幫我們排除混淆四大組件,因此必須要手動聲明一下才行。點擊最下方的Add按鈕,然後在彈出的界面上編寫排除邏輯,如下圖所示:

很簡單,就是在繼承那一欄寫上android.app.Activity就行了,其它的組件原理也相同。
繼續點擊Next進入Optimiazation界面,不用修改任何東西,因爲我們本身就不啓用Optimization功能。繼續點擊Next進入Information界面,也不用修改任何東西,因爲我們也不啓用Preverification功能。

接着點擊Next,進入Process界面,在這裏可以通過點擊View configuration按鈕來預覽一下目前我們的混淆配置文件,內容如下所示:

-injars /Users/guolin/AndroidStudioProjects/AndroidTest/androidtest.jar
-outjars /Users/guolin/androidtest_obfuscated.jar

-libraryjars /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar
-libraryjars /Users/guolin/Library/Android/sdk/platforms/android-23/android.jar
-libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.2.0/jars/classes.jar
-libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/build/intermediates/exploded-aar/com.android.support/support-v4/23.2.0/jars/classes.jar
-libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/build/intermediates/exploded-aar/com.android.support/support-v4/23.2.0/jars/libs/internal_impl-23.2.0.jar
-libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/libs/litepal-1.3.1.jar

-dontshrink
-dontoptimize
-dontusemixedcaseclassnames
-keepattributes *Annotation*
-dontpreverify
-verbose
-dontwarn android.support.**


-keep public class com.google.vending.licensing.ILicensingService

-keep public class com.android.vending.licensing.ILicensingService

# 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);
}

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

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

-keep class * extends android.app.Activity

-keep class * extends android.app.Service

-keep class * extends android.content.BroadcastReceiver

-keep class * extends android.content.ContentProvider

# Also keep - Enumerations. Keep the special static methods that are required in
# enumeration classes.
-keepclassmembers enum  * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# Keep names - Native method names. Keep all native class/method names.
-keepclasseswithmembers,allowshrinking class * {
    native <methods>;
}

恩,由此可見其實GUI工具只是給我們提供了一個方便操作的平臺,背後工作的原理還是通過這些配置來實現的,相信上面的配置內容大家應該都能看得懂了吧。

接下來我們還可以點擊Save configuration按鈕來保存一下當前的配置文件,這樣下次混淆的時候就可以直接Load進來而不用修改任何東西了。

最後點擊Process!按鈕來開始混淆處理,中間會提示一大堆的Note信息,我們不用理會,只要看到最終顯示Processing completed successfully,就說明混淆Jar包已經成功了,如下圖所示:

混淆後的文件我將它配置在了/Users/guolin/androidtest_obfuscated.jar這裏,如果反編譯一下這個文件,你會發現和剛纔反編譯APK得到的結果是差不多的:MainActivity的類名以及從父類繼承的方法名不會被混淆,NativeUtils的類名和其中的native方法名不會被混淆,Utils的methodUnsed方法不會被移除,因爲我們禁用了Shrink功能,其餘的代碼都會被混淆。由於結果實在是太相似了,我就不再貼圖了,參考本篇文章第一部分的截圖即可。


好了,本篇文章的內容就到這裏,混淆技術掌握這麼多相信已經足夠大家在平時的工作當中使用了。當然除了使用混淆之外,還有一些加固軟件也能提升程序的安全性,不過這些軟件都是第三方的,並非Google原生支持,所以我就不進行講解和推薦了。那麼我們Android安全攻防戰系列的文章到此結束,感謝大家有耐心看到最後。

關注我的技術公衆號,每天都有優質技術文章推送。關注我的娛樂公衆號,工作、學習累了的時候放鬆一下自己。

微信掃一掃下方二維碼即可關注:

        

發佈了120 篇原創文章 · 獲贊 2萬+ · 訪問量 1031萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章