簡介
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交互的越多則越慢。
競品對比
待補充