我們都希望自己的代碼足夠"安全",即使別人反編譯了我們的應(yīng)用噪裕,他們也很難從反編譯的代碼中找出漏洞蹲盘。這時候我們就依賴編譯器的混淆功能,混淆會將大部分(下面會解釋為什么是大部分)類和成員的名稱重命名為沒有意義的短名膳音,例如aa
召衔、ab
這種,此時的代碼基本沒有可讀性祭陷,也就不容易找到漏洞苍凛。想要從代碼的角度分析混淆做了什么,我們就得查看混淆后的代碼兵志,本文通過反編譯來分析混淆前后的代碼有何不同醇蝴。
一、混淆與反編譯
1.1 混淆想罕、縮減與優(yōu)化應(yīng)用
混淆并不是單獨(dú)使用的悠栓,當(dāng)你啟用混淆時,編譯器還會同時縮減和優(yōu)化你的應(yīng)用按价,以盡可能地減小應(yīng)用的大小惭适。當(dāng)發(fā)布應(yīng)用的release版本時就需要開啟混淆,在build.gradle中添加以下代碼即可啟用楼镐。
android {
buildTypes {
release { // 用于應(yīng)用的release版本
// 啟用 代碼縮減癞志、混淆、代碼優(yōu)化
minifyEnabled true
// 資源縮減
shrinkResources true
// 這里引入了Android插件自帶的混淆規(guī)則
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
...
}
1.1.1 minifyEnabled
minifyEnabled為true表示啟用代碼縮減框产、混淆處理和優(yōu)化凄杯。
-
代碼縮減:也稱"搖樹優(yōu)化"错洁,指從應(yīng)用及其依賴庫中檢測并安全地移除未使用的類、字段戒突、方法和屬性屯碴。如果應(yīng)用僅使用某個依賴庫的少數(shù)幾個 API,縮減功能可以識別應(yīng)用未使用的庫代碼并僅從應(yīng)用中移除這部分代碼妖谴。
混淆處理:通過縮短類和成員變量的名稱窿锉,減小dex包的大小。寫代碼時膝舅,我們?yōu)榱舜a的可讀性嗡载,會為類、方法和變量定義通俗的名稱仍稀。例如
boolean isDataLoadFinished
洼滚,一看就知道是判斷數(shù)據(jù)是否加載完畢的,但是混淆之后就會變?yōu)轭愃?code>boolean aa這樣的名稱技潘。
當(dāng)然并不是所有的類和成員都能被混淆遥巴,上方配置的第3項中的proguard-rules.pro是用戶自定義的混淆規(guī)則,用戶可以自行決定哪些類不該被混淆享幽。例如反射或自定義View這些需要用到原始類名或者方法名的類和成員就不該被混淆铲掐,之后會詳細(xì)介紹如何自定義混淆規(guī)則。代碼優(yōu)化:檢查并重寫代碼值桩。例如摆霉,如果檢測到
if/else
語句中的else{...}
代碼塊從未被執(zhí)行,那么編譯器會移除該部分代碼奔坟,以進(jìn)一步縮減dex包的大行啊;或者檢測到某個方法只被調(diào)用一次咳秉,可能會將該方法移除并內(nèi)嵌在調(diào)用的地方婉支。
1.1.2 shrinkResources
資源縮減:從封裝應(yīng)用中移除不使用的資源,包括依賴庫中不使用的資源澜建。此功能可與代碼縮減功能結(jié)合使用向挖,這樣一來,移除不使用的代碼后炕舵,也可以安全地移除不再引用的所有資源户誓。
不過這并非萬無一失,我同事之前遇到過這樣一種情況:我們的應(yīng)用分為淺色模式與深色模式幕侠,淺色模式下的資源名為xxx
,深色模式下的資源名為xxx_dark
碍彭。當(dāng)從淺色模式切換至深色模式時晤硕,代碼沒有直接引用深色模式下的資源圖片悼潭,而是在資源名xxx
后面拼接_dark
,修改資源名字達(dá)到替換圖片的效果舞箍。但是編譯器在資源縮減階段發(fā)現(xiàn)xxx_dark
沒有被引用舰褪,就將所有深色模式的圖刪掉了。
此時我們只能自定義資源保留的規(guī)則:修改res/raw/keep.xml
疏橄,在 tools:keep 屬性中指定每個要保留的資源占拍,在 tools:discard 屬性中指定每個要舍棄的資源。這兩個屬性都接受以逗號分隔的資源名稱列表捎迫,可以將星號字符用作通配符晃酒。如下所示,指定保留以_dark
結(jié)尾的資源窄绒。
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/*_dark />
1.2 反編譯APK
1.2.1 反編譯步驟
查看反編譯后的代碼主要依賴dex2jar和jd-gui這2個工具贝次,具體步驟也很簡單:
① 將APK后綴改為rar,解壓得到dex文件彰导。
② 通過dex2jar將dex文件轉(zhuǎn)為jar文件蛔翅。以Windows系統(tǒng)為例,下載dex-tools后解壓位谋,將dex文件復(fù)制到該目錄下山析,執(zhí)行d2j-dex2jar.bat classes.dex
命令即可得到classes-dex2jar.jar文件。不過網(wǎng)絡(luò)上的dex-tools不一定是最新版掏父,最好在github下載源碼笋轨,編譯成功后執(zhí)行gradlew assemble
,執(zhí)行完畢后可在dex2jar-2.x\dex-tools\build\distributions
目錄下得到dex-tools损同。
③ 下載jd-gui翩腐,解壓后打開jd-gui.exe,選擇jar文件查看源碼即可膏燃。
1.2.2 反編譯實(shí)踐
新建一個測試混淆的項目茂卦,由于AndroidStudio打包默認(rèn)是debug包,先啟用debug包的混淆组哩。
buildTypes {
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
release {
......
}
}
項目中添加以下文件等龙。
① MainActivity和SecondActivity
② 自定義視圖TestView(繼承自View)
③ WebView交互類CommonJSApi
④ 工具類SizeUtils
此時項目使用的是默認(rèn)的混淆規(guī)則,來看一下反編譯后的項目結(jié)構(gòu)伶贰,我們發(fā)現(xiàn)MainActivity蛛砰、SecondActivity和TestView還保留著原本的名字,而CommonJSApi和SizeUtils的類名已經(jīng)被混淆成了a和b黍衙。
下面來看每個類混淆前后的具體代碼泥畅。
① MainActivity
原始代碼如下,定義了2個沒有使用的變量mUnuesed和mUnUsedString琅翻,onCreate(...)
中有一段if/else
代碼位仁,很明顯else{}
代碼塊中的內(nèi)容不會被執(zhí)行柑贞。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private int mUnuesed;
private String mUnUsedString = "hahaha";
private boolean mShouldShowDensity = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_test).setOnClickListener(this);
if (mShouldShowDensity) {
Log.e("TAG", "density: " + SizeUtils.getDensity(this));
} else {
Log.e("TAG", "nothing to show");
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_test:
startActivity(new Intent(this, SecondActivity.class));
break;
}
}
}
混淆后代碼如下,沒有使用到的變量mUnuesed和mUnUsedString已經(jīng)被刪除聂抢,但是不可能被執(zhí)行到的else{}
代碼塊卻并沒有被刪除钧嘶。帶著疑惑,我改成了if (true) {...}
琳疏,再次打包反編譯有决,發(fā)現(xiàn)else{}
代碼塊被刪除了......
希望有朋友交流一下這個優(yōu)化規(guī)則的觸發(fā)條件,非要寫成if (true)
這樣才進(jìn)行代碼優(yōu)化的話空盼,這就顯得有點(diǎn)雞肋书幕。
public class MainActivity extends d implements View.OnClickListener {
public boolean s = true;
public void onClick(View paramView) {
if (paramView.getId() == 2131165250)
startActivity(new Intent((Context)this, SecondActivity.class));
}
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
setContentView(2131361820);
findViewById(2131165250).setOnClickListener(this);
if (this.s) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("density: ");
stringBuilder.append(b.a((Activity)this));
Log.e("TAG", stringBuilder.toString());
} else {
Log.e("TAG", "nothing to show"); // 為何沒有被刪除?
}
}
}
② 自定義視圖TestView
這個自定義View比較簡單我注,原始代碼就不貼了按咒,直接看混淆后的代碼,只是變量名和方法名被修改了但骨。
public class TestView extends View {
public Paint b;
public int c; (原始: private int mWidth;)
public int d; (原始: private int mHeight;)
public TestView(Context paramContext) {
this(paramContext, null);
}
public TestView(Context paramContext, AttributeSet paramAttributeSet) {
this(paramContext, paramAttributeSet, 0);
}
public TestView(Context paramContext, AttributeSet paramAttributeSet, int paramInt) {
super(paramContext, paramAttributeSet, paramInt);
a();
}
public final void a() {
this.b = new Paint(1);
this.b.setStyle(Paint.Style.FILL_AND_STROKE);
this.b.setColor(-16776961);
}
public void onDraw(Canvas paramCanvas) {
super.onDraw(paramCanvas);
paramCanvas.drawRect(0.0F, 0.0F, this.c, this.d, this.b);
}
public void onSizeChanged(int paramInt1, int paramInt2, int paramInt3, int paramInt4) {
super.onSizeChanged(paramInt1, paramInt2, paramInt3, paramInt4);
this.c = paramInt1;
this.d = paramInt2;
}
}
③ WebView交互類CommonJSApi
原始代碼如下,構(gòu)造函數(shù)中雖然傳入了Context變量掠抬,但是并未被使用两波。
還有1個被@JavascriptInterface
注解的getVersion()
方法。
public class CommonJSApi {
public CommonJSApi(Context context) {}
@JavascriptInterface
public String getVersion() {
return "1";
}
}
混淆后的代碼如下劣坊,由于構(gòu)造函數(shù)中的Context沒有用到局冰,因此我們自己添加的構(gòu)造函數(shù)被刪除了模蜡,可以直接使用原本的無參構(gòu)造函數(shù)谆焊。而getVersion()
的方法名沒有被更改,因?yàn)镴S會通過方法名進(jìn)行調(diào)用烫罩。
public class a {
@JavascriptInterface
public String getVersion() {
return "1";
}
}
④ 工具類SizeUtils
原始代碼如下,getDensity(Activity activity)
在MainActivity中被使用過时甚,而getString()
沒有被用到過。
public class SizeUtils {
public static float getDensity(Activity activity) {
DisplayMetrics dm = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
return dm.density;
}
public static String getString() {
return "HAHA";
}
}
混淆后的代碼如下刀诬,類名和方法名都被混淆了陕壹,而沒有用到的方法被刪除了糠馆。
public class b {
public static float a(Activity paramActivity) {
DisplayMetrics displayMetrics = new DisplayMetrics();
paramActivity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.density;
}
}
二、混淆規(guī)則
2.1 默認(rèn)混淆規(guī)則
雖然'proguard-rules.pro'文件中還沒有添加任何混淆規(guī)則毕匀,但是編譯器已經(jīng)知道哪些類和變量一定不能被混淆,例如Activity凤薛、自定義View缤苫、JavascriptInterface等等活玲,這些屬于默認(rèn)的混淆規(guī)則舒憾。如果想告訴編譯器還有哪些類和變量也不該被混淆镀迂,就需要用戶自己添加規(guī)則探遵,項目中的以下內(nèi)容都不該被混淆箱季。
① 枚舉
② 第三方庫
③ 運(yùn)用了反射的類
④ 網(wǎng)絡(luò)數(shù)據(jù)解析的JavaBean實(shí)體類
⑤ Parcelable的子類和 Creator 靜態(tài)成員變量
⑥ 四大組件拷况、自定義的Application
⑦ JNI中調(diào)用的類
2.2 自定義混淆規(guī)則
先來看混淆規(guī)則的通配符赚瘦。
通配符 | 描述 |
---|---|
<field> | 匹配類中的所有字段 |
<method> | 匹配類中所有的方法 |
<init> | 匹配類中所有的構(gòu)造函數(shù) |
* | 匹配任意長度字符蚤告,不包含包名分隔符(.) |
** | 匹配任意長度字符,包含包名分隔符(.) |
*** | 匹配任意參數(shù)類型 |
再來看制定混淆規(guī)則的關(guān)鍵字心褐,這些關(guān)鍵字指定了混淆規(guī)則的粒度逗爹。
① keep: 保留類名或整個類不被混淆
// 直接將keep作用于類掘而,只是保證類名不被混淆,如下所示斑胜,成員還是會被混淆
-keep public class com.lister.autopacktest.SizeUtils
// 如果不混淆整個類的話止潘,規(guī)則如下所示
-keep public class com.lister.autopacktest.SizeUtils { *; }
// * 通配符表示保持該包下的類名涧狮,但是子包的類名還是會被混淆
-keep public class com.lister.autopacktest.utils.*
// ** 通配符表示保持該包及其子包下的類名
-keep public class com.lister.autopacktest.utils.**
// 如果想保留該包下的所有類名與方法,需要加上{ *; }
-keep public class com.lister.autopacktest.utils.** { *; }
② keepnames: 保留類和類中的成員的命名钢颂,成員沒有被引用會被移除
注意keepnames只是防止類和成員被重命名殊鞭,沒有被引用的成員還是會被移除锯仪。
③ keepclassmembers: 保留類中的成員,防止被移除和重命名
// 類似之前示例中的CommonJSApi,雖然類名被混淆了斥扛,但是方法未被混淆稀颁。
// 如果想保留特定的方法,可以定義如下的規(guī)則粘昨。
// 1. 不混淆某個類的構(gòu)造方法
-keepclassmembers class com.lister.autopacktest.SizeUtils {
public <init>();
}
// 2. 不混淆某個類的特定的方法
-keepclassmembers class com.lister.autopacktest.SizeUtils {
public void test(java.lang.String);
}
④ keepclassmembernames: 保留類中成員的命名,成員沒有引用會被移除
⑤ keepclasseswithmembers: 保留指明的類和成員馁启,防止被移除和重命名
⑥ keepclasseswithmembernames: 保留指明的類和成員妖啥,防止被重命名荆虱,成員沒有引用會被移除
上面提到诉位,keep關(guān)鍵字可以保留單個類或者某個包下的類,如果不被混淆的類不在一個包下岳瞭,就需要一個一個添加到混淆規(guī)則中枫耳。那么有沒有什么辦法能夠簡化這個流程呢钻心?這里介紹一種通過注解定義混淆規(guī)則的方法,在不被混淆的類和成員上加上注解即可说墨,先定義@Keep和@KeepAll兩個注解。
@Keep注解可添加在類熄捍、變量碟贾、方法上,表示不混淆當(dāng)前被注解的內(nèi)容缕陕。
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface Keep {
String value() default "";
}
@KeepAll注解添加在類上,表示不混淆當(dāng)前類的所有內(nèi)容疙挺。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface KeepAll {
String value() default "";
}
在proguard-rules添加如下規(guī)則即可扛邑,可以發(fā)現(xiàn)@Keep注解對方法、變量和類名做了約束铐然,而@KeepAll注解對整個類做了約束蔬崩。
-keep @interface <packagename>.Keep
-keep @interface <packagename>.KeepAll
-keepclassmembers class * {
@<packagename>.Keep <methods>;
@<packagename>.Keep <fields>;
}
-keep @<packagename>.Keep class *
-keep @<packagename>.KeepAll class * { *; }
當(dāng)需要對內(nèi)部類操作時,通過$指明內(nèi)部類搀暑;同時可以用private沥阳、public進(jìn)一步指定需要保留的內(nèi)容。例如當(dāng)你需要保留某個內(nèi)部類的public構(gòu)造函數(shù)時:
-keep class Test$T {
public <init>;
}
三自点、mapping文件
當(dāng)應(yīng)用崩潰或者發(fā)生錯誤時桐罕,我們會得到方法調(diào)用棧來分析問題,而混淆過的應(yīng)用提供的是混淆后的調(diào)用棧桂敛,此時我們就需要解混淆功炮。打開...build/outputs/mapping
目錄下的mapping.txt文件,可以找到項目中所有類和變量混淆前后的名字與對應(yīng)關(guān)系术唬。
來看看測試項目的mapping文件薪伏,不僅有類和成員的對應(yīng)關(guān)系,還很貼心地為你標(biāo)出了方法所在的行數(shù)粗仓。
com.lister.autopacktest.CommonJSApi -> b.a.a.a:
9:10:void <init>(android.content.Context) -> <init>
14:14:java.lang.String getVersion() -> getVersion
com.lister.autopacktest.MainActivity -> com.lister.autopacktest.MainActivity:
boolean mShouldShowDensity -> s
11:15:void <init>() -> <init>
33:38:void onClick(android.view.View) -> onClick
19:29:void onCreate(android.os.Bundle) -> onCreate
com.lister.autopacktest.SecondActivity -> com.lister.autopacktest.SecondActivity:
android.webkit.WebView mWebView -> t
android.widget.FrameLayout mWebViewContainer -> s
11:11:void <init>() -> <init>
18:42:void onCreate(android.os.Bundle) -> onCreate
46:49:void onDestroy() -> onDestroy
com.lister.autopacktest.SizeUtils -> com.lister.autopacktest.SizeUtils:
7:7:void <init>() -> <init>
10:12:float getDensity(android.app.Activity) -> a
com.lister.autopacktest.TestView -> com.lister.autopacktest.TestView:
android.graphics.Paint mPaint -> b
int mHeight -> d
int mWidth -> c
20:21:void <init>(android.content.Context) -> <init>
24:25:void <init>(android.content.Context,android.util.AttributeSet) -> <init>
28:30:void <init>(android.content.Context,android.util.AttributeSet,int) -> <init>
33:36:void init() -> a
47:49:void onDraw(android.graphics.Canvas) -> onDraw
40:43:void onSizeChanged(int,int,int,int) -> onSizeChanged
四嫁怀、Gradle打包
4.1 實(shí)現(xiàn)debug與release包不同包名
商業(yè)化的應(yīng)用都分為debug版和release版设捐,debug版用于快速調(diào)試錯誤,release版本用于外發(fā)塘淑。在AS中直接run app或者build apk生成的就是debug版本萝招。為了兩個版本的應(yīng)用在手機(jī)上共存,可以修改debug版應(yīng)用的包名朴爬,加一個.debug后綴即寒,如下所示。
android {
......
buildTypes {
debug {
applicationIdSuffix ".debug"
......
}
}
}
4.2 簽名
APK的打包都需要簽名召噩,平時打debug包時如果沒有指定簽名母赵,默認(rèn)使用debug.keystore作為debug包的簽名,可以通過keytool -list -v -keystore xxx
命令查看某個簽名的信息具滴。
也可以通過gradle的signingReport這個Task查看凹嘲,在命令行運(yùn)行gradlew signingReport
即可在命令行看到debug.keystore的信息,如下所示构韵。
> Task :app:signingReport
Variant: debug
Config: debug
Store: C:\Users\win10\.android\debug.keystore
Alias: AndroidDebugKey
MD5: ......
SHA1: ......
SHA-256: ......
Valid until: 2049年9月14日 星期二
而打release肯定不能用debug簽名周蹭,首先需要新建一個簽名文件。點(diǎn)擊AS的build->Generated signed Bundle/APK疲恢,選擇APK凶朗,點(diǎn)擊Create new...新建簽名文件,我這里已經(jīng)新建過了显拳,存放在項目里app目錄下棚愤。隨后點(diǎn)擊next,選擇release版本杂数,在下方選擇v1宛畦、v2兩個簽名,點(diǎn)擊finish即可打包揍移。
我們也可以在gradle.build中配置打包所使用的簽名次和,如下所示。在signingConfigs中配置release版本的簽名路徑那伐、密碼等信息后踏施,在buildTypes中通過signingConfig signingConfigs.release
指定簽名配置為signingConfigs中的信息。隨后在命令行運(yùn)行gradlew assembleRelease
即可罕邀。
signingConfigs {
release {
storeFile file('../app/autoKey.jks')
storePassword "123456"
keyAlias "autoKey"
keyPassword "123456"
v1SigningEnabled true
v2SigningEnabled true
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules-release.pro'
}
}
上面的配置中直接聲明了簽名的密碼等信息读规,如果應(yīng)用開源,這些信息很容易被獲取燃少。因此官方更推薦通過properties文件的形式聲明簽名信息束亏,新建keystore.properties
文件。
storePassword=......
keyPassword=......
keyAlias=......
storeFile=......
之后在build.gradle
中讀取keystore.properties
文件即可阵具,注意keystore.properties
應(yīng)該存儲于安全的地方碍遍,不應(yīng)該隨著應(yīng)用的代碼一起上傳上去定铜,否則它就沒有意義了。
......
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
如果要查看APK的簽名信息怎么辦怕敬?
將APK解壓后進(jìn)入META-INF目錄揣炕,其中的CERT.RSA文件中就存放著簽名信息。在該目錄下運(yùn)行命令keytool -printcert -file CERT.RSA
即可查看該APK的簽名信息东跪。
五畸陡、Jenkins打包
在項目過程中,測試經(jīng)常需要研發(fā)去打某個分支的包虽填,研發(fā)人員需要進(jìn)行保存代碼丁恭、切換分支、修改配置......等一系列操作斋日,影響開發(fā)效率牲览。而使用Jenkins進(jìn)行遠(yuǎn)程打包就沒有這個煩惱了,只要輸入對應(yīng)的分支名即可打包恶守。
Jenkins可以去官網(wǎng)下載第献,不過速度很慢,windows版本我上傳到了CSDN兔港,有需要的同學(xué)自扔购痢:Jenkins。
具體打包流程具體見參考5衫樊、6岔绸,本來想寫一下這塊的踩坑經(jīng)歷,但是發(fā)現(xiàn)大神們都寫的很詳細(xì)了橡伞,就不畫蛇添足了。實(shí)踐中唯一的問題是插件下載太慢晋被,可以在官網(wǎng)jenkins插件手動下載插件后安裝兑徘。