本文來自於騰訊Bugly公眾號(weixinBugly),未經作者同意,請勿轉載,原文地址:https://mp.weixin.qq.com/s/WmJyiA3fDNriw5qXuoA9MA
作者:lilycai
本文主要講述了代碼混淆和資源混淆的原理,Studio默認的混淆方案,混淆的參數,以及如何對Apk進行代碼混淆(自定義混淆文件)和資源混淆(結合微信混淆和美團混淆兩種方案),避免Apk被逆向。
為什么要混淆
我們的apk在打包發布之前,都要進行混淆處理來避免源代碼和資源文件被小白用戶通過反編譯拿到。未混淆代碼的反編譯操作非常簡單,網上有很多教程, 也可以通過使用Android Studio自帶的apk分析工具(Build---Analyze APK)直接看到未混淆Apk的源代碼和原始的資源文件。對比圖如下,從圖中可以看到未混淆apk所有的代碼都一目了然,隨便改改資源和代碼,就能變成一個新的apk。為了避免我們的勞動成果被竊取,也避免出現安全漏洞和隱患,此篇文章從混淆的原理到代碼和資源文件的混淆實踐做一下闡述。
混淆前:
混淆后:
混淆的原理
Java 是一種跨平台、解釋型語言,Java 源代碼編譯成的class文件中有大量包含語義的變量名、方法名的信息,很容易被反編譯為Java 源代碼。為了防止這種現象,我們可以對Java字節碼進行混淆。混淆不僅能將代碼中的類名、字段、方法名變為無意義的名稱,保護代碼,也由於移除無用的類、方法,並使用簡短名稱對類、字段、方法進行重命名縮小了程序的size。
ProGuard由shrink、optimize、obfuscate和preverify四個步驟組成,每個步驟都是可選的,需要哪些步驟都可以在腳本中配置。 參見ProGuard官方介紹。
- 壓縮(Shrink): 偵測並移除代碼中無用的類、字段、方法、和特性(Attribute)。
- 優化(Optimize): 分析和優化字節碼。
- 混淆(Obfuscate): 使用a、b、c、d這樣簡短而無意義的名稱,對類、字段和方法進行重命名。
上面三個步驟使代碼size更小,更高效,也更難被逆向工程。
- 預檢(Preveirfy): 在java平台上對處理后的代碼進行預檢。
混淆流程圖如下:
Proguard讀入input jars(or wars,zips or directories),經過四個步驟生成處理之后的jars(or wars,ears,zips or directories),Optimization步驟可選擇多次進行。
為了確定哪些代碼應該被保留,哪些代碼應該被移除或混淆,需要確定一個或多個Entry Point。Entry Point經常是帶有main methods,applets,midlets的classes,它們在混淆過程中會被保留。我們來看一下Proguard的幾個步驟如何處理Entry Points。
- 在壓縮階段,Proguard從上述Entry Points開始遍歷搜索哪些類和類成員被使用。其他沒有被使用的類和類成員會移除。
- 在優化階段,Proguard進一步設置非Entry Point的類和方法為private、static和final來進行優化,不使用的參數會被移除,某些方法會被標記被內聯。
- 在混淆階段,Proguard重命名非Entry Points的類和類成員。
- 預檢階段是唯一沒有觸及Entry Points的階段。
Android Studio 默認的混淆方案及字段解讀
開啟混淆
參見google官方文檔壓縮代碼和資源
要通過Proguard啟動代碼壓縮,在build.gradle文件內相應的構建類型中添加minifyEnabled true。
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
...
}
除了 minifyEnabled 屬性外,還有用於定義 ProGuard 規則的 proguardFiles 屬性:
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
google的官方文檔介紹:
getDefaultProguardFile('proguard-android.txt') 方法可從 Android SDK tools/proguard/ 文件夾獲取默認的 ProGuard 設置。要想做進一步的代碼壓縮,請嘗試使用位於同一位置的 proguard-android-optimize.txt 文件。它包括相同的 ProGuard 規則,但還包括其他在字節碼一級(方法內和方法間)執行分析的優化,以進一步減小 APK 大小和幫助提高其運行速度。
proguard-rules.pro 文件用於添加自定義 ProGuard 規則。默認情況下,該文件位於模塊根目錄(build.gradle 文件旁),內容為空。
通過試驗,gradle 2.2之后,defaultProguardFile沒有使用sdk目錄下的proguard-android.txt,而是使用了gradle自帶的proguard-android.txt,不同的gradle版本帶有不同的默認混淆文件,在項目根目錄的build/intermediates/proguard-files/proguard-android.txt-2.3.3(筆者用的gradle版本)即為gradle自帶的混淆文件。在proguard-android.txt-2.3.3文件中也寫有說明,gradle 2.2之后自帶混淆文件:
Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
will be ignored by new version of the Android plugin for Gradle.
構建輸出
構建時Proguard都會輸出下列文件:
(1)dump.txt --- 說明APK中所有類文件的內部結構
(2)mapping.txt --- 提供原始與混淆過的類、方法和字段名稱之間的轉換
(3)seeds.txt --- 列出未進行混淆的類和成員
(4)usage.txt --- 列出從APK移除的代碼
這些文件保存在
解碼混淆過的堆疊追蹤
使用混淆后,一定要保存好mapping文件,程序csh時通過腳本進行解碼。
retrace工具位於
retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]
例如mac平台下:
retrace.sh -verbose mapping.txt obfuscated_trace.txt
默認的混淆方案及字段解讀
下面結合默認混淆文件中的內容來解釋混淆的參數: 參見Proguard官方字段解讀
不使用大小寫混寫類名
-dontusemixedcaseclassnames
默認情況下混淆的類名可以包含大小寫字符的混合。
不忽略公共類庫
-dontskipnonpubliclibraryclasses
指定不去忽略非public的library classes。從Proguard 4.5開始,是默認的設置。
-dontoptimize
-dontpreverify
默認optimize和preverify選項是關閉的,因為Android的dex並不像Java虛擬機需要optimize(優化)和previrify(預檢)兩個步驟。
指定哪個屬性不要混淆,可一次指定多個屬性
-keepattributes [attribute_filter]
通常Exceptions, Signature, Deprecated, SourceFile, SourceDir, LineNumberTable, LocalVariableTable, LocalVariableTypeTable, Synthetic, EnclosingMethod, RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations, and AnnotationDefault屬性需要被保留,根據項目具體使用情況保留。
這里需要特別注意的一點是,gradle默認的keepattributes屬性不全,只保留了Annotation,Signature,InnerClasses,EnclosingMethod,為了混淆之后定位csh代碼方便,我們需要在proguard_rules.pro中手動添加拋出異常時保留代碼行號,並且重命名拋出異常時的文件名稱,這樣能方便定位問題:
拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable
重命名拋出異常時的文件名稱
-renamesourcefileattribute SourceFile
keep選項非常重要,keep指定了哪些類,哪些方法不被混淆,從而保證了程序的正常運行。官方的keep用法有6種:
Keep | From being removed or renamed | From being renamed |
---|---|---|
Classes and class members | keep | keepnames |
Class members only | keepclassmembers | keepclassmembernames |
Classes and class members | keepclasseswithmembers | keepclasseswithmembernames |
左邊不帶names的選項為From being removed or renames,即不會被移除或重命名,即使類或類成員未被使用。帶有names的選項為From being renamed,不會被重命名,如果是無用的類或類成員,會被移除。
(1)-keep(names)選項 指定類和類成員(變量和方法)不被混淆
-keep [,modifier,...] class_specification
eg.
指定類名不被改變
-keep public class com.google.vending.licensing.ILicensingService
指定使用了Keep注解的類和類成員都不被改變
-keep @android.support.annotation.Keep class * {*;}
關於Keep注解的解釋參見文末參考鏈接
(2)-keepclassmembers(names) 指定類成員不被混淆,類名會被混淆
-keepclassmembers [,modifier,...] class_specification
eg.keep setters in views 使得animations仍然能夠工作
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
(3)-keepclasseswithmembers(names) 指定類和類成員都不被混淆
-keepclasseswithmembers [,modifier,...] class_specification
eg.包含native方法的類名和native方法都不能被混淆,如果native方法未被調用,則被移除。由於native方法與對應so庫中的方法名稱對應,方法名被混淆會導致調用出現問題,所以native方法不能被混淆。
-keepclasseswithmembernames class * {
native <methods>;
}
通用Options:
(1)-verbose 打印混淆詳細信息
(2)-dontnote選項:指定不去輸出打印該類產生的錯誤或遺漏
-dontnote com.android.vending.licensing.ILicensingService
-dontnote android.support.**
(3)-dontwarn選項:指定不去warn unresolved references和其他重要的problem
-dontwarn android.support.**
如上面(2)(3)所示,android.support的libraries需要保留
至此,gradle自帶的proguard-android.txt文件相關字段已解析完畢。下面將介紹我們自定義的proguard-rules.pro文件需要添加什么參數。
自定義混淆文件
一般而言,我們會定義我們自己的proguard-rules.pro,下面列出自定義的一個proguard-rules.pro供大家參考。在看自定義的混淆文件之前,先講解一下Filters和assumenosideeffects,以便更好地理解下面的指令。
(1)Filters
? matches any single character in a name.(匹配一個字符)
* matches any part of a name not containing the directory separator.(匹配一個名字,除了目錄分隔符外的任意部分)
** matches any part of a name, possibly containing any number of directory separators.(匹配任意名,可能包含任意路徑分隔符)
! exclude
<field> 匹配類中的所有字段
<method> 匹配類中所有的方法
<init> 匹配類中所有的構造函數
eg.
-keep class com.lily.test.** 本包和所包含子包下的類名都保持
-keep class com.lily.test.* 保持該包下的類名
-keep class com.lily.test.** {*;} 保持包和子包的類名和里面的內容均不被混淆
(2)-assumenosideeffects 指令: 下文會用在android log的移除上
assumeosideeffects是Optimization過程中的選項,所以為保證指令的有效,需要開啟optimization。這個指令的含義是Proguard會在optimization過程中刪除對這些方法的調用,需要注意:Only use this option if you know what you're doing!
下面是自定義混淆文件的一個范例,四大組件,native方法,反射用到的類,一些引入的第三方庫等都不能進行混淆:
# 代碼混淆壓縮比,在0~7之間
-optimizationpasses 5
# 混合時不使用大小寫混合,混合后的類名為小寫
-dontusemixedcaseclassnames
# 指定不去忽略非公共庫的類
-dontskipnonpubliclibraryclasses
# 不做預校驗,preverify是proguard的四個步驟之一,Android不需要preverify,去掉這一步能夠加快混淆速度。
-dontpreverify
-verbose
#google推薦算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 避免混淆Annotation、內部類、泛型、匿名類
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod
# 重命名拋出異常時的文件名稱
-renamesourcefileattribute SourceFile
# 拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable
# 處理support包
-dontnote android.support.**
-dontwarn android.support.**
# 保留四大組件,自定義的Application等這些類不被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService
# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留枚舉類不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留Parcelable序列化類不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#第三方jar包不被混淆
-keep class com.github.test.** {*;}
#保留自定義的Test類和類成員不被混淆
-keep class com.lily.Test {*;}
#保留自定義的xlog文件夾下面的類、類成員和方法不被混淆
-keep class com.test.xlog.** {
<fields>;
<methods>;
}
#assume no side effects:刪除android.util.Log輸出的日志
-assumenosideeffects class android.util.Log {
public static *** v(...);
public static *** d(...);
public static *** i(...);
public static *** w(...);
public static *** e(...);
}
#保留Keep注解的類名和方法
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
@android.support.annotation.Keep *;
}
資源文件的混淆:
上面講述了如何進行代碼混淆,再來講講如何對資源文件進行混淆。對資源文件進行混淆操作本質上是通過修改resources.arsc(參見文末鏈接詳見resources.arsc作用及文件格式)。現針對兩種資源混淆方案進行簡要說明。第一種是微信的資源混淆方案,第二種是美團的資源混淆方案,兩篇文章中都對原理進行了詳細的闡述。
(1)微信的資源混淆方案:
微信的資源混淆是自己做了一個安裝包解壓並且用7z極限壓縮打包器,修改的內容也是resources.arsc,優點是可以最大地混淆,不依賴源碼與編譯過程,無需在編譯過程中修改源文件(java、xml、資源文件),無需改變Android打包流程。整體的流程如下:
使用微信的資源混淆方案有兩種方法,第一種方式為修改gradle,第二種方式為直接使用命令行。下圖為使用命令行最簡單的方法生成資源混淆的apk,下載github工程后,進入tool_output文件夾,試驗的apk為test.apk
java -jar AndResGuard-cli-1.2.3.jar test.apk
混淆過程中會輸出log,混淆后會出現和apk同名的文件夾,里面包含了混淆后mapping的對應文件,新簽名打包的apk和混淆后的資源文件目錄。如下圖所示:
混淆前資源文件:
混淆后資源文件:
可以看到資源文件的路徑以及文件名都被混淆了。
(2)美團的資源混淆方案:
采用更改AAPT(Android Asset Packaging Tool)(參見文末鏈接詳細解讀AAPT)源碼的方式,參考了Proguard Obfuscator,對APK中資源文件名使用簡短無意義名稱進行替換,如下面代碼所示,在AAPT生成resources.arsc和*.ap*時把資源文件的名稱進行替換。下面是美團修改后的Resource.cpp文件
static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets,
ResourceTable* table,
const sp<ResourceTypeSet>& set,
const char* resType)
{
String8 type8(resType);
String16 type16(resType);
bool hasErrors = false;
ResourceDirIterator it(set, String8(resType));
ssize_t res;
while ((res=it.next()) == NO_ERROR) {
if (bundle->getVerbose()) {
printf(" (new resource id %s from %s)\n",
it.getBaseName().string(), it.getFile()->getPrintableSource().string());
}
String16 baseName(it.getBaseName());
const char16_t* str = baseName.string();
const char16_t* const end = str + baseName.size();
while (str < end) {
if (!((*str >= 'a' && *str <= 'z')
|| (*str >= '0' && *str <= '9')
|| *str == '_' || *str == '.')) {
fprintf(stderr, "%s: Invalid file name: must contain only [a-z0-9_.]\n",
it.getPath().string());
hasErrors = true;
}
str++;
}
String8 resPath = it.getPath();
resPath.convertToResPath();
String8 obfuscationName;
String8 obfuscationPath = getObfuscationName(resPath, obfuscationName);
table->addEntry(SourcePos(it.getPath(), 0), String16(assets->getPackage()),
type16,
baseName, // String16(obfuscationName),
String16(obfuscationPath), // resPath
NULL,
&it.getParams());
assets->addResource(it.getLeafName(), obfuscationPath/*resPath*/, it.getFile(), type8);
}
return hasErrors ? UNKNOWN_ERROR : NO_ERROR;
}
修改的部分在:
String8 obfuscationName;
String8 obfuscationPath = getObfuscationName(resPath, obfuscationName);
assets->addResource(it.getLeafName(), obfuscationPath/*resPath*/, it.getFile(), type8);
混淆時常見的問題解決
參見官方問題解答
參考文獻:
1、Android 項目的代碼混淆,Android proguard 使用說明
2、google 混淆官方文檔
3、混淆官方網址
4、Android混淆快速配置之@Keep
5、Android resources.arsc文件格式及逆向修改res路徑思路
6、Android應用程序資源的編譯和打包過程分析(AAPT)
更多精彩內容歡迎關注騰訊 Bugly的微信公眾賬號:
騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合並功能幫助開發同學把每天上報的數千條 Crash 根據根因合並分類,每日日報會列出影響用戶數最多的崩潰,精准定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發布后快速的了解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!