Simplify 以虛擬方式執行應用程式以了解其行為,然後嘗試優化程式碼,使其行為相同但更易於人們理解。每種最佳化類型都是簡單且通用的,因此使用哪種特定類型的混淆並不重要。
左邊的程式碼是混淆應用程式的反編譯,右邊的程式碼已經被反混淆。
該專案由三個部分組成:smalivm、simple 和演示應用程式。
if
或switch
條件都會導致採用兩個分支。 usage: java -jar simplify.jar <input> [options]
deobfuscates a dalvik executable
-et,--exclude-types <pattern> Exclude classes and methods which include REGEX, eg: "com/android", applied after include-types
-h,--help Display this message
-ie,--ignore-errors Ignore errors while executing and optimizing methods. This may lead to unexpected behavior.
--include-support Attempt to execute and optimize classes in Android support library packages, default: false
-it,--include-types <pattern> Limit execution to classes and methods which include REGEX, eg: ";->targetMethod("
--max-address-visits <N> Give up executing a method after visiting the same address N times, limits loops, default: 10000
--max-call-depth <N> Do not call methods after reaching a call depth of N, limits recursion and long method chains, default: 50
--max-execution-time <N> Give up executing a method after N seconds, default: 300
--max-method-visits <N> Give up executing a method after executing N instructions in that method, default: 1000000
--max-passes <N> Do not run optimizers on a method more than N times, default: 100
-o,--output <file> Output simplified input to FILE
--output-api-level <LEVEL> Set output DEX API compatibility to LEVEL, default: 15
-q,--quiet Be quiet
--remove-weak Remove code even if there are weak side effects, default: true
-v,--verbose <LEVEL> Set verbosity to LEVEL, default: 0
建置需要安裝 Java Development Kit 8 (JDK)。
因為專案包含 Android 框架的子模組,所以可以使用--recursive
進行複製:
git clone --recursive https://github.com/CalebFenton/simplify.git
或隨時更新子模組:
git submodule update --init --recursive
然後,建立一個包含所有依賴項的 jar:
./gradlew fatjar
Simplify jar 將位於simplify/build/libs/
中。您可以透過簡化提供的混淆範例應用程式來測試它的工作情況。以下是運行它的方式(您可能需要更改simplify.jar
):
java -jar simplify/build/libs/simplify.jar -it " org/cf/obfuscated " -et " MainActivity " simplify/obfuscated-app.apk
要了解反混淆的內容,請查看混淆應用程式的自述文件。
若簡化失敗,請依序嘗試以下建議:
-it
選項僅針對少數方法或類別。--max-address-visits
、 --max-call-depth
和--max-method-visits
。-v
或-v 2
並透過日誌和 DEX 或 APK 的雜湊值報告問題。如果在 Windows 上構建,並且構建失敗並出現類似以下內容的錯誤:
找不到tools.jar。請檢查 C:Program FilesJavajre1.8.0_151 是否包含有效的 JDK 安裝。
這意味著 Gradle 無法找到正確的 JDK 路徑。確保已安裝 JDK,將JAVA_HOME
環境變數設定為 JDK 路徑,並確保關閉並重新開啟用於建置的命令提示字元。
別害羞。我認為虛擬執行和反混淆是令人著迷的問題。任何有興趣的人都會自然而然地很酷,並且歡迎做出貢獻,即使只是為了修復一個拼寫錯誤。請隨意在問題中提出問題並提交拉取請求。
請包含 APK 或 DEX 的連結以及您正在使用的完整命令。這使得重現(從而修復)您的問題變得更加容易。
如果您無法共用範例,請包含檔案雜湊(SHA1、SHA256 等)。
如果一個操作放置了一個可以轉換為常數(例如字串、數字或布林值)的值,則此最佳化將以該常數取代該操作。例如:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
# Decrypts to: "Tell me of your homeworld, Usul."
move-result v0
在此範例中,加密的字串被解密並放入v0
中。由於字串是「可常量化」的,因此move-result v0
可以替換為const-string
:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
const-string v0, "Tell me of your homeworld, Usul."
如果刪除程式碼不可能改變應用程式的行為,則程式碼已死亡。最明顯的情況是程式碼無法訪問,例如if (false) { // dead }
)。如果程式碼是可達的,如果它不影響方法之外的任何狀態,即它沒有副作用,則可以認為它是死的。例如,程式碼可能不會影響方法的回傳值、更改任何類別變數或執行任何 IO。這在靜態分析中很難確定。幸運的是,smalvm 並不需要很聰明。它只是愚蠢地執行它能執行的所有操作,並假設如果不能確定就會產生副作用。考慮恆定傳播中的範例:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
const-string v0, "Tell me of your homeworld, Usul."
在此程式碼中, invoke-static
不再影響方法的回傳值,我們假設它不會執行任何奇怪的操作,例如將位元組寫入檔案系統或網路套接字,因此它沒有副作用。它可以簡單地被移除。
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
最後,第一個const-string
將一個值指派給暫存器,但該值從未被使用,即分配已失效。也可以移除。
const-string v0, "Tell me of your homeworld, Usul."
好哇!
Java 靜態分析的一大挑戰是反射。如果不進行仔細的資料流分析,就不可能知道參數是否適用於反射方法。有很多聰明的方法可以做到這一點,但 smalivm 只需執行程式碼即可做到這一點。當它發現反射的方法呼叫時,例如:
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
它可以知道v0
、 v1
和v2
的值。如果確定這些值是什麼,它可以用實際的非反射方法呼叫來取代對Method.invoke()
的呼叫。這同樣適用於反射欄位和類別查找。
對於所有不完全適合特定類別的事物,都有窺視孔優化。這包括刪除無用的check-cast
操作,用const-string
取代Ljava/lang/String;-><init>
調用,等等。
.method public static test1 () I
.locals 2
new-instance v0 , L java/lang/Integer ;
const/4 v1 , 0x1
invoke-direct { v0 , v1 }, L java/lang/Integer ; -> <init> ( I ) V
invoke-virtual { v0 }, L java/lang/Integer ; -> intValue () I
move-result v0
return v0
.end method
這一切所做的就是v0 = 1
。
.method public static test1 () I
.locals 2
new-instance v0 , L java/lang/Integer ;
const/4 v1 , 0x1
invoke-direct { v0 , v1 }, L java/lang/Integer ; -> <init> ( I ) V
invoke-virtual { v0 }, L java/lang/Integer ; -> intValue () I
const/4 v0 , 0x1
return v0
.end method
move-result v0
被替換為const/4 v0, 0x1
。這是因為intValue()I
只有一個可能的回傳值,而回傳型別可以設為常數。參數v0
和v1
是明確的且不會改變。也就是說, intValue()I
處的每個可能的執行路徑都有一個值的共識。其他可以轉換為常數的值類型:
const/4
、 const/16
等const-string
const-class
.method public static test1 () I
.locals 2
const/4 v0 , 0x1
return v0
.end method
由於const/4 v0, 0x1
上面的程式碼不會影響方法外部的狀態(無副作用),因此可以刪除而不改變行為。如果有一個方法呼叫向檔案系統或網路寫入了某些內容,則無法將其刪除,因為它會影響方法外部的狀態。或者,如果test()I
採用可變參數,例如LinkedList
,則任何存取它的指令都不能被視為已死。
死程式碼的其他範例:
if (false) { dead_code(); }
此工具可在雙重許可證下使用:適用於閉源專案的商業許可證和可在開源軟體中使用的 GPL 許可證。
根據您的需要,您必須選擇其中一個並遵循其政策。 LICENSE.COMMERCIAL 和 LICENSE.GPL 文件中提供了每個授權類型的策略和協定的詳細資訊。