得益于Unicorn的強大的指令trace能力,可以很容易實現對cpu執行的每一條匯編指令的跟蹤,進而對ollvm保護的函數進行剪枝,去掉虛假塊,大大提高逆向分析效率。請分別使用Unidbg和Stalker引擎完成對該app中的jnicheck函數的trace跟蹤,并簡單分析該apk邏輯,找出flag。

 

首先是用unidbg簡單的實現下調用。結果出現問題。


java.lang.IllegalStateException: Please vm.setJni(jni)    at com.github.unidbg.linux.android.dvm.Hashable.checkJni(Hashable.java:8)    at com.github.unidbg.linux.android.dvm.DvmClass.getStaticMethodID(DvmClass.java:101)    at com.github.unidbg.linux.android.dvm.DalvikVM64$110.handle(DalvikVM64.java:1697)


很明顯的提示讓我們加上setJni。加上后,并且修改類繼承自AbstractJni。然后再來一次。然后又出現錯誤。


JNIEnv->GetStaticMethodID(com/kanxue/crackme/MainActivity.crypt2(Ljava/lang/String;)Z) => 0xf66a2c58 was called from RX@0x40028d98[libnative-lib.so]0x28d98[23:35:36 063]  WARN [com.github.unidbg.linux.ARM64SyscallHandler] (ARM64SyscallHandler:369) - handleInterrupt intno=2, NR=-129104, svcNumber=0x172, PC=unidbg@0xfffe07b4, LR=RX@0x400291c8[libnative-lib.so]0x291c8, syscall=nulljava.lang.UnsupportedOperationException: com/kanxue/crackme/MainActivity->crypt2(Ljava/lang/String;)Z    at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethodV(AbstractJni.java:169)    at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethodV(AbstractJni.java:164)


看著好像是沒有找到這個靜態函數的實現的樣子。但是我測試,直接調用這個crypt2函數是沒問題的。那么問題可能就是出在jni函數的內部調用jni函數了。然后既然我們已知直接調用crypt2沒問題。實際上我們也可以直接調用crypt2來分析即可。但是既然是作業,那么還是跑通一下吧。那么我自己重寫實現一下內部的調用。


@Override    public boolean callStaticBooleanMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {        switch (signature){            case "com/kanxue/crackme/MainActivity->crypt2(Ljava/lang/String;)Z":                Symbol symbol=module.findSymbolByName("Java_com_kanxue_crackme_MainActivity_crypt2");                StringObject str=vaList.getObjectArg(0);                Number num=module.callFunction(emulator,symbol.getAddress(),str.toString());                return num.intValue()>0;        }         throw new UnsupportedOperationException(signature);    }


然后又出現問題了。錯誤如下:


java.lang.IllegalStateException: running    at com.github.unidbg.AbstractEmulator.emulate(AbstractEmulator.java:358)    at com.github.unidbg.thread.Function64.run(Function64.java:39)    at com.github.unidbg.thread.MainTask.dispatch(MainTask.java:19)    at com.github.unidbg.thread.UniThreadDispatcher.run(UniThreadDispatcher.java:172)


大致意思就是虛擬機正在運行中,不能再調用另外一個jni函數。也就是說再callFunction中執行一個函數,實際就是開一個虛擬機去執行,然后因為這個虛擬機正在執行中,就不能調用另外一個函數。所以。我決定用兩個虛擬機,就不會有問題啦。下面貼上完整代碼。


