幫你把java代碼自動翻譯到c/c++ jni調用


簡介

andorid上有個工具叫dex2oat,在apk安裝的階段會把dex轉換成elf的二進制格式。基於此思路擴展,如果我們在java字節碼生成后產品發布前就把java字節碼轉換成平台的c/c++ jni調用代碼,結合私有ollvm,那么對外發布的直接就是一個高度混淆的二進制的jni動態庫,其逆向難度會大大的增強。(主要場景在於android java && 對外開放jar包代碼保護)

實例

http://androidxref.com/9.0.0_r3/xref/art/test/003-omnibus-opcodes/src/Goto.java 為例

static int smallGoto(boolean which) {
        System.out.println("Goto.smallGoto");

        int i = 0;

        if (which) {
            i += filler(i);
        } else {
            i -= filler(i);
        }

        return i;
}

新建idea工程導入Goto.java,編譯工程生成jar。編寫規則文件config_test.yaml

apikey: helloworld

rules:
- com.test.TestCompiler.Goto:
    #保留的方法
    keep:
    #要編譯的方法
    compile:
    - smallGoto: 'cons,bogus,call'   #給私有ollvm的編譯參數

#生成的動態庫名字
libname: hello

#需要生成的平台
platform:
- MacOS-x64

使用命令行與編譯服務器交互

./javatojni_helper_mac --infile=./jar_test.jar --config=./config_test.yaml

編譯服務器會接收到jar后,解析生成的jar的字節碼得到cfg

通過def-use鏈以及調用的api接口信息回歸方式遞進分析可推導出變量的類型,結合上異常處理表,codegen一把梭把ir轉換到c/c++ jni代碼

//Java_com_test_TestCompiler_Goto.cpp
#include "java2cpp.h"
#include "ids.h"

