Android安全之---應用防dex2jar原理及實現

轉自: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表示,也因爲intbool在Android虛擬機中都存儲在32位寄存器中,如果使用intlong類型的參數互換,在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"
}

七、參考文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章