做脫機協議,首先要找到關鍵的加密代碼,然而這些代碼一般都在so里面,因為逆向c/c++的難度遠比java大多了!找到關鍵代碼后,一般情況下是逐行分析,然后自己寫代碼復現整個加密過程。但是,有些非標准的加密算法是由一個團隊實現的,整個過程非常復雜。逆向人員再去逐行分析和復現,有點“不划算”!怎么才能直接調用so里面的這些關鍵代碼了?可以通過前面的介紹的frida hook,也可以通過今天介紹的這個so的模擬框架--unidbg!官方的功能介紹如下:
- Emulation of the JNI Invocation API so JNI_OnLoad can be called.
- Support JavaVM, JNIEnv.
- Emulation of syscalls instruction.
- Support ARM32 and ARM64.
- Inline hook, thanks to Dobby.
- Android import hook, thanks to xHook.
- iOS fishhook and substrate and whale hook.
- unicorn backend support simple console debugger, gdb stub, instruction trace, memory read/write trace.
- Support iOS objc and swift runtime.
- Support dynarmic fast backend.
- Support Apple M1 hypervisor, the fastest ARM64 backend.
- Support Linux KVM backend with Raspberry Pi B4.
看着很多,有點唬人,實際並不復雜,以本文分享的為例:我們平時開發android app,在Android studio配置好各種環境和參數后是能直接在java層調用so層函數的。那么在unidbg,也能實現同樣的功能:即調用so層的函數!這也是unidbg最核心的功能之一了!具體該怎么操作了? 步驟一:當然是先去unidbg的官網下載unidbg的框架啦,然后用intelij打開,里面能看到作者已經寫好的各種java的測試工程代碼,如下:
這些測試的demo代碼已經說明了unidbg的接口和核心功能了。今天就用kanxue提供的so來說明核心api和調用流程!
1、選擇執行引擎:如果明確使用了以下代碼,那么unidbg使用dynarmic引擎,否則默認使用unicorn引擎!
static { DynarmicLoader.useDynarmic(); }
2、創建虛擬機/模擬器,並執行虛擬機的類型是art還是dailvik:
AndroidARMEmulator emulator= new AndroidARMEmulator("com.sun.jna",null,null); final Memory memory = emulator.getMemory(); VM vm = emulator.createDalvikVM(); vm.setVerbose(true);//這里如果是true,后續調用jni_onload的時候就能打印日志
3、指定SDK的版本,這里用23版本:
LibraryResolver resolver = new AndroidResolver(23); memory.setLibraryResolver(resolver);
4、開始加載so庫了:
Module unicorn08module=emulator.loadLibrary(new File("D:\\xxxx\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\unicorncourse08\\unicorn08.so"));
5、調用so層的導出函數:這兩個都是導出函數,直接用符號名就行了;
Number result=unicorn08module.callFunction(emulator,"_Z3addii",1,2)[0];//導出函數直接用符號名就行了 System.out.println("_Z3addii result:"+result.intValue()); //_Z7add_sixiiiiii result=unicorn08module.callFunction(emulator,"_Z7add_sixiiiiii",1,2,3,4,5,6)[0]; System.out.println("_Z7add_sixiiiiii result:"+result.intValue());
這個是打印的結果:
_Z3addii result:3
_Z7add_sixiiiiii result:21
6、這兩個都是對字符串做操作的,so層僅僅求了傳入字符串的長度:
MemoryBlock block1=memory.malloc(10,true); UnidbgPointer str1_ptr=block1.getPointer(); str1_ptr.write("hello".getBytes()); String content=ARM.readCString(emulator.getBackend(),str1_ptr.peer); System.out.println("_Z15getstringlengthPKc:"+str1_ptr.toString()+"---"+content); result=unicorn08module.callFunction(emulator,"_Z15getstringlengthPKc",new PointerNumber(str1_ptr))[0]; Number r0value=emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0); System.out.println("_Z15getstringlengthPKc result:"+result.intValue()+"----"+r0value); MemoryBlock block2=memory.malloc(10,true); UnidbgPointer str2_ptr=block2.getPointer(); str2_ptr.write("666".getBytes()); String content2=ARM.readCString(emulator.getBackend(),str2_ptr.peer); System.out.println("_Z16getstringlength2PKcS0_:"+str2_ptr.toString()+"---"+content2); result=unicorn08module.callFunction(emulator,"_Z16getstringlength2PKcS0_",new PointerNumber(str1_ptr),new PointerNumber(str2_ptr))[0]; r0value=emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0); System.out.println("_Z16getstringlength2PKcS0_ result:"+result.intValue()+"----"+r0value);
打印結果:
_Z15getstringlengthPKc:RW@0x4016b000---hello _Z15getstringlengthPKc result:5----5 _Z16getstringlength2PKcS0_:RW@0x4016c000---666 _Z16getstringlength2PKcS0_ result:8----8
7、這核心的核心:直接調用jni_onload
vm.callJNI_OnLoad(emulator,unicorn08module);
打印結果:這里可以看到分別在so的0x8c7、0xccb調用了FindClass和RegisterNative方法,然后注冊MainActivity這個類的stringFromJNI2方法,該方法和so層中0xb35的方法是映射的!
JNIEnv->FindClass(com/example/unicorncourse08/MainActivity) was called from RX@0x40000c87[libnative-lib.so]0xc87 JNIEnv->RegisterNatives(com/example/unicorncourse08/MainActivity, unidbg@0xbffff778, 1) was called from RX@0x40000ccb[libnative-lib.so]0xccb RegisterNative(com/example/unicorncourse08/MainActivity, stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;, RX@0x40000b35[libnative-lib.so]0xb35)
去so的0xc87和0xccb查看,果然是FindClass和RegisterNative方法,unidbg 誠不我欺! 作為逆向,其實最重要的還是最后那個打印結果:java層的stringFromJNI2方法就是和so層的這個方法是映射的:
進入里面調用的各個函數仔細分析,發現這個函數還是比較簡單:先接受傳入的string,再打印出來!由於這個函數並未導出,但是和java層的函數做了映射,所以這里也可以直接通過java層的名字來直接調用,代碼如下:
//調用jni函數,對於動態注冊的jni函數必須在完成地址的綁定才能調用 System.out.println("stringFromJNI1-------------------------"); DvmClass MainActivity_dvmclass=vm.resolveClass("com/example/unicorncourse08/MainActivity");//先把類找到,這里的原理很像反射 DvmObject resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI1(Ljava/lang/String;)Ljava/lang/String;","helloworld");//再通過類去調用里面的函數 System.out.println("resultobj:"+resultobj); resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI1(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellokanxue")); System.out.println("resultobj:"+resultobj); System.out.println("stringFromJNI1-------------------------"); //動態注冊的jni函數stringFromJNI2 resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellostringFromJNI2")); System.out.println("resultobj:"+resultobj); System.out.println("stringFromJNI2-------------------------"); DvmObject mainactivity=MainActivity_dvmclass.newObject(null); mainactivity.callJniMethodObject(emulator,"stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellostringFromJNI2")); System.out.println("resultobj:"+resultobj); System.out.println("stringFromJNI2-------------------------");
打印的結果如下:這里面除了我們顯式調用println打印的日志,還有unidbg這個框架自身打印的日志(主要是JNIenv這個類的函數調用):
stringFromJNI1------------------------- Find native function Java_com_example_unicorncourse08_MainActivity_stringFromJNI1(Ljava/lang/String;)Ljava/lang/String; => RX@0x40000a71[libnative-lib.so]0xa71 JNIEnv->GetStringUtfChars("helloworld") was called from RX@0x40000b03[libnative-lib.so]0xb03 JNIEnv->NewStringUTF("helloworld") was called from RX@0x40000b2f[libnative-lib.so]0xb2f resultobj:"helloworld" Find native function Java_com_example_unicorncourse08_MainActivity_stringFromJNI1(Ljava/lang/String;)Ljava/lang/String; => RX@0x40000a71[libnative-lib.so]0xa71 JNIEnv->GetStringUtfChars("hellokanxue") was called from RX@0x40000b03[libnative-lib.so]0xb03 [main]I/stringFromJNI1: content:helloworld,length:10 [main]I/stringFromJNI1: content:hellokanxue,length:11 [main]I/stringFromJNI2: content:hellostringFromJNI2,length:19 [main]I/stringFromJNI2: content:hellostringFromJNI2,length:19 JNIEnv->NewStringUTF("hellokanxue") was called from RX@0x40000b2f[libnative-lib.so]0xb2f resultobj:"hellokanxue" stringFromJNI1------------------------- Find native function Java_com_example_unicorncourse08_MainActivity_stringFromJNI2(Ljava/lang/String;)Ljava/lang/String; => RX@0x40000b35[libnative-lib.so]0xb35 JNIEnv->GetStringUtfChars("hellostringFromJNI2") was called from RX@0x40000b03[libnative-lib.so]0xb03 JNIEnv->NewStringUTF("hellostringFromJNI2") was called from RX@0x40000b2f[libnative-lib.so]0xb2f resultobj:"hellostringFromJNI2" stringFromJNI2------------------------- Find native function Java_com_example_unicorncourse08_MainActivity_stringFromJNI2(Ljava/lang/String;)Ljava/lang/String; => RX@0x40000b35[libnative-lib.so]0xb35 JNIEnv->GetStringUtfChars("hellostringFromJNI2") was called from RX@0x40000b03[libnative-lib.so]0xb03 JNIEnv->NewStringUTF("hellostringFromJNI2") was called from RX@0x40000b2f[libnative-lib.so]0xb2f resultobj:"hellostringFromJNI2" stringFromJNI2-------------------------
完整的代碼:
package com.unicorncourse08; import com.github.unidbg.LibraryResolver; import com.github.unidbg.Module; import com.github.unidbg.PointerNumber; import com.github.unidbg.arm.ARM; import com.github.unidbg.arm.backend.dynarmic.DynarmicLoader; import com.github.unidbg.linux.android.AndroidARMEmulator; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.DvmClass; import com.github.unidbg.linux.android.dvm.DvmObject; import com.github.unidbg.linux.android.dvm.StringObject; import com.github.unidbg.linux.android.dvm.VM; import com.github.unidbg.memory.Memory; import com.github.unidbg.memory.MemoryBlock; import com.github.unidbg.pointer.UnidbgPointer; import unicorn.ArmConst; import java.io.File; public class MainActivity { static { DynarmicLoader.useDynarmic(); } public static void main(String[] args) { AndroidARMEmulator emulator= new AndroidARMEmulator("com.sun.jna",null,null); final Memory memory = emulator.getMemory(); VM vm = emulator.createDalvikVM(); vm.setVerbose(true); LibraryResolver resolver = new AndroidResolver(23); memory.setLibraryResolver(resolver); Module unicorn08module=emulator.loadLibrary(new File("D:\\xxxx\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\unicorncourse08\\unicorn08.so")); Number result=unicorn08module.callFunction(emulator,"_Z3addii",1,2)[0];//導出函數直接用符號名就行了 System.out.println("_Z3addii result:"+result.intValue()); //_Z7add_sixiiiiii result=unicorn08module.callFunction(emulator,"_Z7add_sixiiiiii",1,2,3,4,5,6)[0]; System.out.println("_Z7add_sixiiiiii result:"+result.intValue()); //_Z15getstringlengthPKc MemoryBlock block1=memory.malloc(10,true); UnidbgPointer str1_ptr=block1.getPointer(); str1_ptr.write("hello".getBytes()); String content=ARM.readCString(emulator.getBackend(),str1_ptr.peer); System.out.println("_Z15getstringlengthPKc:"+str1_ptr.toString()+"---"+content); result=unicorn08module.callFunction(emulator,"_Z15getstringlengthPKc",new PointerNumber(str1_ptr))[0]; Number r0value=emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0); System.out.println("_Z15getstringlengthPKc result:"+result.intValue()+"----"+r0value); MemoryBlock block2=memory.malloc(10,true); UnidbgPointer str2_ptr=block2.getPointer(); str2_ptr.write("666".getBytes()); String content2=ARM.readCString(emulator.getBackend(),str2_ptr.peer); System.out.println("_Z16getstringlength2PKcS0_:"+str2_ptr.toString()+"---"+content2); result=unicorn08module.callFunction(emulator,"_Z16getstringlength2PKcS0_",new PointerNumber(str1_ptr),new PointerNumber(str2_ptr))[0]; r0value=emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0); System.out.println("_Z16getstringlength2PKcS0_ result:"+result.intValue()+"----"+r0value); //調用jni_OnLoad函數 vm.callJNI_OnLoad(emulator,unicorn08module); //調用jni函數,對於動態注冊的jni函數必須在完成地址的綁定才能調用 System.out.println("stringFromJNI1-------------------------"); DvmClass MainActivity_dvmclass=vm.resolveClass("com/example/unicorncourse08/MainActivity");//先把類找到,這里的原理很像反射 DvmObject resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI1(Ljava/lang/String;)Ljava/lang/String;","helloworld");//再通過類去調用里面的函數 System.out.println("resultobj:"+resultobj); resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI1(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellokanxue")); System.out.println("resultobj:"+resultobj); System.out.println("stringFromJNI1-------------------------"); //動態注冊的jni函數stringFromJNI2 resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellostringFromJNI2")); System.out.println("resultobj:"+resultobj); System.out.println("stringFromJNI2-------------------------"); DvmObject mainactivity=MainActivity_dvmclass.newObject(null); mainactivity.callJniMethodObject(emulator,"stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellostringFromJNI2")); System.out.println("resultobj:"+resultobj); System.out.println("stringFromJNI2-------------------------"); } }
總結一下,上述API包括了3種so函數的調用方法:普通的so方法、jni_onload調用、jni函數調用!上面舉例的這些內容相對簡單,並不涉及到so層調用java層的函數。如果遇到so層函數調用java層函數怎么辦么?我們如果自己在apk寫java代碼調用so層函數,遇到so通過反射調用java層函數時,需要自己補上java層對應的類、方法和變量,因為這些需要執行的代碼是繞不過去的!unidbg是這么樣的么? 答案是肯定的!比如下面的這個so層的方法,會在jni_onload中被調用;這里需要獲取java層普通變量、static變量后打印出來;也會獲取java層的普通方法然后調用,這該怎么辦了?
上面說了:so層調用java層的代碼肯定是要補齊的(如果直接簡單粗暴改so層代碼,可能導致別人原來的邏輯錯誤)! 這里該怎么實操了? 大概的思路是:
- 自己補上缺失的方法(當然java層的方法可以用jadx、jeb等反編譯得到,不用自己反編譯smali),這里缺失了base64方法,需要補上!
- 自己補上缺失的變量,方法同上!
- 重寫getStaticObjectField、getObjectField、callObjectMethodV等方法,然后檢測傳入的參數。一旦發現使用/調用的是java層變量、方法,用自己補上的哪些代碼替換(原理像不像平時常用的hook?)
說了那么多,完整的demo代碼如下:
package com.unicorncourse08; import com.github.unidbg.Module; import com.github.unidbg.linux.android.AndroidARMEmulator; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory; import com.github.unidbg.memory.Memory; import com.github.unidbg.virtualmodule.android.AndroidModule; import com.github.unidbg.virtualmodule.android.JniGraphics; import org.apache.commons.codec.binary.Base64; import sun.applet.Main; import java.io.File; import java.lang.reflect.Field; public class MainActivitymethod1 extends AbstractJni { private static DvmClass MainActivityClass; @Override /* * staticcontent是java層的靜態變量;getStaticObjectField,一旦檢測到so層引用這個變量,那么自己返回這個變量的值 * */ public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) { System.out.println("getStaticObjectField->"+signature); if(signature.equals("com/example/testjni/MainActivity->staticcontent:Ljava/lang/String;")){ return new StringObject(vm,"staticcontent");//源碼 public static string staticcontent = "staticcontent" } return super.getStaticObjectField(vm, dvmClass, signature); } @Override /* * objcontent是java層的變量;這里重寫getObjectField方法,一旦檢測到so層引用這個變量,那么自己返回這個變量的值 * */ public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) { System.out.println("getObjectField->"+signature); if(signature.equals("com/example/testjni/MainActivity->objcontent:Ljava/lang/String;")){ return new StringObject(vm,"objcontent");//public string objcontent } return super.getObjectField(vm, dvmObject, signature); } /* * java層的方法,這里需要復現,否則不知道怎么執行 * */ public String base64(String arg3) { String result=Base64.encodeBase64String(arg3.getBytes()); return result; } @Override /* * base64是java層的方法,這里重寫callObjectMethodV方法:一旦發現調用的是java層的base64方法,這里就用自己復現的base64方法替換 * */ public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { System.out.println("callObjectMethodV->"+signature); if(signature.equals("com/example/testjni/MainActivity->base64(Ljava/lang/String;)Ljava/lang/String;")){ DvmObject dvmobj=vaList.getObjectArg(0); String arg= (String) dvmobj.getValue(); String result=base64(arg); return new StringObject(vm,result); } return super.callObjectMethodV(vm, dvmObject, signature, vaList); } public static void main(String[] args) { MainActivitymethod1 mainActivitymethod1=new MainActivitymethod1(); AndroidARMEmulator emulator = new AndroidARMEmulator("org.telegram.messenger",null,null); final Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); VM vm = emulator.createDalvikVM(); vm.setVerbose(true); vm.setJni(mainActivitymethod1); DalvikModule dm = vm.loadLibrary(new File("D:\\xxxx\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\unicorncourse08\\calljava.so"), true); dm.callJNI_OnLoad(emulator); MainActivityClass=vm.resolveClass("com/example/testjni/MainActivity"); DvmObject obj=MainActivityClass.newObject(null); obj.callJniMethodObject(emulator,"base64byjni(Ljava/lang/String;)Ljava/lang/String;","callbase64byjni"); } }
整個邏輯其實並不復雜,從main函數開始:創建模擬器、創建虛擬機、加載so、調用so層函數!打印的結果如下:
JNIEnv->FindClass(com/example/testjni/MainActivity) was called from RX@0x400009df[libnative-lib.so]0x9df JNIEnv->RegisterNatives(com/example/testjni/MainActivity, unidbg@0xbffff768, 1) was called from RX@0x40000f19[libnative-lib.so]0xf19 RegisterNative(com/example/testjni/MainActivity, stringFromJNI(Ljava/lang/String;)Ljava/lang/String;, RX@0x40000cb1[libnative-lib.so]0xcb1)
Find native function Java_com_example_testjni_MainActivity_base64byjni(Ljava/lang/String;)Ljava/lang/String; => RX@0x4000088d[libnative-lib.so]0x88d JNIEnv->FindClass(com/example/testjni/MainActivity) was called from RX@0x400009df[libnative-lib.so]0x9df getStaticObjectField->com/example/testjni/MainActivity->staticcontent:Ljava/lang/String;
JNIEnv->GetStaticObjectField(class com/example/testjni/MainActivity, staticcontent Ljava/lang/String; => "staticcontent") was called from RX@0x40000aa5[libnative-lib.so]0xaa5 JNIEnv->GetStringUtfChars("staticcontent") was called from RX@0x40000adb[libnative-lib.so]0xadb
[main]I/stringFromJNI: staticcontent:staticcontent [main]I/stringFromJNI: objcontent:objcontent
getObjectField->com/example/testjni/MainActivity->objcontent:Ljava/lang/String;
JNIEnv->GetObjectField(com.example.testjni.MainActivity@7b3300e5, objcontent Ljava/lang/String; => "objcontent") was called from RX@0x40000b11[libnative-lib.so]0xb11 JNIEnv->GetStringUtfChars("objcontent") was called from RX@0x40000adb[libnative-lib.so]0xadb JNIEnv->GetMethodID(com/example/testjni/MainActivity.base64(Ljava/lang/String;)Ljava/lang/String;) was called from RX@0x40000b55[libnative-lib.so]0xb55
callObjectMethodV->com/example/testjni/MainActivity->base64(Ljava/lang/String;)Ljava/lang/String;
JNIEnv->CallObjectMethodV(com.example.testjni.MainActivity@7b3300e5, base64("callbase64byjni") => "Y2FsbGJhc2U2NGJ5am5p") was called from RX@0x40000bb1[libnative-lib.so]0xbb1 JNIEnv->GetStringUtfChars("Y2FsbGJhc2U2NGJ5am5p") was called from RX@0x40000adb[libnative-lib.so]0xadb JNIEnv->NewStringUTF("Y2FsbGJhc2U2NGJ5am5p") was called from RX@0x40000c05[libnative-lib.so]0xc05
[main]I/stringFromJNI: base64result:Y2FsbGJhc2U2NGJ5am5p
參考:
1、https://github.com/zhkl0228/unidbg unidbg官網