自研Unidbg trace工具实战ollvm反混淆
得益于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=null
java.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]0x25904
JNIEnv->GetStringUtfChars("aasd1123") was called from RX@0x400270a4[libnative-lib.so]0x270a4
JNIEnv->NewStringUTF("aasd1123666") was called from RX@0x40027640[libnative-lib.so]0x27640
JNIEnv->GetStringUtfChars("aasd1123666") was called from RX@0x400270a4[libnative-lib.so]0x270a4
JNIEnv->FindClass(com/kanxue/crackme/MainActivity) was called from RX@0x40028008[libnative-lib.so]0x28008
JNIEnv->GetStaticMethodID(com/kanxue/crackme/MainActivity.crypt2(Ljava/lang/String;)Z) => 0xf66a2c58 was called from RX@0x40028d98[libnative-lib.so]0x28d98
Find native function Java_com_kanxue_crackme_MainActivity_crypt2 => RX@0x40029750[libnative-lib.so]0x29750
JNIEnv->GetStringUtfChars(""aasd1123666"") was called from RX@0x400270a4[libnative-lib.so]0x270a4
JNIEnv->CallStaticBooleanMethodV(class com/kanxue/crackme/MainActivity, crypt2("aasd1123666") => false) was called from RX@0x400291c8[libnative-lib.so]0x291c8
false
接下来就是开启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=616173643131323336363600000000000000000000000000000000000000000000000000000000000000000000000000
size: 48
0000: 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=5746566c4e6a593200000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 48
0000: 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=5746566c4e6a59320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 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=5957467a5a4445784d6a4d324e6a593d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 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 -> XUe666
YWFzZDExMjM2NjY= -> aasd1123666
那么这个结果就拿到了。直接用XUe来调用jnicheck函数,就成功了。
实际上这个结果我们用ida搜索666的字符串也是可以拿到的。不过我还是走一下分析的流程。
=========================================================
stalker的部分,其实主要还是trace汇编记录出来,分析流程大致相同,而stalker早期我有进行工具的整合,处理过这一部分。我把当时写的部分拿贴一下吧。
//这个地方实际上是每个block触发的,所以我们在触发的时候,要把整个block内的指令全部传递给py
function 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)
// }
}
})
}
//对指定地址进行trace
function 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"
})
}
})
}
//对指定符号函数,或者是指定地址进行trace
function 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))
这段处理是来自我之前写的项目https://github.com/dqzg12300/fridaUiTools.git
简单测试过没啥问题。这里我就不详细测试这个作业了。
看雪ID:misskings
https://bbs.kanxue.com/user-home-659397.htm
# 往期推荐
3、如何用纯猜的方式逆向喜马拉雅xm文件加密(wasm部分)
球分享
球点赞
球在看