补环境的三条准则
常规object,比如jstring,jarray使用Unidbg封装的API
除此之外的jobject使用vm.resolveClass(className).newObject(object),jclass使用vm.resolveClass(className)
如果object不可得,传空/方法签名/标识
目录
基本框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private final AndroidEmulator emulator;private final VM vm;private final Module module ;xxxx() { emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("xxxxx" ).build(); final Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 )); vm = emulator.createDalvikVM(new File ("apk路径" )); DalvikModule dm = vm.loadLibrary(new File ("so路径" ), true ); module = dm.getModule(); vm.setJni(this ); vm.setVerbose(true ); dm.callJNI_OnLoad(emulator); };
AndroidEmulator AndroidEmulator介绍 AndroidEmulator是一个抽象的Android模拟器接口。它作为一个枢纽,协调unidbg中大多数模块相互工作,至关重要。我们需要的大多数操作也都需要借助于它。
创建AndroidEmulator实例 1 2 3 4 5 6 7 8 9 10 11 AndroidEmulator build = AndroidEmulatorBuilder .for32Bit() .addBackendFactory(new DynarmicFactory (true )) .setProcessName("com.github.unidbg" ) .setRootDir(new File ("target/rootfs/default" )) .build();
AndroidEmulator操作接口 getMemory 1 2 Memory memory = emulator.getMemory();
获取内存操作接口,关于Memory接口介绍查看Memory 。
getPid 1 2 int pid = emulator.getPid();
createDalvikVM 1 2 3 4 VM vm = emulator.createDalvikVM();VM vm = emulator.createDalvikVM(new File ("apk file path" ));
创建虚拟机,关于VM的接口查看VM介绍
loadLibrary 1 2 3 4 5 Module module = emulator.loadLibrary(new File ("Elf文件路径" ), true );Module module = emulator.loadLibrary(new File ("Elf文件路径" ));
getDalvikVM 1 2 VM vm = emulator.getDalvikVM();
获取已创建的虚拟机。
showRegs 1 2 3 4 emulator.showRegs(); emulator.showRegs(ArmConst.UC_ARM_REG_R0, ArmConst.UC_ARM_REG_R1);
打印当前寄存器信息。
getBackend 1 2 Backend backend = emulator.getBackend();
获取后端,关于后端请查看Backend 介绍
getProcessName 1 2 String processName = emulator.getProcessName();
获取进程名。
getContext 1 2 RegisterContext context = emulator.getContext();
获取寄存器上下文,关于寄存器上下文介绍请查看RegisterContext介绍 。
isRunning 1 2 boolean running = emulator.isRunning();
是否正在运行。
Trace 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 emulator.traceCode(); emulator.traceCode(1 , 0 ); emulator.traceCode(1 , 0 , new TraceCodeListener () { @Override public void onInstruction (Emulator<?> emulator, long address, Instruction insn) { String mnemonic = insn.getMnemonic(); if (mnemonic.equals("svc" ) || mnemonic.equals("swi" )){ System.out.println("0x" +Long.toHexString(address)+"执行了svc指令" ); } } }); emulator.traceRead(); emulator.traceWrite();
注意:Trace功能仅支持Unicorn后端引擎 如使用带范围的Trace,起始位置大于结束位置将全部进行Trace。与无参方法作用相同。每种Trace都包含了三种重载,参照traceCode()即可。
Set/Get 1 2 emulator.set("Test" ,this ); DocTest test = emulator.<DocTest>get("Test" );
emulator内部维护了一个Map,如有全局的对象需要存储,可借助这两个API来操作,无需额外维护Map对象
SyscallHandler 1 SyscallHandler<AndroidFileIO> syscallHandler = emulator.getSyscallHandler();
获取系统调用处理器,其API请查看SyscallHandler介绍 。
Debugger 1 2 3 4 5 6 7 8 9 10 11 12 Debugger attach = emulator.attach();Debugger attach = emulator.attach(DebuggerType.CONSOLE);public enum DebuggerType { CONSOLE, GDB_SERVER, ANDROID_SERVER_V7 }
附加调试器。关于调试器请查看Debugger介绍 。
emulateSignal 1 2 boolean success = emulator.emulateSignal(2 );
模拟向进程发送一个信号。
disassemble 1 2 3 4 5 6 byte [] code = new byte []{0x00 , 0x20 };Instruction[] disassemble = emulator.disassemble(0 , code, true , 1 );
如需灵活转换,请使用Capstone,此处只是Capstone的简单封装。disassemble方法的另一重载不推荐大家使用,为Trace而封装的,了解即可。
close
调用此方法来释放Android虚拟机使用的资源。
getUnwinder 1 emulator.getUnwinder().unwind();
此方法用来打印调用栈
Memory Memory介绍 Memory内存接口主要提供了两个功能
下面我们来介绍下Memory操作的主要接口。
Memory操作 获取Memory实例 1 2 Memory memory = emulator.getMemory();
setLibraryResolver 1 2 memory.setLibraryResolver(new AndroidResolver (23 ));
此接口属于必调用接口。 设置SDK版本,管理了Android提供的常用基本so库。如不调用此方法,在处理Elf文件的依赖时,需要自行处理依赖问题。只有So中无任何依赖的情况下可以不调用此方法。
findModule 1 2 3 4 5 6 Module module = memory.findModule("libc.so" );Module module = memory.findModuleByAddress(address);Collection<Module> loadedModules = memory.getLoadedModules();
上面两个方法都为获取已加载的模块,需在自行加载模块之后调用。
allocateStack 1 2 3 4 5 6 UnidbgPointer pointer = memory.allocateStack(0x10 );UnidbgPointer pointer = memory.writeStackBytes(new byte []{0x01 , 0x02 });UnidbgPointer pointer = memory.writeStackString("doc" );
不要在程序运行期间修改栈,如果你不清楚在做什么,最好不要自行开辟栈空间,unidbg会自行维护。
malloc 1 2 3 4 5 6 7 MemoryBlock malloc = memory.malloc(0x10 , true );UnidbgPointer pointer = malloc.getPointer();malloc.free();
参数二的runtime标志:true表示使用mmap按页大小分配,相应的调用MemoryBlock.free方法则使用munmap释放。false表示使用libc.malloc分配,相应的调用MemoryBlock.free方法则使用libc.free释放。
setErrno 1 memory.setErrno(UnixEmulator.EPERM);
如需在模拟执行某函数前需要指定errno,可使用此方法设置。
getStackPoint 1 long stackPoint = memory.getStackPoint();
返回当前SP寄存器指向的地址。
disableCallInitFunction 1 2 3 memory.disableCallInitFunction(); memory.setCallInitFunction(false );
禁用Elf加载时调用初始化函数。需配合加载模块的forceCallInit参数使用。如不进行上述设置,默认为true。
getMemoryMap 1 2 3 4 5 Collection<MemoryMap> memoryMap = memory.getMemoryMap(); for (MemoryMap map : memoryMap){ System.out.println(map); }
获取内存映射信息,所有正在使用的内存块都维护在此Map中。
pointer 1 2 UnidbgPointer pointer = memory.pointer(0x40000000 );UnidbgPointer pointer2 = UnidbgPointer.pointer(emulator, 0x40000000 );
在unidbg中,可以借助unidbgPointer封装的API来读写内存,此API供已知地址的情况下,获得一个指针指向已知地址,继而进行操作。可以理解为指哪打哪。具体内存读写操作请查看UnidbgPointer 。
addModuleListener 1 2 3 4 5 6 memory.addModuleListener(new ModuleListener () { @Override public void onLoaded (Emulator<?> emulator, Module module ) { System.out.println(module .name + "已被加载" ); } });
添加一个模块加载监听器,当一个新模块被加载后,会回调ModuleListener的onLoaded方法,用来感知某模块加载。一般用于想在某个模块加载后接着进行hook的操作。需在模块加载前设置,否则无效。
load 1 Module module = memory.load(new File ("文件地址" ));
其余几个重载就不介绍了,emulator接口提供的模块加载API和VM提供的加载API底层都基于此,仅是对此API的一层封装。
Memory总结 Memory模块封装了关于内存管理的各个接口,其余未介绍的方法不推荐大家使用,多为unidbg内部封装的功能。
VM VM介绍 如果要加载的模块中存在JNI交互的场景,需要创建VM。VM主要的作用就是代理了一套JNI,在模拟执行的过程中,如果需要借助JNI来操作Java层,就少不了VM的帮助。
VM创建 在AndroidEmulator 一节我们介绍过,主要有两种创建VM的方式
1 2 3 4 VM dalvikVM = emulator.createDalvikVM();VM dalvikVM = emulator.createDalvikVM(new File ("apk file path" ));
推荐使用指定APK文件进行创建,某些API需要指定APK文件才可使用。
VM接口 日志控制
此开关只控制VM的日志,主要表现为JNI交互的详细日志。
设置JNI
如程序中存在使用JNI的接口,必须设置JNI。推荐自实现的类继承AbstractJni类,此类封装了绝大部份常用的方法,免去部分繁杂的内容。如签名校验(需指定APK文件创建VM)、常用JDK操作等。 关于JNI部分请查看JNI介绍
调用JNI_OnLoad函数 1 2 3 dalvikVM.callJNI_OnLoad(emulator,module );
对JNI_OnLoad函数进行模拟执行。如某些JNI方法在JNI_OnLoad函数进行动态绑定需调用此函数后才能够调用JNI方法,推荐加载模块后紧接执行此方法。
获取模拟器实例 1 Emulator<?> emulator = dalvikVM.getEmulator();
获取APK相关信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 byte [] bytes = dalvikVM.openAsset("fileName" );String packageName = dalvikVM.getPackageName();String versionName = dalvikVM.getVersionName();long versionCode = dalvikVM.getVersionCode();String manifestXml = dalvikVM.getManifestXml();
加载模块 1 2 3 4 5 6 7 8 DalvikModule docModule = dalvikVM.loadLibrary("doc" , true );DalvikModule docModule = dalvikVM.loadLibrary("doc" , soFile.getBytes(), true );
VM同样能够加载Elf模块,具体的差异就是如果指定了APK文件,可以使用上述第一个重载方法指定模块名称,unidbg会自行从APK文件中提取该So文件进行加载。第二个重载方法使用较少,指定byte[]数据进行加载,名称由参数一指定。 返回一个DalvikModule实例,可调用JNI_OnLoad函数:
1 docModule.callJNI_OnLoad(emulator);
该实例还维护了一个Module实例,可使用下面方法获取。
1 Module module = docModule.getModule();
有关Module实例请查看Module
VM总结 上述的几个接口主要来控制VM或获取APK文件信息,VM其中的方法不止于这些,我们留到JNI部分来介绍,因为其他的几个方法都是有关于JNI交互场景的,也就是unidbg中的环境问题。 unidbg补环境、JNI交互请查看JNI介绍
CallMethod 执行JNI函数
我们首先来写一段小案例来说明如何在unidbg中执行JNI函数
Java层:
1 2 3 4 5 6 7 8 package com.github.unidbg;public class Test { static { System.loadLibrary("native-doc" ); } public native String jnitest (String arg) ; }
So层:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <jni.h> #include <string> using namespace std; extern "C" JNIEXPORT jstring JNICALL Java_com_github_unidbg_Test_jnitest (JNIEnv *env, jobject thiz, jstring arg) { const char *c_arg = env->GetStringUTFChars(arg, nullptr); string hello = "unidbg:" ; hello.append(c_arg); return env->NewStringUTF(hello.c_str()); }
非常简单的一段小例子,我们就以该例子来说明如何调用libnative-doc.so中的jnitest函数。
执行任意函数 当我们想执行的函数并非导出函数,也可使用地址进行调用,请自行参考unidbg对JNI函数调用的封装,自行实现此部分。
执行main函数 如果加载的文件是一个可执行文件,需要持有该文件被加载后返回的Module实例来调用callEntry方法,请查看Module
JNI方法封装 void 1 2 3 4 5 DvmClass testClass = vm.resolveClass("com/github/unidbg/Test" );DvmObject<?> obj = testClass.newObject(null ); testClass.callStaticJniMethod(emulator, "signature" , args); obj.callJniMethod(emulator,"signature" , args);
boolean 1 2 3 4 5 DvmClass testClass = vm.resolveClass("com/github/unidbg/Test" );DvmObject<?> obj = testClass.newObject(null ); boolean ret = testClass.callStaticJniMethodBoolean(emulator, "signature" , args);boolean ret = obj.callJniMethodBoolean(emulator, "signature" , args);
int 1 2 3 4 5 DvmClass testClass = vm.resolveClass("com/github/unidbg/Test" );DvmObject<?> obj = testClass.newObject(null ); int ret = testClass.callStaticJniMethodInt(emulator, "signature" , args);int ret = obj.callJniMethodInt(emulator, "signature" , args);
long 1 2 3 4 5 DvmClass testClass = vm.resolveClass("com/github/unidbg/Test" );DvmObject<?> obj = testClass.newObject(null ); long ret = testClass.callStaticJniMethodLong(emulator, "signature" , args);long ret = obj.callJniMethodLong(emulator, "signature" , args);
float 1 2 3 4 DvmClass testClass = vm.resolveClass("com/github/unidbg/Test" );DvmObject<?> obj = testClass.newObject(null ); int ret = testClass.callStaticJniMethodInt(emulator, "signature" , args);int ret = obj.callJniMethodInt(emulator, "signature" , args);
先拿到4字节的int数据,然后将其转换
1 2 3 4 ByteBuffer allocate = ByteBuffer.allocate(4 );allocate.putInt(ret); allocate.flip(); float f_ret = allocate.getFloat();
double 1 2 3 4 DvmClass testClass = vm.resolveClass("com/github/unidbg/Test" );DvmObject<?> obj = testClass.newObject(null ); long ret = testClass.callStaticJniMethodLong(emulator, "signature" , args);long ret = obj.callJniMethodLong(emulator, "signature" , args);
先拿到8字节的long数据,然后将其转换
1 2 3 4 ByteBuffer allocate = ByteBuffer.allocate(8 );allocate.putLong(ret); allocate.flip(); double d_ret = allocate.getDouble();
对象类型 1 2 3 4 5 DvmClass testClass = vm.resolveClass("com/github/unidbg/Test" );DvmObject<?> obj = testClass.newObject(null ); DvmObject<?> ret = testClass.callStaticJniMethodObject(emulator, "signature" , args); DvmObject<?> ret = obj.callJniMethodObject(emulator, "signature" , args);
基本类型外的所有类型都为对象类型,包括基本类型的数组类型
JNI方法签名 通过类名与方法签名的配合,才能定位到唯一的函数。所以一定要将方法签名填写正确,unidbg才可以找到对应的函数,所有的类型都按JNI本身定义的签名一致。
Java类型
类型签名
boolean
Z
byte
B
char
C
long
J
float
F
double
D
short
S
int
I
类
L全限定类名;
数组
[元素类型签名
unidbg方法签名如下格式: 方法名(参数列表)返回值 举个例子:
1 public native String jnitest (String arg) ;
对应的方法签名为:
1 jnitest(Ljava/lang/String;)Ljava/lang/String;
Hook Hook介绍 Hook模块为unidbg引入第三方Hook框架而封装的一个模块,使用它可以快速对目标So进行hook操作。而API基本与第三方Hook框架保持一致。如果是32位程序,要注意Hook目标地址是否为Thumb模式,手动处理+1操作。
Hook框架分类 HookZz HookZz为Dobby的前身,32位模式下推荐使用HookZz。支持inline hook。
获取HookZz实例 1 HookZz hook = HookZz.getInstance(emulator);
在使用任何Hook框架之前都需要拿到unidbg对该Hook框架的封装实例。
HookZz API wrap wrap的作用相当于在目标地址进行一层包装,回调到我们的代码,可以对其打印当前参数或寄存器的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 HookZz hookZz = HookZz.getInstance(emulator);WrapCallback<HookZzArm32RegisterContextImpl> wrapCallback = new WrapCallback <HookZzArm32RegisterContextImpl>() { @Override public void preCall (Emulator<?> emulator, HookZzArm32RegisterContextImpl ctx, HookEntryInfo info) { System.out.println("arg0:" + ctx.getIntArg(0 )); } @Override public void postCall (Emulator<?> emulator, HookZzArm32RegisterContextImpl ctx, HookEntryInfo info) { System.out.println("ret:" + ctx.getIntArg(0 )); } }; long functionAddress = module .base + 0xC09D ;hookZz.wrap(functionAddress, wrapCallback); Symbol symbol = module .findSymbolByName("symbolName" );hookZz.wrap(symbol, wrapCallback);
replace replace可以进行替换目标函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 HookZz hookZz = HookZz.getInstance(emulator);ReplaceCallback replaceCallback = new ReplaceCallback () { @Override public HookStatus onCall (Emulator<?> emulator, HookContext context, long originFunction) { int arg0 = context.getIntArg(0 ); System.out.println("arg0:" +arg0); context.push(context.getPointerArg(1 )); EditableArm32RegisterContext ctx = (EditableArm32RegisterContext) context; ctx.setR2(0 ); long newFuncAddr = module .base + 0xDC0 ; return HookStatus.RET(emulator, newFuncAddr); } @Override public void postCall (Emulator<?> emulator, HookContext context) { UnidbgPointer arg2 = context.pop(); super .postCall(emulator, context); } }; long functionAddress = module .base + 0xC09D ;hookZz.replace(functionAddress, replaceCallback); hookZz.replace(functionAddress, replaceCallback, true ); Symbol symbol = module .findSymbolByName("symbolName" );hookZz.replace(symbol, replaceCallback); hookZz.replace(symbol, replaceCallback, true );
instrument instrument提供了一种对单行汇编进行Hook的能力,使用场景较少。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 HookZz hookZz = HookZz.getInstance(emulator);InstrumentCallback<RegisterContext> instrumentCallback = new InstrumentCallback <RegisterContext>() { @Override public void dbiCall (Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) { System.out.println(ctx.getIntArg(0 )); } }; long functionAddress = module .base + 0xC09D ;hookZz.instrument(functionAddress, instrumentCallback); Symbol symbol = module .findSymbolByName("symbolName" );hookZz.instrument(symbol, instrumentCallback);
Dobby 64位模式下推荐使用Dobby进行inline hook。
获取Dobby实例 1 Dobby dobby = Dobby.getInstance(emulator);
Dobby API replace 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Dobby dobby = Dobby.getInstance(emulator);long functionAddress = module .base + 0xC09D ;dobby.replace(functionAddress, new ReplaceCallback () { @Override public HookStatus onCall (Emulator<?> emulator, HookContext context, long originFunction) { Pointer result = context.getPointerArg(0 ); System.out.println("input:" + result.getString(0 )); return super .onCall(emulator, context, originFunction); } @Override public void postCall (Emulator<?> emulator, HookContext context) { super .postCall(emulator, context); } },true );
Dobby.replace的用法同HookZz,详细使用请查看HookZz-replace
instrument 同HookZz中的instrument 。
xHook xHook框架实现原理本身就存在局限性,只能够hook符号表函数。有好有坏,它的优点就是稳定。
获取xHook实例 1 IxHook hook = XHookImpl.getInstance(emulator);
xHook API register 1 2 IxHook hook = XHookImpl.getInstance(emulator);
xHook的register方法的第一个参数为pathname_regex_str, 其支持POSIX BRE (Basic Regular Expression) 定义的正则表达式语法。用来检索符合条件的So。第二个参数为符号名。详情请看xhook介绍
refresh
在使用xHook进行hook时,务必调用此方法进行刷新才能生效。
Whale Whale Hook框架为跨平台的Hook框架,在Android中的实现为inline hook。
获取Whale实例 IWhale whale = Whale.getInstance(emulator);
Whale API TODO
原生hook 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 emulator.attach().addBreakPoint(module .findSymbolByName("memcmp" ).getAddress(), new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { System.out.println("callMemcmp作比较" ); RegisterContext registerContext = emulator.getContext(); UnidbgPointer arg1 = registerContext.getPointerArg(0 ); UnidbgPointer arg2 = registerContext.getPointerArg(1 ); int length = registerContext.getIntArg(2 ); Inspector.inspect(arg1.getByteArray(0 , length), "arg1" ); Inspector.inspect(arg2.getByteArray(0 , length), "arg2" ); if (arg1.getString(0 ).equals("Context" )){ emulator.attach().debug(); } return true ; } });
patch 第一种,使用arm转机器码 转换网址 https://armconverter.com/
1 2 3 4 5 public void patch () { UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module .base + 0x3E8 ); byte [] code = new byte []{(byte ) 0xd0 , 0x1a }; pointer.write(code); }
第二种,使用keystone
1 2 3 4 5 6 7 8 public void patch2 () { UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module .base + 0x3E8 ); Keystone keystone = new Keystone (KeystoneArchitecture.Arm, KeystoneMode.ArmThumb); String s = "subs r0, r2, r3" ; byte [] machineCode = keystone.assemble(s).getMachineCode(); pointer.write(machineCode); }
JNI JNI介绍 当一个So在模拟执行的过程中,它要与Java层进行交互,比如获取Java层某个字段的值、调用Java层的某个方法获取返回值等等,如果你不告诉unidbg如何做,那程序就无法执行下去。你告诉undibg碰到访问Java层的某些东西的时候该怎么做的这个过程,从而让程序正确的执行下去,就叫做补环境。 我们在JNI部分来介绍unidbg如何来补环境,首先我们要来看unidbg中的几个概念
类/类型 Java中可以抽象出一个类,一个类可以创建对象,可以调用该类上的静态方法。一个类的基本属性有很多,unidbg模拟了父类和接口。在unidbg中对应的为DvmClass。
1 2 3 4 DvmClass Test = vm.resolveClass("com/github/unidbg/Test" );DvmClass dvmClass = vm.resolveClass("com/github/unidbg/TestUnidbg" , Test);
VM的resolveClass方法会创建出一个类型,可以理解为Java中的一个类,参数为全限定类名,将.换为/。这样就创建出来一种类型了。我们可以基于此类型创建对象。
对象/实例 Java层肯定会有对象的,或者叫做实例,unidbg中对应的叫做DvmObject。unidbg中所有的Java层的对象都直接或间接的继承自DvmObject。模拟创建对象:
第一种方法
1 2 3 DvmClass dvmClass = vm.resolveClass("com/github/unidbg/Test" );DvmObject<?> obj = dvmClass.newObject(null );
在Java中,有了类,就可以new对象了对吧。unidbg的newObject操作就类似这样。它可以有了一种类型后进行创建对象,其中的参数你可以存储任意类型的数据,此数据就会与该DvmObject绑定。
第二种方法
1 2 3 4 DvmObject<?> obj = ProxyDvmObject.createObject(vm, this );
unidbg会根据此value值的类型来创建DvmObject。如果value为基本数据类型则会被封装为该基本类型的包装类型。如果value为一个对象,则会获取该对象所对应的Class,根据Class名称来自动创建该类型,然后创建一个对象,相当于对第一种方法的一个封装。如果为数组类型,则会被封装为由unidbg创建的包装对象。
包装对象 理论上所有的Java层的对象在unidbg中都可以按照上面所述的方法进行创建。但unidbg内部有部分封装,所以在使用的时候我们要使用unidbg的包装对象
String String类型被封装为StringObject
1 new StringObject (vm,"unidbg" );
Array 数组类型在使用的时候按如下使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ByteArray byteArray = new ByteArray (vm, new byte []{1 , 2 , 3 });IntArray intArray = new IntArray (vm, new int []{1 , 2 , 3 });ShortArray shortArray = new ShortArray (vm, new short []{1 , 2 , 3 });FloatArray floatArray = new FloatArray (vm, new float []{1.0f , 2.0f , 3.0f });DoubleArray doubleArray = new DoubleArray (vm, new double []{1.0d , 2.0d , 3.0d });new ArrayObject (byteArray, intArray);
List List类型的数据被封装为ArrayListObject
1 2 3 4 DvmClass Test = vm.resolveClass("com/github/unidbg/Test" );List<DvmObject<?>> list = new ArrayList <>(); list.add(Test.newObject(null )); new ArrayListObject (vm,list);
Module Module介绍 在undibg中,经过Memory加载到内存中的Elf文件,都被抽象为一个Module(LinuxModule)对象。无论是Emulator还是VM加载的,底层都是对Memory的load进行了封装,返回值就是一个Module实例。
Module常用的属性 base 1 2 Module module = emulator.loadLibrary(file, true );System.out.println("加载的基址: " +module .base);
base属性维护了该Elf文件被加载到内存中的基址。
name 1 2 Module module = emulator.loadLibrary(file, true );System.out.println("模块的名称: " +module .name);
name属性维护了该模块的名称,通常为文件名。
callEntry 1 2 Module module = emulator.loadLibrary(file, true );int ret = module .callEntry(emulator);
当被加载的文件是可执行文件时,需要通过该方法来调用main函数。
findSymbolByName 1 2 Module module = emulator.loadLibrary(file, true );Symbol symbol = module .findSymbolByName("symbolName" );
通过名字查找当前模块中的符号。
Backend Backend介绍 unidbg内置了五种Backend。Backend的作用是用来模拟执行机器指令。它作为一个模块被unidbg引入,提供了模拟CPU的能力,且被封装为统一的Backend接口。
Backend列表 unidbg默认使用Unicorn后端。即在创建AndroidEmulator时无额外添加BackendFactory即为Unicorn后端。
Unicorn/Unicorn2 Unicorn官网
使用Unicorn2后端: emulator = AndroidEmulatorBuilder .for32Bit() .addBackendFactory(new Unicorn2Factory(true)) .build(); Unicorn2Factory构造方法的参数表示在Unicorn2创建失败时,是否退回使用Unicorn后端。如设置false,在Unicorn2后端创建失败后就退出,而不选择使用Unicorn后端。
Unicorn后端特点
执行较其他后端慢
支持原生Hook
支持Debugger功能
支持原生Trace
对调试、Trace功能支持较好,缺点是执行较慢。推荐在实验环境中使用。
Dynarmic Dynarmic官网
使用Dynarmic后端 1 2 3 4 emulator = AndroidEmulatorBuilder .for32Bit() .addBackendFactory(new DynarmicFactory (true )) .build();
Dynarmic后端特点
生产环境推荐使用,速度比Unicorn快很多。
Hypervisor Hypervisor官网
使用Hypervisor后端 1 2 3 4 emulator = AndroidEmulatorBuilder .for32Bit() .addBackendFactory(new HypervisorFactory (true )) .build()
Hypervisor后端特点
Kvm Kvm官网
使用Kvm后端 1 2 3 4 emulator = AndroidEmulatorBuilder .for64Bit() .addBackendFactory(new KvmFactory (false )) .build();
Kvm后端特点
Backend接口 Backend为CPU的抽象接口,通过它可以直接读写CPU的寄存器的值、内存读写。unidbg对此类接口都有对应的封装,不建议直接使用此类接口。
Deggbuer Debugger介绍 重要提醒: 只有unicorn后端支持调试功能!!! 重要提醒: 只有unicorn后端支持调试功能!!! 重要提醒: 只有unicorn后端支持调试功能!!! 调试是在逆向工程中不可缺少的一个功能,可以帮助我们快速定位问题。在unidbg也是支持的,而且这部分功能基于原生Unicorn后端,支持非常好。 主要功能:
断点调试
内存读写
寄存器读写
单步调试
堆栈搜索
调用栈打印
反汇编
patch
ConsoleDebugger 使用步骤 第零步 再提示一下,先将后端切换回Unicorn。如何切换请查看: Backend
第一步 得到控制台调试器实例。
1 Debugger debugger = emulator.attach();
第二步 下断点。下断点有多种重载方便使用:
模块 + 偏移
模块 + 符号名
绝对地址1 2 3 4 5 6 7 8 9 debugger.addBreakPoint(module , 0x8607 ); debugger.addBreakPoint(module , "Java_com_github_unidbg_Test_sign" ); long address = module .base + 0x8607 ;debugger.addBreakPoint(address);
下断点无需手动thumb指令+1操作,手动+1下断点也是一样的效果。但手动+1会有其他的作用,如查看断点等功能,推荐手动+1。 还有其他三种断点重载,添加一个断点回调:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 BreakPointCallback breakPointCallback = new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { return false ; } }; debugger.addBreakPoint(module , 0x8607 , breakPointCallback); debugger.addBreakPoint(module , "Java_com_github_unidbg_Test_sign" , breakPointCallback); long address = module .base + 0x8607 ;debugger.addBreakPoint(address, breakPointCallback);
第三步 执行。无论使用哪种方式对目标地址下断点,在执行起来后如果执行到断点位置,就会在控制台断下等待命令的输入。以我目前例子,命中断点后输出如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 debugger break at: 0x40008606 @ Function32 address=0x40008607 , arguments=[unidbg@0xfffe12a0 [libadd.so]0x2a0 , 17037394 , 1 ] >>> r0=0xfffe12a0 (-126304 ) r1=0x103f852 r2=0x1 r3=0x2 r4=0x0 r5=0x0 r6=0x0 r7=0x0 r8=0x0 sb=0x0 sl=0x0 fp=0x0 ip=0x401315e0 >>> SP=0xbffff730 LR=unidbg@0xffff0000 PC=RX@0x40008606 [libnative-doc.so]0x8606 cpsr: N=0 , Z=1 , C=0 , V=0 , T=1 , mode=0b10000 >>> d0=0x0 (0.0 ) d1=0x3220302034203720 (3.002229861217884E-67 ) d2=0x3436333832203236 (3.5366761868402984E-57 ) d3=0x3120323938343135 (4.583358096989596E-72 ) d4=0x2030203020302030 (1.2027122125173386E-153 ) d5=0x2030203020302030 (1.2027122125173386E-153 ) d6=0x2030203020302030 (1.2027122125173386E-153 ) d7=0x2030203020302030 (1.2027122125173386E-153 ) >>> d8=0x0 (0.0 ) d9=0x0 (0.0 ) d10=0x0 (0.0 ) d11=0x0 (0.0 ) d12=0x0 (0.0 ) d13=0x0 (0.0 ) d14=0x0 (0.0 ) d15=0x0 (0.0 ) Java_com_github_unidbg_Test_sign + 0x0 => *[libnative-doc.so*0x08607 ]*[80b5 ]*0x40008606 :*"push {r7, lr}" [libnative-doc.so 0x08609 ] [6f46 ] 0x40008608 : "mov r7, sp" [libnative-doc.so 0x0860b ] [84b0 ] 0x4000860a : "sub sp, #0x10" [libnative-doc.so 0x0860d ] [0390 ] 0x4000860c : "str r0, [sp, #0xc]" [libnative-doc.so 0x0860f ] [0291 ] 0x4000860e : "str r1, [sp, #8]" [libnative-doc.so 0x08611 ] [0192 ] 0x40008610 : "str r2, [sp, #4]" [libnative-doc.so 0x08613 ] [0198 ] 0x40008612 : "ldr r0, [sp, #4]" [libnative-doc.so 0x08615 ] [4ff48061] 0x40008614 : "mov.w r1, #0x400" [libnative-doc.so 0x08619 ] [fff75aec] 0x40008618 : "blx #0x40007ed0" [libnative-doc.so 0x0861d ] [04b0 ] 0x4000861c : "add sp, #0x10" [libnative-doc.so 0x0861f ] [80bd ] 0x4000861e : "pop {r7, pc}" [libnative-doc.so 0x08621 ] [80b5 ] 0x40008620 : "push {r7, lr}" [libnative-doc.so 0x08623 ] [6f46 ] 0x40008622 : "mov r7, sp" [libnative-doc.so 0x08625 ] [82b0 ] 0x40008624 : "sub sp, #8" [libnative-doc.so 0x08627 ] [0190 ] 0x40008626 : "str r0, [sp, #4]" [libnative-doc.so 0x08629 ] [0198 ] 0x40008628 : "ldr r0, [sp, #4]"
主要信息有: 断点和当前函数参数信息,当前所有寄存器信息,当前位置的反汇编信息。 看到如上调试信息后,unidbg就会等待命令的输入,命令才是ConsoleDebugger的核心。下面我们就来看如何使用命令来操作ConsoleDebugger。
调试指令 help
会看到下面的输出,此为ConsoleDebugger的帮助文档。我们下面也是介绍此帮助文档的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 help c: continue n: step over bt: back trace st hex: search stack shw hex: search writable heap shr hex: search readable heap shx hex: search executable heap nb: break at next block s|si: step into s[decimal]: execute specified amount instruction s (blx) : execute util BLX mnemonic, low performancem (op) [size]: show memory, default size is 0x70 , size may hex or decimalmr0-mr7, mfp, mip, msp [size]: show memory of specified register m (address) [size]: show memory of specified address, address must start with 0xwr0-wr7, wfp, wip, wsp <value>: write specified register wb (address) , ws(address), wi(address) <value>: write (byte , short , integer) memory of specified address, address must start with 0xwx (address) <hex>: write bytes to memory at specified address, address must start with 0xb (address) : add temporarily breakpoint, address must start with 0x, can be module offsetb: add breakpoint of register PC r: remove breakpoint of register PC blr: add temporarily breakpoint of register LR p (assembly) : patch assembly at PC addresswhere: show java stack trace trace [begin end]: Set trace instructions traceRead [begin end]: Set trace memory read traceWrite [begin end]: Set trace memory write vm: view loaded modules vbs: view breakpoints d|dis: show disassemble d (0x) : show disassemble at specify addressstop: stop emulation run [arg]: run test gc: Run System.gc() cc size: convert asm from 0x40008608 - 0x40008608 + size bytes to c function
shr
内存查找字节
show disassemble
打印当前位置反编译信息。
打印指定地址处的反编译信息。
continue
继续执行。直至遇到下一个断点断下或运行结束。
step over
单步步过,不进入跳转指令。
step into
单步执行。执行一句指令。
执行10句指令。
执行到blx指令。性能较低。
next block
执行到下一block 处。
b
可在当前PC处下断点。
指定地址下断点。
指定偏移下断点,会根据当前位置的模块来计算。
在LR处下断点。
r
删除当前位置的断点。
blr 在函数返回时候下断点
vbs
查看当前下的所有断点。
back trace
打印当前位置调用栈。
show memory 1 2 3 mr0-mr7 | mfp | mip | msp [size] mr1 0x10
读取对应寄存器所指向的地址的内存。size指定大小,默认0x70大小。
1 2 3 m(address) [size] m0xbffff730 10
读取指定地址的内存。地址必须为0x开头的十六进制数。
write memory 1 2 3 wr0-wr7, wfp, wip, wsp <value> wr2 0x100
向对应寄存器写入value指定的值。此值可为0x开头十六进制数,可为十进制数。
1 2 3 4 wb(address), ws(address), wi(address) <value> wb0x40008606 1 ws0x40008606 0xb502
向指定地址写入value指定的值。写入的宽度由不同指令所指定,wb能写入一个字节数据、ws能short数据、wi能写入int数据,且大小端序会被自动转换。
1 2 3 wx(address) <hex> wx0x40008606 b502
向指定地址写入hex数据,此处hex不能加0x开头,直接写十六进制就可以了。
search stack
在当前栈上搜索指定的hex数据。
search writable heap
在可写的内存搜索指定的hex数据。
search readable heap
在可读的内存搜索指定的hex数据。
search executable heap
在可执行的内存搜索指定的hex数据。
where
打印当前unidbg的调用栈。
vm
查看当前所有加载的模块。
stop
停止运行。
patch 1 2 3 p <assembly> p movs r0, #1
将当前位置进行patch,无需关心thumb。
trace 1 2 3 4 5 6 trace [begin end] trace 0x40008606 0x40008612 traceRead [begin end] traceWrite [begin end]
指定起始结束位置进行trace。
GDB_SERVER 不推荐使用。请使用ConsoleDebugger 。
ANDROID_SERVER_V7 不推荐使用。请使用ConsoleDebugger 。
Block 什么叫做block?简单来说就是未发生跳转的一整段汇编为一个block,如发生B系列指令跳转,就会进入下一block。
RegisterContext TODO
SyscallHandler SyscallHandler介绍 SyscallHandler模块为处理系统调用处理器的核心模块,模拟了在执行过程中所产生的大大小小的系统调用。虽然该模块为核心模块,但提供的API并不多,我们来看。
API 获取SyscallHandler实例 1 SyscallHandler<AndroidFileIO> syscallHandler = emulator.getSyscallHandler();
setVerbose 1 syscallHandler.setVerbose(true );
部分系统调用日志开关。
addIOResolver 1 2 3 4 5 6 7 8 9 syscallHandler.addIOResolver(new IOResolver <AndroidFileIO>() { @Override public FileResult<AndroidFileIO> resolve (Emulator<AndroidFileIO> emulator, String pathname, int oflags) { return null ; } });
添加IO处理器。如果在模拟执行期间,有读写文件操作,可由自定义指定的IO处理器进行处理,且自定义IO处理器的优先级最高。关于返回值请查看FileResult 。
setEnableThreadDispatcher 1 syscallHandler.setEnableThreadDispatcher(true );
多线程自动调度开关。开启此开关,会以多线程模式运行,当有线程创建会自动进行处理调度。
setFileListener 1 2 3 4 5 6 7 8 9 10 11 syscallHandler.setFileListener(new FileListener () { @Override public void onOpenSuccess (Emulator<?> emulator, String pathname, FileIO io) { System.out.println(pathname+"文件打开成功!" ); } @Override public void onClose (Emulator<?> emulator, FileIO io) { System.err.println(io.getPath()+"文件打开失败" ); } });
设置文件监听器。此API在在运行期间,可以监听所有成功打开和关闭的文件,打开失败的文件不会进行回调。
getFileIO 1 FileIO fileIO = syscallHandler.getFileIO(0 );
根据fd查询对应的File。
自处理系统调用 如果unidbg处理的系统调用不足以满足你的需求,可以自己参考unidbg的处理方式,定制SyscallHandler来处理。
FileResult 在自定义IO处理中,FileResult作为回调方法的返回值。其有3个静态方法,分别代表对文件的不同操作。
success 1 2 FileResult.<AndroidFileIO>success(new SimpleFileIO (oflags, new File ("filepath" ),pathname));
success方法表示一个文件正常处理。参数是一个NewFileIO接口的实现,根据不同的场景返回不同。如上面例子,就是返回一个普通文件,就使用SimpleFileIO实现,如果是一个文件夹,就使用DirectoryFileIO实现,其他场景可自行查看接口的实现类。
failed
表示文件打开失败。参数是一个errno,会由上层处理将errno设置为该参数。
fallback 1 FileResult.<AndroidFileIO>fallback(new DirectoryFileIO (oflags,pathname,new File ("filepath" )));
表示降低自己的优先级,在其他处理器无法找到该文件时,由fallback指定的文件IO来代替。
VirtualModule VirtualModulem介绍 VirtualModule是unidbg提供的一种虚拟模块方案。它的用途是当一个So由于某种原因,无法完整加载进unidbg,或想自己实现该So的部分功能时,就可以使用虚拟模块来解决。
unidbg自带虚拟模块 unidbg中自带了两个虚拟模块:
AndroidModule(libandroid.so)
JniGraphics(libjnigraphics.so)
这两个模块中的部分常用符号已经在unidbg中有提供,当我们要模拟执行一个So时,而它又正好使用了libandroid.so,我们就可以这样做:
1 new AndroidModule (emulator,vm).register(memory);
然后再加载目标So。libjnigraphics.so同理。
自定义虚拟模块 当然我们也可以自己实现一个虚拟模块。下面我们以一个例子来说明虚拟模块应该如何使用。
模拟场景 我们先来模拟一个使用虚拟So的场景。先来看Java层的代码:
1 2 3 4 5 6 7 8 9 10 package com.github.unidbg;public class Test { static { System.loadLibrary("native-doc" ); } public native int sign (int a) ; }
Java层加载了libnative-doc.so,有一个sign方法,参数跟返回值都为int类型。我们再来看So层的代码:
1 2 3 4 5 6 7 8 #include <jni.h> #include "myAdd.h" extern "C" JNIEXPORT jint JNICALL Java_com_github_unidbg_Test_sign (JNIEnv *env, jobject thiz, jint a) { return myAdd(a, 1024 ); }
So的代码有sign方法的实现,而且实现非常简单,调用了myAdd函数。但是这个myAdd函数是来自其他的So的一个函数,而且这个So还由于某种原因不能被正常加载或没必要去加载,我们来看在unidbg中该如何去做。
首次尝试 现在首先要做的是在unidbg中加载native-doc模块,看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package com.github.unidbg.android;import com.alibaba.fastjson.util.IOUtils;import com.github.unidbg.*;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 DocTest extends AbstractJni { public static void main (String[] args) { DocTest test = new DocTest (); test.sign(); test.destroy(); } private void destroy () { IOUtils.close(emulator); } private final AndroidEmulator emulator; private final Module module ; private final VM vm; private DocTest () { emulator = AndroidEmulatorBuilder .for32Bit() .build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 )); vm = emulator.createDalvikVM(); module = emulator.loadLibrary(new File ("unidbg-android/src/test/java/com/github/unidbg/libnative-doc.so" ), true ); vm.callJNI_OnLoad(emulator, module ); } private void sign () { DvmClass testClass = vm.resolveClass("com/github/unidbg/Test" ); DvmObject<?> obj = testClass.newObject(null ); int ret = obj.callJniMethodInt(emulator, "sign(I)I" , 1 ); System.out.println("执行结果: " + ret); } }
当我们写完上面的代码,执行时会发现如下错误:
1 2 libnative-doc.so load dependency libadd.so failed
unidbg会提示你libnative-doc.so需要一个libadd.so,而unidbg又找不到这个So来加载,所以就无法执行出正常的结果。这个libadd.so提供了myAdd函数的实现,但是它无法被正常加载,我们知道的是myAdd函数的实现是什么样子的。它就是将两个参数进行相加,然后返回结果。我们知道实现就好办了,下面通过虚拟So来解决这个问题。
处理问题 处理这种问题就分两步。第一步创建虚拟模块,来处理未找到的符号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static class Add extends VirtualModule <VM>{ public Add (Emulator<?> emulator, VM extra) { super (emulator,extra,"libadd.so" ); } @Override protected void onInitialize (Emulator<?> emulator, VM extra, Map<String, UnidbgPointer> symbols) { SvcMemory svcMemory = emulator.getSvcMemory(); symbols.put("_Z5myAddii" ,svcMemory.registerSvc(new ArmSvc () { @Override public long handle (Emulator<?> emulator) { RegisterContext context = emulator.getContext(); int arg0 = context.getIntArg(0 ); int arg1 = context.getIntArg(1 ); return arg0 + arg1; } })); } }
第二步,在加载我们目标So时,先来加载我们的虚拟So:
1 2 new Add (emulator, vm).register(memory);
这样就可以搞定啦。
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package com.github.unidbg.android;import com.alibaba.fastjson.util.IOUtils;import com.github.unidbg.*;import com.github.unidbg.Module;import com.github.unidbg.arm.ArmSvc;import com.github.unidbg.arm.context.RegisterContext;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 com.github.unidbg.memory.SvcMemory;import com.github.unidbg.pointer.UnidbgPointer;import com.github.unidbg.virtualmodule.VirtualModule;import java.io.File;import java.util.Map;public class DocTest extends AbstractJni { public static void main (String[] args) { DocTest test = new DocTest (); test.sign(); test.destroy(); } private void destroy () { IOUtils.close(emulator); } private final AndroidEmulator emulator; private final Module module ; private final VM vm; private DocTest () { emulator = AndroidEmulatorBuilder .for32Bit() .build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 )); vm = emulator.createDalvikVM(); new Add (emulator, vm).register(memory); module = emulator.loadLibrary(new File ("unidbg-android/src/test/java/com/github/unidbg/libnative-doc.so" ), true ); vm.callJNI_OnLoad(emulator, module ); } private void sign () { DvmClass testClass = vm.resolveClass("com/github/unidbg/Test" ); DvmObject<?> obj = testClass.newObject(null ); int ret = obj.callJniMethodInt(emulator, "sign(I)I" , 1 ); System.out.println("执行结果: " + ret); } public static class Add extends VirtualModule <VM> { public Add (Emulator<?> emulator, VM extra) { super (emulator, extra, "libadd.so" ); } @Override protected void onInitialize (Emulator<?> emulator, VM extra, Map<String, UnidbgPointer> symbols) { SvcMemory svcMemory = emulator.getSvcMemory(); symbols.put("_Z5myAddii" , svcMemory.registerSvc(new ArmSvc () { @Override public long handle (Emulator<?> emulator) { RegisterContext context = emulator.getContext(); int arg0 = context.getIntArg(0 ); int arg1 = context.getIntArg(1 ); return arg0 + arg1; } })); } } }
Jnitrace 使用 1 jnitrace -l so文件 包名 > xxx.txt
jnitrace函数调用返回的是地址的时候 SO运行存在多线程的问题,多线程会给Trace分析带来干扰,我们在文本中查找TID 11372,跟着这个线 程走下去 此处在获取0x49即我们的字符串的长度,继续往下 此处将jstring转成Native 字符串,在JNI开发中,大部分JObject都属于局部引用,局部引用在用完之后需要手动释放 比如GetStringUTFChars获取的资源,最后一定要ReleaseStringUTFChars,JNItrace会在Releasexxx这 系列API中,打印内容 但是需要注意的是,引用的释放方式有两种,除了Releasexx系列,也可以用DeleteLocalRef释放局部引 用,如果通过DeleteLocalRef释放,JNItrace并不会打印出具体值
堆栈检测 “java/lang/Throwable->()V” new一个异常之后 “java/lang/Throwable->getStackTrace()[Ljava/lang/StackTraceElement;” 查看堆栈,这不就是堆栈检测吗,如果被Xposed Hook了,堆栈里会有Xposed这一 层。 new 异常之后,getStackTrace即得到异常堆栈数组,写代码验证一下 继续看JNItrace 首先获取数据长度,这很好理解嘛,后面肯定是想循环遍历取出完整的调用栈,得知道调用栈有多长 获取数组第0个元素 getClassName,即获取具体的类名 因为是在JNI中处理,所以后面还得getString 用完之后Release释放这个字符串资源,防止内存泄漏 com.xunmeng.pinduoduo.secure.DeviceNative即调用栈的第一层 然后它对数组的第二个元素做同样的操作,一套流程总结下来,就是构造异常后读取调用栈嘛
推荐使用objection查看堆栈
函数调用两种方式 Unidbg call 参数传递,到底是自己call 地址 还是用Unidbg封装的API? 各有优劣,用API的话代码量小一些,自己call的话更灵活,限制小。熟能生巧。
符号调用 1 2 3 4 5 DvmObject obj = ProxyDvmObject.createObject(vm,this );String data = "dta" ;DvmObject dvmObject = obj.callJniMethodObject(emulator, "md5(Ljava/lang/String;)Ljava/lang/String;" , data);String result = (String) dvmObject.getValue();System.out.println("[symble] Call the so md5 function result is ==> " + result);
地址调用 1 2 3 4 5 6 7 8 9 10 11 12 13 Pointer jniEnv = vm.getJNIEnv();DvmObject obj = ProxyDvmObject.createObject(vm,this );StringObject data = new StringObject (vm,"dta" );List<Object> args = new ArrayList <>(); args.add(jniEnv); args.add(vm.addLocalObject(obj)); args.add(vm.addLocalObject(data)); Number[] numbers = module .callFunction(emulator, 0x8E81 , args.toArray()); DvmObject<?> object = vm.getObject(numbers[0 ].intValue()); String value = (String) object.getValue();System.out.println("[addr] Call the so md5 function result is ==> " + value);
构建类的两种方式 以当前包类为类名
1 DvmObject obj = ProxyDvmObject.createObject(vm,this );
自定义包名类名
1 DvmObject<?> context = vm.resolveClass("android/content/Context" ).newObject(null );
类继承的写法 链式继承 vm.resolveClass(“子类”, vm.resolveClass(“父类”, vm.resolveClass(“父类的父类”))).newObject(signature);
1 2 3 case "android/app/ActivityThread->getApplication()Landroid/app/Application;" : { return vm.resolveClass("android/app/Application" , vm.resolveClass("android/content/ContextWrapper" , vm.resolveClass("android/content/Context" ))).newObject(signature); }
多行的写法
1 2 3 4 DvmClass context = vm.resolveClass("android/content/Context" );DvmClass ContextWrapper = vm.resolveClass("android/content/ContextWrapper" ,context);DvmClass Application = vm.resolveClass("android/app/Application" ,ContextWrapper);return Application.newObject(signature);
unidbg补获取系统属性 unidbg补获取系统属性 可以使用unidbg封装好的hook方法知道系统调用了哪些系统属性,__system_property_get
1 2 3 4 5 6 7 8 9 10 11 SystemPropertyHook systemPropertyHook = new SystemPropertyHook (emulator);systemPropertyHook.setPropertyProvider(new SystemPropertyProvider () { @Override public String getProperty (String key) { System.out.println("lilac Systemkey:" +key); switch (key){ } return "" ; }; }); memory.addHookListener(systemPropertyHook);
文件重定向,实现 IOResolver 需要继承 IOResolver 重写 resolve方法 在定义emulator好后,在so加载前添加文件重定位器 emulator.getSyscallHandler().addIOResolver(this); 指定路径文件定向(单文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 if (pathname.equals ("/data/user/0/com.example.fileinunidbg/files/key.txt" )){ return FileResult.success (new SimpleFileIO (oflags, new File ("unidbg-android/src/test/resources/FileDemo/key.txt" ), pathname)); } if (pathname.equals ("/data/user/0/com.example.fileinunidbg/files/demo2" )){ return FileResult.success (new DirectoryFileIO (oflags, pathname, new DirectoryFileIO.DirectoryEntry (true , String.valueOf (System.currentTimeMillis ())), new DirectoryFileIO.DirectoryEntry (true , "1234" ))); } if (pathname.equals ("/data/user/0/com.example.fileinunidbg/files/demo2/1.txt" )){ return FileResult.success (new SimpleFileIO (oflags, new File ("unidbg-android/src/test/resources/FileDemo/demo2/1.txt" ), pathname)); } if (pathname.equals ("/data/user/0/com.example.fileinunidbg/files/demo2/2.txt" )){ return FileResult.success (new SimpleFileIO (oflags, new File ("unidbg-android/src/test/resources/FileDemo/demo2/2.txt" ), pathname)); } if (pathname.equals ("/data/user/0/com.example.fileinunidbg/files/demo2/3.txt" )){ return FileResult.success (new SimpleFileIO (oflags, new File ("unidbg-android/src/test/resources/FileDemo/demo2/3.txt" ), pathname)); } if (("proc/" + emulator.getPid () + "/status" ).equals (pathname)) {return FileResult.success (new ByteArrayFileIO (oflags, pathname, ("文本内容....." ).getBytes ())}
或者是虚拟目录(对比文件夹里的文件比较多的情况下比较好) 在创建emulator的时候直接指定目录
1 emulator = AndroidEmulatorBuilder.for32Bit().setRootDir (new File ("unidbg-android/src/test/resources/FileDemo/VFS" )).build ();
补了虚拟目录和代码,哪边会生效? 代码执行会先看有没有重写文件重定位器,没有就默认重定位器,两个都没有找到就到虚拟目录系统找,
一些常用功能 打印内存,hexdump 1 Inspector.inspect(ctx.getR0Pointer().getByteArray(0 , 0x10 ), "Arg1" );
unidbg下断点 1 2 3 4 5 普通断点 emulator.attach().addBreakPoint(module .base + 0x3161E ); 内存写入断点 emulator.traceWrite(module .base + 0x3A0C0 ,module .base + 0x3A0C0 );
开启所有的日志 1 2 3 4 5 6 7 8 9 10 Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler" ).setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.unix.UnixSyscallHandler" ).setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.AbstractEmulator" ).setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM" ).setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM" ).setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.linux.android.dvm" ).setLevel(Level.DEBUG); 所需要的头文件 import org.apache.log4j.Level;import org.apache.log4j.Logger;
计算汇编的行数 计算某个函数的汇编行数前使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void traceLength () { emulator.getBackend().hook_add_new(new CodeHook () { int count = 0 ; @Override public void hook (Backend backend, long address, int size, Object user) { count += 1 ; System.out.println(count); } @Override public void onAttach (UnHook unHook) { } @Override public void detach () { } }, module .base, module .base + module .size, null ); }
打印函数返回值或修改返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void hookRandom () { emulator.attach().addBreakPoint(module .findSymbolByName("lrand48" , true ).getAddress(), new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { System.out.println("call lrand48" ); emulator.getUnwinder().unwind(); emulator.attach().addBreakPoint(emulator.getContext().getLRPointer().peer, new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { Number number = emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0); System.out.println("number " + number.intValue()); return true ; } }); return true ; } }); }
随机数函数返回值固定 程序中最常见的随机变量是时间戳和随机数,绝大多数情况下,程序会使用 arc4random 这样的库函数获取随机数,其底层依赖于 /dev/urandom
、/dev/random
等随机文件,或者直接访问这些文件,获取随机数。 在 Unidbg 里,这几个伪设备文件的模拟位置在 src/main/java/com/github/unidbg/linux/file/DriverFileIO.java
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static DriverFileIO create (Emulator<?> emulator, int oflags, String pathname) { if ("/dev/urandom" .equals(pathname) || "/dev/random" .equals(pathname) || "/dev/srandom" .equals(pathname)) { return new RandomFileIO (emulator, pathname); } if ("/dev/alarm" .equals(pathname) || "/dev/null" .equals(pathname)) { return new DriverFileIO (emulator, oflags, pathname); } if ("/dev/ashmem" .equals(pathname)) { return new Ashmem (emulator, oflags, pathname); } if ("/dev/zero" .equals(pathname)) { return new ZeroFileIO (emulator, oflags, pathname); } return null ; }
到 RandomFileIO 类里,把 randBytes 函数里的 ThreadLocalRandom.current().nextBytes(bytes); 注释掉
获取 Trace 使用方式看 Trace ,下面是保存到文件 如果不希望 trace 只是打印在日志里,而是要重定位到文件里,所以要多几行代码
1 2 3 4 5 6 7 8 String traceFile = "unidbg-android/src/test/resources/trace/tracecode.txt" ;PrintStream traceStream; try { traceStream = new PrintStream (new FileOutputStream (traceFile), true ); emulator.traceCode(module .base,module .base+module .size).setRedirect(traceStream); } catch (FileNotFoundException e) { e.printStackTrace(); }
大概80w行耗时3分钟左右,是否可以优化和加速 Unidbg trace 的速度。 指令追踪的耗时有两部分,1 是为了实现指令追踪的能力,所做的指令膨胀和插桩。2 是在展示上的耗时。前一部分的损耗我们很难控制,而后一部分展示上的耗时,可做很多优化。 在展示上,trace 需要包括三要素。 ● 地址 ● 汇编代码 ● 寄存器、内存的值变化展示 耗时主要在要素二,反汇编机器码获取汇编代码上。在 Unidbg 的处理逻辑里,运行每一条指令都会对它重新做反汇编。但我们都知道,如果地址和机器码都不变化(即没有发生代码自修改的情况里),使用先前对这个地址缓存下来的反汇编即可。 接下来让我们对 Unidbg 的源码做一点修改,下面只处理 ARM64。 找到unidbg-api/src/main/java/com/github/unidbg/arm/AbstractARM64Emulator.java
,在类中添加一个 HashMap 用于存储反汇编结果,我们将使用它来缓存反汇编。
1 private final Map<Long, Instruction[]> disassembleCache = new HashMap <>();
printAssemble 函数的代码原先如下
1 2 3 4 5 6 @Override public Instruction[] printAssemble(PrintStream out, long address, int size, int maxLengthLibraryName, InstructionVisitor visitor) { Instruction[] insns = disassemble(address, size, 0 ); printAssemble(out, insns, address, ARM.isThumb(backend), maxLengthLibraryName, visitor); return insns; }
修改它,当调用 printAssemble 函数时,首先检查缓存中是否已有相应的反汇编结果,如果有则直接使用,否则进行反汇编并将结果存入缓存。
1 2 3 4 5 6 7 8 9 10 @Override public Instruction[] printAssemble(PrintStream out, long address, int size, int maxLengthLibraryName, InstructionVisitor visitor) { Instruction[] insns = disassembleCache.get(address); if (insns == null ) { insns = disassemble(address, size, 0 ); disassembleCache.put(address, insns); } printAssemble(out, insns, address, maxLengthLibraryName, visitor); return insns; }
重新测试 trace,这次重定向到 tracecode2.txt。
1 2 3 4 5 6 7 8 String traceFile = "unidbg-android/src/test/resources/trace/tracecode2.txt" ;PrintStream traceStream; try { traceStream = new PrintStream (new FileOutputStream (traceFile), true ); emulator.traceCode(module .base,module .base+module .size).setRedirect(traceStream); } catch (FileNotFoundException e) { e.printStackTrace(); }
在耗时上,仅花费半分钟不到,可以说是速度大大提升,这么算的话,即使 trace 指令流的行数以亿计,也能在短短几个小时内 trace 完。 接下来继续修改 printAssemble 函数的代码,上面的代码没有考虑到代码自修改的情况,自修改即程序在运行期间修改自身指令,不管是出于解密、反调,还是其它目的,自修改都是一件常事。所以我们还要判断一下字节是否发生了改变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public Instruction[] printAssemble(PrintStream out, long address, int size, int maxLengthLibraryName, InstructionVisitor visitor) { Instruction[] insns = disassembleCache.get(address); byte [] currentCode = backend.mem_read(address, size); boolean needUpdateCache = false ; if (insns != null ) { byte [] cachedCode = new byte [size]; int offset = 0 ; for (Instruction insn : insns) { byte [] insnBytes = insn.getBytes(); System.arraycopy(insnBytes, 0 , cachedCode, offset, insnBytes.length); offset += insnBytes.length; } if (!Arrays.equals(currentCode, cachedCode)) { needUpdateCache = true ; } } else { needUpdateCache = true ; } if (needUpdateCache) { insns = disassemble(address, currentCode, false , 0 ); disassembleCache.put(address, insns); } printAssemble(out, insns, address, maxLengthLibraryName, visitor); return insns; }
新代码会判断当前地址的机器码是否和缓存的这个地址上的机器码一致,这增加了一点开销,运行完毕后比较一下,也就慢了几秒钟,完全能承受。
1 2 3 4 5 6 7 8 String traceFile = "unidbg-android/src/test/resources/trace/tracecode3.txt" ;PrintStream traceStream; try { traceStream = new PrintStream (new FileOutputStream (traceFile), true ); emulator.traceCode(module .base,module .base+module .size).setRedirect(traceStream); } catch (FileNotFoundException e) { e.printStackTrace(); }
获取函数调用流 基于对指令流中BL
、BLR
等函数调用指令的监控,Unidbg 实现了对函数的监控和追踪,我们稍作处理,将它重定向到文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 String traceFile = "unidbg-android/src/test/resources/trace/tracefunction.txt" ;try { final PrintStream traceStream; traceStream = new PrintStream (new FileOutputStream (traceFile), true ); emulator.attach().traceFunctionCall(module , new FunctionCallListener () { @Override public void onCall (Emulator<?> emulator, long callerAddress, long functionAddress) { } @Override public void postCall (Emulator<?> emulator, long callerAddress, long functionAddress, Number[] args) { traceStream.println("onCallFinish caller=" + UnidbgPointer.pointer(emulator, callerAddress) + ", function=" + UnidbgPointer.pointer(emulator, functionAddress)); } }); } catch (FileNotFoundException e) { e.printStackTrace(); }
代码置于 JNI_OnLoad 后。
在内存中搜索数据出现的位置 假如目标数据是EC 03 CE 8A 99 16 7F 9A 8F 2D F5 CD 8E E1 47 F2。 ● 这串数据在第 25w 行左右的地址生成,地址是 A ● 在 36w 行的位置由 A 赋值给 B,即 A –> B ● 在 47w 行的位置 B –> C ● 在 55w 行的位置 C –> D ● 在 82w 行的位置 D –> E ● 在 97w 行的位置 E–> F 在逆向追溯时,从 F 开始做 traceWrite,经过四五次跳转,最终才能找到 A,非常的麻烦。 在这个 Android Native Runtime 里,只需要处理单个 SO 里的单个函数,因此它所使用和操纵的内存区块很有效,只有几 MB 大小。因此我们可以从函数起始点开始,高频率的搜索内存里是否出现了某个值,从而确定某个数据最早出现的位置,下面是基本的实现代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 import com.github.unidbg.Emulator;import com.github.unidbg.arm.backend.*;import com.github.unidbg.pointer.UnidbgPointer;import com.sun.jna.Pointer;import java.util.*;public class DataSearch { private final Emulator<?> emulator; private final TreeSet<Long> addressUsed = new TreeSet <>(); private final Map<Long, Byte> memoryData = new HashMap <>(); private final byte [] data; private boolean find = false ; private int interval; private long count = 0 ; private final List<long []> activePlace = new ArrayList <>(); public DataSearch (Emulator<?> emulator, String data) { this .emulator = emulator; this .data = hexStringToByteArray(data); this .interval = 100 ; hookReadAndWrite(); hookBlock(); } public DataSearch (Emulator<?> emulator, String data, int interval) { this .emulator = emulator; this .data = hexStringToByteArray(data); this .interval = interval; hookReadAndWrite(); hookBlock(); } public static byte [] hexStringToByteArray(String s) { int len = s.length(); byte [] data = new byte [len / 2 ]; for (int i = 0 ; i < len; i += 2 ) { data[i / 2 ] = (byte ) ((Character.digit(s.charAt(i), 16 ) << 4 ) + Character.digit(s.charAt(i + 1 ), 16 )); } return data; } private void hookReadAndWrite () { WriteHook writeHook = new WriteHook () { @Override public void onAttach (UnHook unHook) { } @Override public void detach () { } @Override public void hook (Backend backend, long address, int size, long value, Object user) { for (int offset = 0 ; offset < size; offset++) { memoryData.put(address + offset, (byte ) (value >>> (offset * 8 ))); } updateActivePlace(address, size); } }; ReadHook readHook = new ReadHook () { @Override public void onAttach (UnHook unHook) { } @Override public void detach () { } @Override public void hook (Backend backend, long address, int size, Object user) { byte [] readBytes = emulator.getBackend().mem_read(address, size); for (int offset = 0 ; offset < size; offset++) { memoryData.put(address + offset, readBytes[offset]); } updateActivePlace(address, size); } }; emulator.getBackend().hook_add_new(writeHook, 1 , 0 , null ); emulator.getBackend().hook_add_new(readHook, 1 , 0 , null ); } private void hookBlock () { BlockHook blockHook = new BlockHook () { @Override public void onAttach (UnHook unHook) { } @Override public void detach () { } @Override public void hookBlock (Backend backend, long address, int size, Object user) { count++; if (!find && (count % interval == 0 )) { searchData(); } } }; emulator.getBackend().hook_add_new(blockHook, 1 , 0 , null ); } private void searchData () { if (!find) { for (long [] range : activePlace) { if (searchDataInRange(range)) { find = true ; printAddress(range); emulator.attach().debug(); break ; } } } } private boolean searchDataInRange (long [] range) { for (long i = range[0 ], max = range[1 ] - data.length; i <= max; i++) { boolean found = true ; for (int j = 0 ; j < data.length; j++) { Byte value = memoryData.get(i + j); if (value == null || value != data[j]) { found = false ; break ; } } if (found) { return true ; } } return false ; } private void printAddress (long [] range) { Collection<Pointer> pointers = searchMemory(range[0 ], range[1 ], data); System.out.println("Search data matches " + pointers.size() + " count" ); for (Pointer pointer : pointers) { System.out.println("data address: " + pointer); } } private Collection<Pointer> searchMemory (long start, long end, byte [] data) { List<Pointer> pointers = new ArrayList <>(); if (end - start >= data.length) { for (long i = start, max = end - data.length; i <= max; i++) { boolean found = true ; for (int j = 0 ; j < data.length; j++) { Byte value = memoryData.get(i + j); if (value == null || value != data[j]) { found = false ; break ; } } if (found) { pointers.add(UnidbgPointer.pointer(emulator, i)); } } } return pointers; } private void updateActivePlace (long address, int size) { for (int i = 0 ; i < size; i++) { long newAddress = address + i; if (addressUsed.add(newAddress)) { updateActivePlace(newAddress); } } } private void updateActivePlace (long address) { Long lower = addressUsed.lower(address); Long higher = addressUsed.higher(address); boolean connectToLower = lower != null && lower + 1 == address; boolean connectToHigher = higher != null && address + 1 == higher; long [] lowerRange = null ; long [] higherRange = null ; for (long [] range : activePlace) { if (connectToLower && range[1 ] == lower) { lowerRange = range; } if (connectToHigher && range[0 ] == higher) { higherRange = range; } } if (connectToLower && connectToHigher) { assert lowerRange != null ; lowerRange[1 ] = higherRange[1 ]; activePlace.remove(higherRange); } else if (connectToLower) { assert lowerRange != null ; lowerRange[1 ] = address; } else if (connectToHigher) { assert higherRange != null ; higherRange[0 ] = address; } else { activePlace.add(new long []{address, address}); } } }
它使用上非常简单,在 loadlibaray 前添加下面一行代码即可。
1 new DataSearch (Emulator<?> emulator, String data);
其中 data 是你所需要搜索内容的 hexstring 形式,加不加空格都行。
1 2 new DataSearch (emulator, "cd 8e ec" );new DataSearch (emulator, "cd8eec" );
DataSearch 默认的搜索频率是每 100指令流,如果处理千万级或亿级的指令流,那么可以使用 DataSearch 的另外一个构造函数。
1 new DataSearch (Emulator<?> emulator, String data, int interval);
interval 用于指定每隔多少个基本块做一次内存检索,建议千万级指令流选择 500 左右,亿级指令流选择 2000 左右。检索频率越高,搜索的准确度越高,越可能发现数据的第一生成现场;检索越稀疏,搜索的耗时就越小,但可能会错过第一生成现场。因此,我们需要在性能和准确度之间找个均衡点。 DataSearch 并非万能,如果数据过短小,比如两三个字节,那么在内存中就不具有标识性和唯一性,无法用它处理。这种情景请使用污点分析或类似的其它分析技术。 除了搜索某个指定字节数组外,DataSearch 稍加修改,可以用于搜索 Key、字符串等等,比如下面是 StringSearch,它会打印内存中出现过的所有可见字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 import com.github.unidbg.Emulator;import com.github.unidbg.arm.backend.*;import com.github.unidbg.pointer.UnidbgPointer;import com.sun.jna.Pointer;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.PrintWriter;import java.util.*;public class StringSearch { private final Emulator<?> emulator; private final TreeSet<Long> addressUsed = new TreeSet <>(); private final Map<Long, Byte> memoryData = new HashMap <>(); private Set<String> foundStrings = new HashSet <>(); private int interval; private int minLength; private long count = 0 ; private final List<long []> activePlace = new ArrayList <>(); private PrintWriter output; public StringSearch (Emulator<?> emulator, int minLength, String outputFile) { this .emulator = emulator; this .interval = 100 ; this .minLength = minLength; try { this .output = new PrintWriter (new FileOutputStream (outputFile)); } catch (FileNotFoundException e) { throw new RuntimeException (e); } hookReadAndWrite(); hookBlock(); } public StringSearch (Emulator<?> emulator, int minLength, int interval, String outputFile) { this .emulator = emulator; this .interval = interval; this .minLength = minLength; try { this .output = new PrintWriter (new FileOutputStream (outputFile)); } catch (FileNotFoundException e) { throw new RuntimeException (e); } hookReadAndWrite(); hookBlock(); } private void hookReadAndWrite () { WriteHook writeHook = new WriteHook () { @Override public void onAttach (UnHook unHook) { } @Override public void detach () { } @Override public void hook (Backend backend, long address, int size, long value, Object user) { for (int offset = 0 ; offset < size; offset++) { memoryData.put(address + offset, (byte ) (value >>> (offset * 8 ))); } updateActivePlace(address, size); } }; ReadHook readHook = new ReadHook () { @Override public void onAttach (UnHook unHook) { } @Override public void detach () { } @Override public void hook (Backend backend, long address, int size, Object user) { byte [] readBytes = emulator.getBackend().mem_read(address, size); for (int offset = 0 ; offset < size; offset++) { memoryData.put(address + offset, readBytes[offset]); } updateActivePlace(address, size); } }; emulator.getBackend().hook_add_new(writeHook, 1 , 0 , null ); emulator.getBackend().hook_add_new(readHook, 1 , 0 , null ); } private void hookBlock () { BlockHook blockHook = new BlockHook () { @Override public void onAttach (UnHook unHook) { } @Override public void detach () { } @Override public void hookBlock (Backend backend, long address, int size, Object user) { count++; if (count % interval == 0 ){ searchString(); } } }; emulator.getBackend().hook_add_new(blockHook, 1 , 0 , null ); } private void searchString () { for (long [] range : activePlace) { searchStringInRange(range); } } private void searchStringInRange (long [] range) { int length = (int ) (range[1 ] - range[0 ] + 1 ); byte [] codes = new byte [length]; for (int i = 0 ; i < length; i++) { codes[i] = memoryData.get(i + range[0 ]); } searchVisibleStringsInMemory(codes, range[0 ]); } public void searchVisibleStringsInMemory (byte [] memory, long addr) { StringBuilder sb = new StringBuilder (); for (int i = 0 ; i < memory.length; i++) { byte b = memory[i]; if (b >= 32 && b <= 126 ) { sb.append((char ) b); } else { processVisibleString(sb, addr + i - sb.length()); } } processVisibleString(sb, addr + memory.length - sb.length()); } private void processVisibleString (StringBuilder sb, long addr) { if (sb.length() >= minLength) { String visibleString = sb.toString(); if (foundStrings.add(visibleString)) { String result = "Address: 0x" + Long.toHexString(addr) + ", String: " + visibleString; output.println(result); output.flush(); } } sb.setLength(0 ); } private void updateActivePlace (long address, int size) { for (int i = 0 ; i < size; i++) { long newAddress = address + i; if (addressUsed.add(newAddress)) { updateActivePlace(newAddress); } } } private void updateActivePlace (long address) { Long lower = addressUsed.lower(address); Long higher = addressUsed.higher(address); boolean connectToLower = lower != null && lower + 1 == address; boolean connectToHigher = higher != null && address + 1 == higher; long [] lowerRange = null ; long [] higherRange = null ; for (long [] range : activePlace) { if (connectToLower && range[1 ] == lower) { lowerRange = range; } if (connectToHigher && range[0 ] == higher) { higherRange = range; } } if (connectToLower && connectToHigher) { assert lowerRange != null ; lowerRange[1 ] = higherRange[1 ]; activePlace.remove(higherRange); } else if (connectToLower) { assert lowerRange != null ; lowerRange[1 ] = address; } else if (connectToHigher) { assert higherRange != null ; higherRange[0 ] = address; } else { activePlace.add(new long []{address, address}); } } }
使用上也很简单,参数 1 是模拟器实例,参数 2 是可见字符串的最短长度,参数 3 是输出的文件路径。
1 public StringSearch (Emulator<?> emulator, int minLength, String outputFile)
它的默认搜索频率是 100 个基本块,可以使用它的重载方法,修改这个值。
1 public StringSearch (Emulator<?> emulator, int minLength, int interval, String outputFile)
具体使用上,比如下面这样
1 new StringSearch (emulator, 5 ,"unidbg-android/src/test/resources/trace/strings.log" );
效果如下,运行时的字符串可以给出很多指引。
findCryptInTrace 哈希算法主要关注下面这几点。 ● 使用了哪种哈希算法 ● 哈希的输入是什么 ● 哈希的输出是什么 ● 哈希算法是否魔改 首先要确定使用了什么哈希算法,上面用 IDA Findcrypt 插件看到,样本里有 SHA256 的魔数,但这并不意味着我们的目标函数就使用了这个算法,对吧。 但是,如果我们能在 trace 里找到 SHA256 算法的相关魔数,那就证实了 SHA256 的存在。我写过一个 010Editor 小脚本,叫 findCryptInTrace.1sc,其内容如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 int md5[4 ] = {0xd76aa478 , 0xe8c7b756 , 0x242070db , 0xc1bdceee };int sha1[4 ] = {0x5A827999 , 0x6ED9EBA1 , 0x8F1BBCDC , 0xCA62C1D6 };int sha256[4 ] = {0x428a2f98 , 0x71374491 , 0xb5c0fbcf , 0xe9b5dba5 };int sm3[2 ] = {0x79cc4519 , 0x7a879d8a };int crc32[2 ] = {0x77073096 , 0xEE0E612C };int chacha20[2 ] = {0x61707865 , 0x3320646e };int hmac[2 ] = {0x36363636 , 0x5C5C5C5C };int tea[1 ] = {0x9e3779b9 };int twofish[4 ] = {0xBCBC3275 , 0xECEC21F3 , 0x202043C6 , 0xB3B3C9F4 };int salsa20[4 ] = {0x61707865 , 0x3320646e , 0x79622d32 , 0x6b206574 };int blowfish[2 ] = {0x243f6a88 , 0x85a308d3 };int rc6[2 ] = {0xb7e15163 , 0x9e3779b9 };int aes[2 ] = {0xC66363A5 , 0xF87C7C84 };int aplib[1 ] = {0x32335041 };void search (int arr[], string name) { int num = sizeof(arr)/sizeof(arr[0 ]); local int i; local string s; local uchar exist = false ; for ( i = 0 ; i < num; i++ ){ SPrintf(s, "%X" , arr[i]); if (FindFirst(s, false ) > 0 ){ exist = true ; break ; } } if (exist){ Printf("Find " +name +", num:0x" +s+"\n" ); } } Printf("***********Findcrypt Search Start****************\n" ); search(md5, "MD5" ); search(sha1, "SHA1" ); search(sha256, "SHA256" ); search(sm3, "sm3" ); search(crc32, "crc32" ); search(chacha20, "chacha20" ); search(hmac, "hmac" ); search(tea, "tea" ); search(twofish,"twofish" ); search(salsa20, "salsa20" ); search(blowfish, "blowfish" ); search(rc6,"rc6" ); search(aes,"aes" ); search(aplib,"aplib" ); Printf("***********Findcrypt Search Over****************\n" );
原理和 Findcrypt 类似,搜索各种常见算法所使用的常量,下面演示它的使用。 打开我们的脚本,选择运行。 trace 里明确找到了 SHA256 算法以及 HMAC 方案的特征。这说明样本可能使用了 HMAC-SHA256,也可能是 SHA256 + HMAC-SHA256,或者其它,这都说不清,需要进一步分析。
反制unidng 如何增加跑通Unidbg模拟执行的成本 ?
一、JNI交互 主要有两点
尽可能增加补JAVA环境所需的时间成本
打个比方,在SO中编写1000个对JAVA层的访问和调用函数,每次计算时根据时间戳使用其中的50 个,这样做的话,运算的时间成本不高,但因为时间戳一直在变,所以得在Unidbg中补齐全量的 JAVA访问。
使用Unidbg暂不支持的JNI调用
src/main/java/com/github/unidbg/linux/android/dvm/DalvikVM.java 是Unidbg中实现JNI方法 的类,可以发现,只实现了最常用的那部分JNI方法,所以可以在样本中使用这些Unidbg尚未实现 的JNI方法。
二、系统调用 Unidbg实现了许多常见的系统调用,但还有一些偏冷门的系统调用未实现,以及不偏僻但较难完美实现 的系统调用,典型代表——fork相关系统调用。
三、文件访问 尽可能多的文件访问和交互
四、检测Unidbg 样本可以感知并检测自己运行在Unidbg环境中,并走向错误的分支。这一块我的了解不多,但确实是可 以的,Unidbg的执行环境和真机是有很大差距的。打个比方,每个Android进程都有一些初始化环境变 量,Android系统启动时通过setEnv设置,Unidbg中则没有。
五、目标函数无法被单独执行 尽其所能的增加目标函数被执行的”前置条件“,或者说初始化函数。这里面可以玩的花招、阴招太多 了。这应该作为一个主要的反Unidbg的方向,它是有效且杀伤力巨大的
六、在真机上使用GetStaticMethodID寻找一个不存在的方法 在真机上,使用GetStaticMethodID寻找一个不存在的方法,系统会返回一个错误。在unidbg中使用GetStaticMethodID,像是无论存不存在都会模拟一个jmethodID返回给so 关于这个反unidbg的思路和形成,如果java层不存在某个类或者方法,jni中使用findclass或者getmethodid就会报错(这很合理嘛,因为找不到),而Unidbg因为没办法了解Java环境具体咋样,所以不管你find啥class还是get什么method,它都当做这是存在的。 解决,如果你想让Unidbg findclass找不到一个类,可以使用BaseVM的addNotFoundClass方法,如果想让Unidbg找不到某个方法,可以修改AbstractJni中的acceptMethod方法。
Unidbg报错 Illegal JNI version JNI版本错误,而只意味 JNIOnLoad 运行过程中出了错。至于具体出错的原因有很多,一个常见的问题是SO的依赖库缺失。 我们的目标SO依赖了 libc++_shared.so ,这个库是C++的支持库,但不在Unidbg默认支持的SO里。 我们要在apk的lib里把它拷贝出来。 还有另一个更通用的办法 ,当报错时,就把 traceCode打开,记录执行流,看最后在哪儿停下来。 比如此处,我在JNI_OnLoad前面打开traceCode,在IDA中跳转到最后一条运行地址 0x177C 可以发现正是C++的标准库函数,符合上面的猜测。 添加之后正常加载
app开发过程中的失误,释放变量后还调用 运行样本后,发现内存错误 第一种尝试,把Unidbg的日志全开,src/test/resources/log4j.properties中的INFO全配置改为DEBUG 再次运行后会在报错的地方停下来。输入bt查看调用栈 unidbg提供了大量详情的调用回溯信息,直接拉到底部,看目标样本是从哪里调用。用IDA跳到0xabf 奇了怪了,打个日志还报错?把日志等级改为INFO,在此处下个断点分析。 emulator.attach().addBreakPoint(module.base+0x00abf); _android_log_print是常用函数,参数1是此日志的优先级,参数2是tag,参数3是格式化字符串,之后的都是内容。都可以在Console Debugger中验证 参数1是6,可以直接看,通过mrx查看其余参数,参数23也都正常,那参数4呢 内存异常了,返回IDA查看代码 可以看到上面释放了变量,下面还在调用。 怎么解决? 直接patch,改为两条无效指令,需要修改4个字节 首先写入0046,debug发现不对,调整了一下大小端序,再DEBUG似乎OK了
1 2 3 4 public void patchLog () { int patchCode = 0x46004600 ; emulator.getMemory().pointer(module .base + 0xABE ).setInt(0 ,patchCode); }
另一种填入两个nop也可以
1 2 3 4 5 6 7 8 9 10 public void patchLog1 () { Pointer pointer = UnidbgPointer.pointer(emulator, module .base + 0xABE ); assert pointer != null ; try (Keystone keystone = new Keystone (KeystoneArchitecture.Arm,KeystoneMode.ArmThumb)) { KeystoneEncoded encoded = keystone.assemble("nop" ); byte [] patch = encoded.getMachineCode(); pointer.write(0 , patch, 0 , patch.length); pointer.write(2 , patch, 0 , patch.length); } };
第三种方法,前面两种都是静态打patch,和用IDA的KeyPatch直接打patch没啥区别
1 2 3 4 5 6 7 8 9 10 public void patchLog2 () { emulator.attach().addBreakPoint(module .base + 0xABE , new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,(address)+5 ); return true ; } }); }
解释一下做了什么,我们在log的调用处下了一个断点,inline hook 都是在调用前断下来了,所以在执 行前断了下来,然后加了BreakPointCallback回调,在这个时机做处理,我们将PC指针直接往前加了 5,PC指向哪里,函数就执行哪里,本来PC指向“blx log”这个地址,程序即将去执行log函数。但我们直 接将PC加了5,为什么加5? 我们知道这里的log是个坑,它长四个字节,我们要越过这个坑,但加4不够,我们是thumb模式,再+1,所以就是+5。