帮你把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交互的越多则越慢。

竞品对比

待补充

posted @ 2020-04-26 10:44  tieyan  阅读(1216)  评论(3编辑  收藏  举报