package com.zuoye; import com.github.unidbg.AndroidEmulator;import com.github.unidbg.LibraryResolver;import com.github.unidbg.Module;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.*;import com.github.unidbg.memory.Memory; import java.io.File; public class KanxueTest extends AbstractJni {    private final AndroidEmulator emulator;    private final VM vm;    private final DvmClass mainActivityDvm;    private final Module module;    public static void main(String[] args) {        KanxueTest bcfTest = new KanxueTest();        bcfTest.call_jnicheck();    }    private KanxueTest(){        emulator = AndroidEmulatorBuilder                .for64Bit()                .build();        Memory memory = emulator.getMemory();        LibraryResolver resolver = new AndroidResolver(23);        memory.setLibraryResolver(resolver);        vm = emulator.createDalvikVM();        vm.setVerbose(true);        vm.setJni(this);         mainActivityDvm = vm.resolveClass("com/kanxue/crackme/MainActivity");         DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/arm64-v8a/libnative-lib.so"), false);        module=dm.getModule();         dm.callJNI_OnLoad(emulator);    }     @Override    public boolean callStaticBooleanMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {        switch (signature){            case "com/kanxue/crackme/MainActivity->crypt2(Ljava/lang/String;)Z":                StringObject input=vaList.getObjectArg(0);                KanxueTest test2=new KanxueTest();                return test2.crypt2(input.getValue());        }        throw new UnsupportedOperationException(signature);    }     //主動調用目標函數    private void call_jnicheck(){        Boolean res = mainActivityDvm.callStaticJniMethodBoolean(emulator, "jnicheck(Ljava/lang/String;)Z","aasd1123");        System.out.println(res);    }     private boolean crypt2(String data){        Boolean res = mainActivityDvm.callStaticJniMethodBoolean(emulator, "crypt2(Ljava/lang/String;)Z",data);        return res;    }}


這次就成功執行完成了,結果如下:


Find native function Java_com_kanxue_crackme_MainActivity_jnicheck => RX@0x40025904[libnative-lib.so]0x25904JNIEnv->GetStringUtfChars("aasd1123") was called from RX@0x400270a4[libnative-lib.so]0x270a4JNIEnv->NewStringUTF("aasd1123666") was called from RX@0x40027640[libnative-lib.so]0x27640JNIEnv->GetStringUtfChars("aasd1123666") was called from RX@0x400270a4[libnative-lib.so]0x270a4JNIEnv->FindClass(com/kanxue/crackme/MainActivity) was called from RX@0x40028008[libnative-lib.so]0x28008JNIEnv->GetStaticMethodID(com/kanxue/crackme/MainActivity.crypt2(Ljava/lang/String;)Z) => 0xf66a2c58 was called from RX@0x40028d98[libnative-lib.so]0x28d98Find native function Java_com_kanxue_crackme_MainActivity_crypt2 => RX@0x40029750[libnative-lib.so]0x29750JNIEnv->GetStringUtfChars(""aasd1123666"") was called from RX@0x400270a4[libnative-lib.so]0x270a4JNIEnv->CallStaticBooleanMethodV(class com/kanxue/crackme/MainActivity, crypt2("aasd1123666") => false) was called from RX@0x400291c8[libnative-lib.so]0x291c8false


接下來就是開啟trace分析這個對比的邏輯,找到那個flag。

 

根據上面的日志,就可以看到實際上第一個函數就是講字符串給加上666。


aasd1123  ->  jnicheck  ->  aasd1123666   ->  crypt2


所以關鍵是看crypt2的邏輯了。接著是對匯編執行對部分進行trace。這里我使用我之前寫的trace方案。

 

https://github.com/dqzg12300/unidbg_tools.git

 

這里我自己封裝了一套對trace的優化處理,能夠打印ldr的內容。以及寄存器的詳細變化。下面是調整后的代碼。


//主動調用目標函數   private void call_jnicheck(){       KingTrace trace1=new KingTrace(emulator);       //dump ldr的數據。包括ldr賦值給寄存器的如果是指針,也會dump       GlobalData.is_dump_ldr=true;       //dump str的數據       GlobalData.is_dump_str=true;       trace1.initialize(0x40025904,0x40025904+0x162c,null);       emulator.getBackend().hook_add_new(trace1,0x40025904,0x40025904+0x162c,emulator);       Boolean res = mainActivityDvm.callStaticJniMethodBoolean(emulator, "jnicheck(Ljava/lang/String;)Z","XUe");       System.out.println(res);   }    private boolean crypt2(String data){       KingTrace trace1=new KingTrace(emulator);       //dump ldr的數據。包括ldr賦值給寄存器的如果是指針,也會dump       GlobalData.is_dump_ldr=true;       //dump str的數據       GlobalData.is_dump_str=true;       trace1.initialize(0x40029750,0x40029750+0x30c,null);       emulator.getBackend().hook_add_new(trace1,0x40029750,0x40029750+0x30c,emulator);       Boolean res = mainActivityDvm.callStaticJniMethodBoolean(emulator, "crypt2(Ljava/lang/String;)Z",data);       return res;   }


由于我們前面知道了處理后的字符串會固定帶一個666.所以我在trace的數據里面直接找666.然后就找到了一段數據如下:


[22:53:56 379]ldr_left_address:bffff680 dump, md5=bff7dd55d78b9378f9b117f668e032f6, hex=616173643131323336363600000000000000000000000000000000000000000000000000000000000000000000000000size: 480000: 61 61 73 64 31 31 32 33 36 36 36 00 00 00 00 00    aasd1123666.....0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................^-----------------------------------------------------------------------------^[libnative-lib.so 0x297dc] [e04f00b9] 0x400297dc: "str w0, [sp, #0x4c]"-----w0=0xb    sp=0xbffff5f0        //w0=0xb[libnative-lib.so 0x297e0] [e00308aa] 0x400297e0: "mov x0, x8"-----x0=0xb    x8=0xbffff680        //x0=0xbffff680[libnative-lib.so 0x297e4] [ebe2ff97] 0x400297e4: "bl #0x40022390"[libnative-lib.so 0x297e8] [a00c0036] 0x400297e8: "tbz w0, #0, #0x4002997c"-----w0=0x0        //w0=0x0[libnative-lib.so 0x2997c] [e8031f2a] 0x4002997c: "mov w8, wzr"-----w8=0x0        //w8=0x0[libnative-lib.so 0x29980] [a8c31738] 0x40029980: "sturb w8, [x29, #-0x84]"-----w8=0x0    x29=0xbffff700        //w8=0x0


這里ldr拿到了我們入參到指針后,跳轉到了22390這個位置的函數來處理。接下來我們把這個函數也trace一下。


private boolean crypt2(String data){    //dump ldr的數據。包括ldr賦值給寄存器的如果是指針,也會dump    GlobalData.is_dump_ldr=true;    //dump str的數據    GlobalData.is_dump_str=true;    KingTrace trace1=new KingTrace(emulator);    trace1.initialize(0x40029750,0x40029750+0x30c,null);    emulator.getBackend().hook_add_new(trace1,0x40029750,0x40029750+0x30c,emulator);     KingTrace trace2=new KingTrace(emulator);    trace2.initialize(0x40022390,0x40022390+0x2574,null);    emulator.getBackend().hook_add_new(trace2,0x40022390,0x40022390+0x2574,emulator);    Boolean res = mainActivityDvm.callStaticJniMethodBoolean(emulator, "crypt2(Ljava/lang/String;)Z",data);    return res;}


然后ida打開看一下這個函數里面的大致內容。然后發現里面調用了strcmp,這個很明顯用來對比的函數,看下這個函數的地址如下:



這里說明調用FA0這個地址函數的,就是對比的函數,我們可以選擇用unidbg來進行hook打印,也可以斷點查入參數來查看,接著搜索一下trace的記錄,發現這個函數調用只有一處。


[libnative-lib.so 0x22464] [a8035cf8] 0x40022464: "ldur x8, [x29, #-0x40]"-----x8=0x1    x29=0xbffff5e0        //x8=0xbffff140[libnative-lib.so 0x22468] [00011ff8] 0x40022468: "stur x0, [x8, #-0x10]"-----x0=0x40359000    x8=0xbffff140        //x0=0x40359000[libnative-lib.so 0x2246c] [490000d0] 0x4002246c: "adrp x9, #0x4002c000"-----x9=0x1        //x9=0x4002c000[libnative-lib.so 0x22470] [206540f9] 0x40022470: "ldr x0, [x9, #0xc8]"-----x0=0x40359000    x9=0x4002c000        //x0=0x40358000 >-----------------------------------------------------------------------------<[23:12:20 478]ldr_left_address:40358000 dump, md5=b9f0352c6f0897767968eee7fdbed86f, hex=5746566c4e6a593200000000000000000000000000000000000000000000000000000000000000000000000000000000size: 480000: 57 46 56 6C 4E 6A 59 32 00 00 00 00 00 00 00 00    WFVlNjY2........0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................^-----------------------------------------------------------------------------^[libnative-lib.so 0x22474] [01015ff8] 0x40022474: "ldur x1, [x8, #-0x10]"-----x1=0xb    x8=0xbffff140        //x1=0x40359000[libnative-lib.so 0x22478] [ca7aff97] 0x40022478: "bl #0x40000fa0"[libnative-lib.so 0x2247c] [00000071] 0x4002247c: "subs w0, w0, #0"-----w0=0xffffffdf    w0=0xffffffdf        //w0=0xffffffdf


所以查一下這里進行對比的兩個值,我是在調試中查看的,結果如下:


>-----------------------------------------------------------------------------<[23:13:25 680]x0=RW@0x40358000, md5=29dd7f057f3a9dda3b877e393e53b6da, hex=5746566c4e6a59320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000size: 1120000: 57 46 56 6C 4E 6A 59 32 00 00 00 00 00 00 00 00    WFVlNjY2........0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................^-----------------------------------------------------------------------------^mx1 >-----------------------------------------------------------------------------<[23:13:27 638]x1=RW@0x40359000, md5=b500df35613831c3228d09ff59574abc, hex=5957467a5a4445784d6a4d324e6a593d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000size: 1120000: 59 57 46 7A 5A 44 45 78 4D 6A 4D 32 4E 6A 59 3D    YWFzZDExMjM2NjY=0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................


看著是兩個base64,轉換一下后得到:


WFVlNjY2     ->     XUe666YWFzZDExMjM2NjY=   ->    aasd1123666


那么這個結果就拿到了。直接用XUe來調用jnicheck函數,就成功了。

 

實際上這個結果我們用ida搜索666的字符串也是可以拿到的。不過我還是走一下分析的流程。

 

=========================================================

 

stalker的部分,其實主要還是trace匯編記錄出來,分析流程大致相同,而stalker早期我有進行工具的整合,處理過這一部分。我把當時寫的部分拿貼一下吧。


//這個地方實際上是每個block觸發的,所以我們在觸發的時候,要把整個block內的指令全部傳遞給pyfunction stalkerTraceRange(tid, base, size) {    Stalker.follow(tid, {        transform: (iterator) => {             const instruction = iterator.next();            const startAddress = instruction.address;            const isModuleCode = startAddress.compare(base) >= 0 &&                startAddress.compare(base.add(size)) < 0;            // const isModuleCode = true;              //遍歷出所有指令            do {                iterator.keep();                if (isModuleCode) {                      //將指令傳遞給py                    var address=ptr(instruction["address"]-moduleBase);                    send({                        type: 'inst',                        tid: tid,                        block: startAddress,                        val: JSON.stringify(instruction),                        jsname:"sktrace",                        moduleBase:moduleBase,                        address:address,                    })                                        //block執行結束后,再給python發個包通知下。                    iterator.putCallout((context) => {                        var callOutAddress=ptr(context.pc-moduleBase)                        send({                            type: 'ctx',                            tid: tid,                            val: JSON.stringify(context),                            jsname:"sktrace",                            moduleBase:moduleBase,                            address:callOutAddress                        })                     })                 }            } while (iterator.next() !== null);            // if(flag){            //     send(data)            // }        }    })} //對指定地址進行tracefunction traceAddr(addr) {    let moduleMap = new ModuleMap();    let targetModule = moduleMap.find(addr);    var msg=initMessage();    msg["data"]=JSON.stringify(targetModule);    send(msg);    let exports = targetModule.enumerateExports();    let symbols = targetModule.enumerateSymbols();     Interceptor.attach(addr, {        onEnter: function(args) {            this.tid = Process.getCurrentThreadId()              // 這里兩種模式,有一個是c的模式來處理,方便直接在js中對二進制進行輸出打印。            // stalkerTraceRangeC(this.tid, targetModule.base, targetModule.size)              // 這個模式是將結果傳遞到py里面進行輸出            stalkerTraceRange(this.tid, targetModule.base, targetModule.size)        },        onLeave: function(ret) {            Stalker.unfollow(this.tid);            Stalker.garbageCollect()            send({                type: "fin",                tid: this.tid,                jsname:"sktrace"            })        }    })} //對指定符號函數,或者是指定地址進行tracefunction trace(symbol,offset){    const targetModule = Process.getModuleByName(libname);    moduleBase=targetModule.base;    let targetAddress = null;      //如果填了符號函數名,就優先根據函數名查找地址    if(symbol.length>0) {        targetAddress = targetModule.findExportByName(symbol);    } else if(offset.length>0) {        var offsetData=parseInt(offset,16);        targetAddress = targetModule.base.add(ptr(offsetData));    }    traceAddr(targetAddress)}


上面處理完了在js中獲取每個block的指令,下面就是看py中如何處理解析這些指令進行打印。


def sktrace_message(self,p):              # 根據我們上面定義的結構,逐步解析        if "data" in p:            self.outlog(p["data"])            return        optype=p["type"]        # 如果這條推送數據是block中的指令        if optype=="inst":            # print(p)            inst=json.loads(p["val"])            address=int(p["address"],16)            oplist=[]            for opdata in inst["operands"]:                if opdata["type"]=="reg":                    if opdata["value"] not in oplist:                        oplist.append(opdata["value"])                elif opdata["type"]=="mem":                    memdata=opdata["value"]                    if memdata["base"] not in oplist:                        oplist.append(memdata["base"])            enddata = ""            for item in oplist:                enddata+="%s={%s} "%(item,item)            outdata="tid:%s address:%s %s %s\t\t//%s"%(str(p["tid"]),str(hex(address)),inst["mnemonic"],inst["opStr"],enddata)            self.outlog(outdata)        elif optype=="ctx":        # 如果這個是當前block結束的通知            context=json.loads(p["val"])            address=int(p["address"],16)            self.outlog("tid:" +str(p["tid"])+" address:"+str(hex(address))+" context:"+ p["val"])        else:            self.outlog(json.dumps(p))