轉自:https://www.jianshu.com/p/0306cb947d7a
一、反編譯某平臺代碼
最近在看某外賣平臺的代碼,發現某外賣平臺最新版本版本無法正常的通過dex2jar工具將dex轉換出Java源代碼,在轉換過程中會提示出錯,如圖:
dex2jar異常圖
查看轉換出的Java源代碼,會發現很多類方法提示下圖所示異常,很多方法中都會拋出RuntimeException:can not merge I and Z
:
public class AsyncTaskService extends IntentService {
protected void onHandleIntent(Intent paramIntent){
throw new RuntimeException("d2j fail translate: java.lang.RuntimeException: can not merge I and Z\n\tat...);
}
public int onStartCommand(Intent paramIntent, int paramInt1, int paramInt2){
throw new RuntimeException("d2j fail translate: java.lang.RuntimeException: can not merge I and Z\n\tat...");
}
}
查看日誌文件會發現很多類似的錯誤信息,可以看到方法內RuntimeException
棧信息和反編譯的錯誤信息是相同的,都提示can not merge I and Z
。
dex2jar日誌
二、爲什麼?
本來以爲這是dex2jar工具低版本的一個bug,但更新了dex2jar以後,依然還是會出現上述錯誤。java.lang.RuntimeException: can not merge I and Z
這個異常,在sourceforge上解釋的比較清楚,其實是一個dex2jar工具檢查出的一個參數異常",The problem is caused by strict type calculation, because in java syntaxt, a boolean can not assign to an inteager. so dex2jar forbid merge type Z and I. 你用布爾類型入參調用一個參數爲整型的函數,當然會檢查出錯,爲啥這麼說,我使用apktool工具,看了一下apk的smali代碼。發現報錯的函數的最前面都含有一段奇怪smali的代碼:
invoke-static {}, Lpnf/this/object/does/not/Exist;->a()Z
move-result v0
invoke-static {v0}, Lpnf/this/object/does/not/Exist;->b(I)V
看上面的代碼,pnf.this.object.does.not.Exist.a()
方法返回一個boolean類型數據,放入v0寄存器,作爲pnf.this.object.does.not.Exist.b(int)
函數的入參。正常情況下這樣的語法錯誤在java代碼編譯時就不會通過的。看到這裏你會不會想,如果我不想別人直接看到我的Java代碼,是不是可以通過在覈心函數中插入上面這段有語法錯誤的代碼,以達到dex2jar工具檢查出錯的目的呢?從而將代碼被閱讀的門檻從java提高到smali。
三、手動代碼注入
爲了驗證上面的猜想,這裏我通過反編譯一個apk,手動插入有語法錯誤的smali代碼,以驗證防dex2jar的思路,具體步驟如下:
- 1.反編譯一個apk。
- 2.修改smali代碼,插入上面這三句有語法錯誤的代碼。
- 3.重打包,使用dex2jar工具轉換新包的dex,看是否能正常轉換出Java源代碼。並檢查運行時是否出錯。
我這裏用一個Hello World應用來測試,使用apktool反編譯出smali代碼,並在Application的onCreate方法中插入這段有語法錯誤的代碼。
# virtual methods
.method public onCreate()V
.locals 3
invoke-super {p0}, Lcn/trinea/android/lib/h/c;->onCreate()V
invoke-virtual {p0}, Lcn/trinea/android/developertools/MyApplication;->getApplicationContext()Landroid/content/Context;
move-result-object v0
invoke-static {}, Lpnf/object/does/not/Exist;->a()Z
move-result v3
invoke-static {v3}, Lpnf/object/does/not/Exist;->b(I)V
return-void
.end method
這裏不要忘了,你可能需要另外編譯出Exist.smali這個文件,不然運行時一定會爆出ClassNotFound異常。將下面的Exist.java編譯出Exist.smali放入相應的包路徑,重打包就可以了。Java代碼如下:
public class Exist {
public static boolean a() {
return false;
}
public static void b(int test) {
}
}
最後,驗證下果然重新打包後的apk,確實不能正常轉換出Java源代碼,這裏就不貼圖了,因爲轉換出錯日誌是一樣的。並且運行時也不會出錯。接下來會寫一個Gradle編譯插件,針對特定的函數,插入代碼,防止dex2jar工具查看Java源代碼。
四、實現思路
Android客戶端在防止其Java代碼被dex2jar轉換時其實就是藉助dex2jar的語法檢查機制,將有語法錯誤的字節碼插入到想要保護的Java函數中裏面,以達到dex2jar轉換出錯的目的。接下來我就大致記錄下如何開發Gradle編譯插件,在編譯過程中實現上述防護思路,先看下Android APK打包流程。
Android apk打包流程
Android APK打包流程如上圖所示,Java代碼先通過Java Compiler生成.class文件,再通過dx工具生成dex文件,最後使用apkbuilder工具完成代碼與資源文件的打包,並使用jarsigner簽名,最後可能還有使用zipalign對簽名後的apk做對齊處理。
如果需要完成對特定函數的代碼注入,可以在Java代碼編譯生成class文件後,在dex文件生成前,針對class字節碼進行操作,以本例爲例需要動態生成Exsit類文件的字節碼。
// 動態生成Exist.class
public class Exist {
public static boolean a() {
return false;
}
public static void b(int test) {
}
}
將下列Java代碼轉換成字節碼插入需要保護的函數中。
// 插入到特定的Java函數內
Exist.b(Exist.a());
並將修改後的.class文件放入dex打包目錄中,完成dex打包,具體流程如下圖所示:
Gradle提供了叫Transform
的API,允許三方插件在class文件轉換爲dex文件前操作編譯好的class文件,這個API的目標就是簡化class文件的自定義的操作而不用對Task進行處理,並且可以更加靈活地進行操作。詳細的可以參考區長的博客。
五、使用ASM操作Java字節碼
ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直
接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行爲。這裏推薦一個IDEA插件:ASM ByteCode Outline
。可以查看.class文件的字節碼,並可以生成成ASM框架代碼。安裝ASM Bytecode Outline
插件後,可以在Intellij IDEA
->Code
->Show Bytecode Outline
查看類文件對應個字節碼和ASM框架代碼,利用ASM框架代碼就可以生成相應的.class文件了。
生成Exist字節碼的具體實現,生成Exist.java的構造函數:
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(51, ACC_PUBLIC + ACC_SUPER, "ivonhoe/dexguard/java/Exist", null, "java/lang/Object", null);
cw.visitSource("Exist.java", null);
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(7, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "Livonhoe/dexguard/java/Exist;", null, l0, l1, 0);
mv.visitMaxs(1, 1);
mv.visitEnd();
聲明一個函數名爲a,返回值爲boolean類型的無參函數:
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "a", "()Z", null, null);
mv.visitCode();
l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(10, l0);
mv.visitInsn(ICONST_0);
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 0);
mv.visitEnd();
聲明一個函數名爲b,參數爲int型,返回類型爲void的函數
MV = CW.VISITmETHOD(acc_public + acc_static, "b", "(i)v", NULL, NULL);
MV.VISITcODE();
L0 = NEW lABEL();
MV.VISITlABEL(L0);
MV.VISITlINEnUMBER(14, L0);
MV.VISITiNSN(return);
L1 = NEW lABEL();
MV.VISITlABEL(L1);
MV.VISITlOCALvARIABLE("TEST", "i", NULL, L0, L1, 0);
MV.VISITmAXS(0, 1);
MV.VISITeND();
在指定函數內,插入Exist.b(Exist.a());
對應的字節碼的具體實現,繞過Java編譯器的語法檢查:
static class InjectClassVisitor extends ClassVisitor {
private String methodName;
InjectClassVisitor(int i, ClassVisitor classVisitor, String method) {
super(i, classVisitor)
this.methodName = method;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
mv = new MethodVisitor(Opcodes.ASM4, mv) {
@Override
void visitCode() {
// 在方法體開始調用時
if (name.equals(methodName)) {
mv.visitMethodInsn(INVOKESTATIC, "ivonhoe/dexguard/java/Exist", "a", "()Z", false);
mv.visitMethodInsn(INVOKESTATIC, "ivonhoe/dexguard/java/Exist", "b", "(I)V", false);
}
super.visitCode()
}
@Override
public void visitMaxs(int maxStack, int maxLocal) {
if (name.equals(methodName)) {
super.visitMaxs(maxStack + 1, maxLocal);
} else {
super.visitMaxs(maxStack, maxLocal);
}
}
}
return mv;
}
}
六、總結
看到這裏可能你會有一個疑惑,爲什麼有語法錯誤的代碼,在運行時不會出錯,個人理解不單單是因爲bool類型在內存中是以0或1
表示,也因爲int
和bool
在Android虛擬機中都存儲在32位寄存器中,如果使用int
和long
類型的參數互換,在dx階段的編譯就會報錯。下面是插件源碼,有興趣的同學可以嘗試一下~
插件源碼
詳細的Gradle源碼和實例可參考https://github.com/Ivonhoe/dexguard
使用方法
- 在root project的build.gradle中添加依賴
classpath 'ivonhoe.gradle.dexguard:dexguard-gradle:0.0.2-SNAPSHOT'
buildscript {
repositories {
maven { url 'https://raw.githubusercontent.com/Ivonhoe/mvn-repo/master/' }
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'ivonhoe.gradle.dexguard:dexguard-gradle:0.0.2-SNAPSHOT'
}
}
- 在app項目的build.gradle中添加插件,map.txt中配置需要保護的方法名
apply plugin: 'ivonhoe.dexguard'
dexguard {
guardConfig = "${rootDir}/map.txt"
}