文章轉(zhuǎn)自我個(gè)人博客
本文前半部分對(duì)照 Proguard 文檔 (Manul 中的 Introduce 部分)進(jìn)行翻譯同時(shí)加上個(gè)人的理解,如果有與原文不同,請(qǐng)以原文為主。后半部分是對(duì)幾個(gè)步驟的驗(yàn)證。
介紹
混淆器(ProGuard)會(huì)對(duì) Java class 文件進(jìn)行 shrinker(壓縮),optimizer(優(yōu)化),obfuscator(混淆)以及preverifier(校驗(yàn))。shrinker(壓縮)這一步會(huì)找到并移除沒(méi)用到的類,變量,方法,屬性。optimization(優(yōu)化)這一步,會(huì)分析并且優(yōu)化方法的字節(jié)碼。obfuscation(混淆)則會(huì)對(duì) class,fields,methods替換成一些短的無(wú)意義的名字。第一步會(huì)把代碼量變小,運(yùn)行更加有效率,同時(shí)更加難以被逆向。在 Java Micro Edition 和 Java 6或者更高版本中,最后一步的檢驗(yàn)過(guò)程,會(huì)向class文件中添加一些預(yù)校驗(yàn)的信息。
上述的每個(gè)步驟,都是可以選擇的(可以進(jìn)行也可以不進(jìn)行)。例如,ProGuard 可以只進(jìn)行preverify,從而更高效的運(yùn)行。