extern "C" JNIEXPORT jint JNICALL Java_com_test_TestCompiler_Goto_smallGoto__Z(JNIEnv *env, jobject thiz, jboolean in_parameter_0) {
    /*
    0 boolean
    */
    //LOG_DEBUG("call method: %s", R"###(<com.test.TestCompiler.Goto: int smallGoto(boolean)>)###");
    jclass _com_test_TestCompiler_Goto_jclass = NULL;
    jclass _java_io_PrintStream_jclass = NULL;
    jclass _java_lang_System_jclass = NULL;
    jvalue temp;
    jthrowable exception;

    jobject $r0 = NULL;
    jboolean local_z0;
    jint $i0;
    jint $i1;
    jint local_i2;
    jint local_i3;
    jboolean local_z1;
    jint local_i4;
label0:{
    temp.z = in_parameter_0;
    local_z0 = temp.z;
    temp = java2cpp_get_static_field(env, &_java_lang_System_jclass, &__0003cjava_lang_System_0003a_00020java_io_PrintStream_00020out_0003e_jfleidID, "java/lang/System", "out", "Ljava/io/PrintStream;", JValueType_l);
    JAVA2CPP_GOTO_IF_EXCEPTION(L_TOP_EX_HANDLE);
    JAVA2CPP_DELETE_OBJECT($r0);
    $r0 = temp.l;
    jvalue var_arg_0[1] = {{.l=0},};
    temp.l = env->NewStringUTF("\x47\x6f\x74\x6f\x2e\x73\x6d\x61\x6c\x6c\x47\x6f\x74\x6f");
    JAVA2CPP_GOTO_IF_NULL(temp.l, L_TOP_EX_HANDLE);
    AutoObject var_arg_1(env, temp.l);
    var_arg_0[0].l = temp.l;
    temp = java2cpp_call_instance_method(env, $r0, &_java_io_PrintStream_jclass, &__0003cjava_io_PrintStream_0003a_00020void_00020println_00028java_lang_String_00029_0003e_jmethodID, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", var_arg_0, JValueType_v);
    var_arg_1.reset();
    JAVA2CPP_GOTO_IF_EXCEPTION(L_TOP_EX_HANDLE);
    temp.i = 0;
    local_i2 = temp.i;
    temp.z = local_z0;
    local_z1 = temp.z;
    temp.i = (jint)local_z1;
    local_i4 = temp.i;
    if(local_i4 == 0) {
        goto label1;
    }
    jvalue var_arg_2[1] = {{.l=0},};
    var_arg_2[0].i = local_i2;
    temp = java2cpp_call_static_method(env, &_com_test_TestCompiler_Goto_jclass, &__0003ccom_test_TestCompiler_Goto_0003a_00020int_00020filler_00028int_00029_0003e_jmethodID, "com/test/TestCompiler/Goto", "filler", "(I)I", var_arg_2, JValueType_i);
    JAVA2CPP_GOTO_IF_EXCEPTION(L_TOP_EX_HANDLE);
    $i1 = temp.i;
    temp.i = local_i2 + $i1;
    local_i3 = temp.i;
    goto label2;
}
label1:{
    jvalue var_arg_3[1] = {{.l=0},};
    var_arg_3[0].i = local_i2;
    temp = java2cpp_call_static_method(env, &_com_test_TestCompiler_Goto_jclass, &__0003ccom_test_TestCompiler_Goto_0003a_00020int_00020filler_00028int_00029_0003e_jmethodID, "com/test/TestCompiler/Goto", "filler", "(I)I", var_arg_3, JValueType_i);
    JAVA2CPP_GOTO_IF_EXCEPTION(L_TOP_EX_HANDLE);
    $i0 = temp.i;
    temp.i = local_i2 - $i0;
    local_i3 = temp.i;
}
label2:{
    temp.i = local_i3;
    return temp.i;
}
L_TOP_EX_HANDLE:
    return 0;
}

自動生成的c/c++ jni代碼在數值賦值方面比較啰嗦,還好clang -o2會幫我們負重前行。此時原先數行的java字節碼已經變成數百行的匯編代碼

未混淆的ida f5偽代碼

未混淆的流程圖

如果結合私有ollvm,原來數百行匯編代碼又將膨脹變成數千行的匯編代碼,此時ida f5偽代碼已經不能正常工作

混淆后的流程圖

多平台支持能力

已知問題

1)由於local-reference-table存在上限,如果jni函數內有大量的jobject對象,運行時有可能造成局部表溢出觸發異常
2)jvm標准規定某些函數不能為native(例如構造函數),而android中的實現又允許,為多平台翻譯一致性差異部分統一成不翻譯
3)
4) Android O, DEX 38新增的invoke-custom指令暫不支持
5) 部分函數邏輯可能依賴函數的參數名字(例如controller的隱式參數映射),而jvm不允許native函數有Code attribute,對此情況需要顯式的通過注解指定參數名字
6)需要傳遞Function類型的地方暫不支持private函數(既目前僅支持default, public, protected), 例如

public class LambdaTest{
    private String getName() {
        System.out.println("getName");
        return name;
    }

    public void test(){
       //getName為私有函數,其引用處不會被翻譯(即test函數不會被翻譯)
       Collectors.mapping(LambdaTest::getName, Collectors.joining(""));
    }
}

7)synthetic的函數,可能會導致同一個類下存在函數名字&&函數入參完全一樣,僅返回值不一樣,這種情況同名函數只會編譯找到的第一個,例如kotlin.collections.EmptyMap

    @Override
    public final Object get(Object arg1) {
        return this.get(arg1);
    }

    public Void get(Object arg1) {
        return null;
    }

大小變化

測試的apk 翻譯規則 翻譯的類個數 翻譯的方法個數 armv7so文件大小 原始apk大小 翻譯后apk大小
開源中國 5.0.1 net.oschina.app.* 2257 9757 8.8m 15m 19m
qksms 3.8.1 com.moez.QKSMS.* 1766 4357 4.0m 6.6m 8.0m

運行性能

待補充
先說結論,首先性能肯定是變慢的(30%+,甚至數倍),現代jvm對字節碼執行做了大量的優化,通過jni調用外部函數會有額外的開銷,具體性能影響視要翻譯的java代碼而定,jvm和jni交互的越多則越慢。

競品對比

待補充


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM