帮你把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交互的越多则越慢。
竞品对比
待补充