- 首先,ProGuard(混淆器) 讀入輸入的 jars (也可以是 aars, wars, ears, zips, apks, 或者目錄)。隨后,開始進(jìn)行 shrinker(壓縮),optimizer(優(yōu)化),obfuscator(混淆)以及preverifier(校驗(yàn))。你可以選擇性的讓ProGuard(混淆器)進(jìn)行多種類型的優(yōu)化操作。ProGuard(混淆器)會(huì)把修改過(guò)的結(jié)果寫入一個(gè)或者多個(gè)輸出的 jars (也可以是 aars, wars, ears, zips, apks, 或者目錄)中。
- 混淆器需要明確輸入文件(Input jars)是jars包(也可以是 aars, wars, ears, zips, apks, 或者目錄)。這些 libraries 本質(zhì)上是你將會(huì)用來(lái)編譯的代碼?;煜鳛榱四軌蛘_進(jìn)行整個(gè)過(guò)程,會(huì)重新構(gòu)建類之間的依賴。而依賴包(Library jars) 往往是不會(huì)被改變的,但你依舊需要把它們放在最終的App的環(huán)境中。
Entry points(入口點(diǎn))
- 在壓縮步驟(shrinker),混淆器會(huì)從這些點(diǎn)(入口點(diǎn))進(jìn)入,并且遞歸尋找決定哪些類和哪些類成員會(huì)被使用。所有的其他類和類成員都會(huì)被拋棄掉
- 在優(yōu)化步驟(optimizer),混淆器會(huì)進(jìn)一步優(yōu)化代碼。在這些優(yōu)化過(guò)程中,那些不是入口點(diǎn)的類和方法會(huì)變成private static或者final,不被用到的參數(shù)會(huì)被移除,一些方法會(huì)變成內(nèi)斂方法
- 在混淆這一步(obfuscator),混淆器會(huì)重新命名那些不是入口點(diǎn)的類和類的成員。在這整個(gè)過(guò)程中,那些成為入口點(diǎn)的地方,依舊會(huì)為他們保留原來(lái)的名字
- 預(yù)驗(yàn)證階段(preverifier)是唯一一個(gè)不需要知道入口點(diǎn)的階段
反射
- 對(duì)于反射和introspection 進(jìn)行代碼的自動(dòng)處理時(shí),都會(huì)存在一些特殊的問(wèn)題。在混淆器進(jìn)行處理時(shí),代碼中類和類成員都是被動(dòng)態(tài)創(chuàng)建或者被動(dòng)態(tài)調(diào)用的(通過(guò)對(duì)應(yīng)類的名字,或者成員名字),這些地方都必須被定義成入口點(diǎn)。例如,
Class.forName()這個(gè)構(gòu)造器會(huì)在運(yùn)行時(shí)指向任何的類。又比如,類的名字可能會(huì)從配置文件中讀入,這通常很難去計(jì)算出是那些類需要被保留(通過(guò)原始的名字)。因此,你必須得在混淆器的配置中,通過(guò)簡(jiǎn)單相同的操作-keep來(lái)指定他們。
然而,混淆器已經(jīng)能夠幫你發(fā)現(xiàn)并處理以下的情況:
Class.forName("SomeClass")
SomeClass.class
SomeClass.class.getField("someField")
SomeClass.class.getDeclaredField("someField")
SomeClass.class.getMethod("someMethod", new Class[] {})
SomeClass.class.getMethod("someMethod", new Class[] { A.class })
SomeClass.class.getMethod("someMethod", new Class[] { A.class, B.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] {})
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class, B.class })
AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicLongFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicReferenceFieldUpdater.newUpdater(SomeClass.class, SomeType.class, "someField")
- 類和類成員的名字會(huì)不一樣,但是構(gòu)造方法必然是相同的,由此,混淆器能夠認(rèn)出他們。被引用的類和類的成員在壓縮(shrinking)階段會(huì)被保留,同時(shí),string 類型的參數(shù)也會(huì)在混淆時(shí)(obfuscation)被準(zhǔn)確的修改。
- 除此之外,混淆器會(huì)提供一些建議:是否保留一些出現(xiàn)的類和類成員。舉例,混淆器會(huì)標(biāo)記
(SomeClass)Class.forName(variable).newInstance()這樣的構(gòu)造器。因?yàn)檫@些方法可能會(huì)指向其他類,這些可能是類,也可能是接口,或者是繼承自這些接口或者類的類。你需要在配置中做相應(yīng)的處理。 - 為了能夠得到正確的混淆結(jié)果,你應(yīng)該對(duì)進(jìn)行混淆的代碼多少有所熟悉。當(dāng)面臨大量反射代碼時(shí),混淆代碼需要進(jìn)行大量的試驗(yàn),并處理錯(cuò)誤,特別是對(duì)于內(nèi)部代碼沒(méi)有足夠的信息的情況下
以上是對(duì)官方文檔首頁(yè)的翻譯內(nèi)容
具體的驗(yàn)證
該部分不是翻譯內(nèi)容,是根據(jù)ProGuard 的使用方法和文檔首頁(yè),對(duì)上述三個(gè)步驟的具體驗(yàn)證。
由于大部分情況下,Android的混淆只需要考慮Obfuse這個(gè)步驟,因?yàn)楹芏嗟谌揭蕾嚢幕煜?guī)則會(huì)把 shrink和optimize去掉(比如友盟)。所以先驗(yàn)證這一步。
下面的驗(yàn)證步驟,涉及三個(gè)類,java打包的命令(Java 環(huán)境),proguard.jar包(混淆器,進(jìn)行整個(gè)混淆過(guò)程的jar包),proguard.pro文件(寫入具體混淆的規(guī)則)和Intelij(用來(lái)查看class文件)等內(nèi)容。
Obfuse 步驟驗(yàn)證
這個(gè)步驟,如上所說(shuō),主要是對(duì)類,方法進(jìn)行名字的修改,也是 Android 混淆中最重要的部分。為了驗(yàn)證這個(gè)過(guò)程,我做了下面的demo操作
- 首先寫了3個(gè)類:
package com.dove.home;
class HelloWorld {
public HelloWorld(){
System.out.println("Hello World");
}
}
package com.dove.home;
class HelloWorld2 {
public HelloWorld2(){
System.out.println("Hello World2");
}
}
package com.dove.home;
class Main {
public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
}
}
- 然后編譯,打包
javac com/dove/home/Main.java
javac com/dove/home/HelloWorld2.java
javac com/dove/home/HelloWorld.java
//注意在進(jìn)行下面步驟的時(shí)候,我把 com/dove/home 下的 java源碼刪了
jar -cvf main_source.jar com

- 然后使用混淆器,混淆器其具體使用方法,主要是調(diào)用
proguard.jar包,然后配置proguard.pro文件進(jìn)行具體的參數(shù)設(shè)置。
下面是我proguard.pro文件內(nèi)容
# 源碼文件
-injars main_source.jar
# 混淆后輸出文件
-outjars main_source_out.jar
# java 核心 jar 不能混淆
-libraryjars <java.home>/lib/rt.jar
-libraryjars <java.home>/lib/jce.jar
# 全部不混淆,即三個(gè)class文件都會(huì)保持原樣
-keep class com.dove.home.Main{*;}
-keep class com.dove.home.HelloWorld{*;}
-keep class com.dove.home.HelloWorld2{*;}
具體的混淆命令,同時(shí)參考下圖(該步驟會(huì)生成混淆后的jar包)
java -jar proguard.jar @proguard.pro

注意:然后修改 proguard.pro 文件,內(nèi)容如下
-injars main_source.jar
# 注意輸出包的名字改了
-outjars main_source_proguard_out.jar
-libraryjars <java.home>/lib/rt.jar
-libraryjars <java.home>/lib/jce.jar
-keep class com.dove.home.Main{*;}
# 刪除了HelloWorld的 keep
-keep class com.dove.home.HelloWorld2{*;}
同樣運(yùn)行上面的混淆命令,生成另一個(gè)混淆后的包
最后對(duì)三個(gè)包進(jìn)行對(duì)比,通過(guò)代碼逆向,進(jìn)行驗(yàn)證,最快的方式是把生成的 jar 包,當(dāng)做第三方依賴包直接導(dǎo)入Intellij 中(有decode的功能),如下圖,三個(gè)包的區(qū)別

最初是的源碼包和保留HelloWorld,HelloWorld2以及Main入口點(diǎn)的包是一樣的,不同的是最后沒(méi)有保留HelloWorld入口點(diǎn)的包,它的HelloWorld變成了a,而Main和HelloWorld2都正常沒(méi)有被修改
Optimize 步驟驗(yàn)證
同樣,修改 proguard.pro 文件,內(nèi)容如下,然后運(yùn)行混淆命令,生成新的 jar 包
-injars main_source.jar
# 輸出包名改了,方便對(duì)比
-outjars main_source_proguard_not_optimize_out.jar
-libraryjars <java.home>/lib/rt.jar
-libraryjars <java.home>/lib/jce.jar
# 加上不進(jìn)行優(yōu)化的限制
-dontoptimize
-keep class com.dove.home.Main{*;}
-keep class com.dove.home.HelloWorld2{*;}
同上,導(dǎo)入IntelliJ,對(duì)比上一步中混淆后的 jar 包,發(fā)現(xiàn)名字沒(méi)啥變化,但內(nèi)容不一樣了

首先是沒(méi)有添加 -dontoptimize
package com.dove.home;
final class a {
public a() {
System.out.println("Hello World");
}
}
然后是添加了 -dontoptimize
package com.dove.home;
class a {
public a() {
System.out.println("Hello World");
}
}
如上述譯文中所說(shuō),optimize 會(huì)進(jìn)行代碼優(yōu)化,不是入口點(diǎn)的代碼,會(huì)變成final,private等等,該步驟驗(yàn)證完畢。
Shrink 步驟驗(yàn)證
修改 proguard.pro 文件,進(jìn)行壓縮,同時(shí)不對(duì) HelloWorld,HelloWorld2進(jìn)行入口點(diǎn)的保留
-injars main_source.jar
-outjars main_source_proguard_shrink_out.jar
-libraryjars <java.home>/lib/rt.jar
-libraryjars <java.home>/lib/jce.jar
-keep class com.dove.home.Main{*;}
# 注意對(duì)比之前,刪除了HelloWorld和HelloWorld2的 keep
修改 proguard.pro 文件,不進(jìn)行壓縮,同樣不對(duì) HelloWorld,HelloWorld2進(jìn)行入口點(diǎn)的保留
-injars main_source.jar
-outjars main_source_proguard_not_shrink_out.jar
-libraryjars <java.home>/lib/rt.jar
-libraryjars <java.home>/lib/jce.jar
# 添加不進(jìn)行壓縮
-dontshrink
-keep class com.dove.home.Main{*;}
其結(jié)果對(duì)比
添加了 -dontshrink標(biāo)志

未添加 -dontshrink標(biāo)志

此處消失的b其實(shí)就是HelloWorld2,而留下的a則是HelloWorld,原因很簡(jiǎn)單,因?yàn)镸ain里面持有了HelloWorld的引用,而HelloWorld2則從未被用到,所以就被拋棄了。
由此驗(yàn)證,shrink階段,Proguard(混淆器)會(huì)把無(wú)用類文件等刪除,一些被動(dòng)態(tài)獲取的類就需要注意了,需要進(jìn)行-keep操作,使其成為入口點(diǎn)。
以上就是對(duì)混淆整個(gè)過(guò)程的驗(yàn)證
對(duì)于 Android 混淆,一些需要注意的東西,會(huì)在下一篇文章中記錄