unicorn
好比是一个CPU,可以模拟执行各种指令,提供了很多编程语言接口,可以操作内存、寄存器等等
但不是一个系统,内存管理、文件管理、系统调用等都需要自己来实现
unidbg
就是Java开的基于unicorn
开发的框架
unidbg
,支持模拟JNI调用,支持模拟系统调用指令,支持ARM32和ARM64,支持Hookzz、Dobby、xHook、原生unicorn Hook等hook方式,支持Android和IOS,好比是在CPU上搭建了一个系统,因此可以很方便地在PC端模拟运行so
项目在github上,用IDEA打开之后,运行unidbg-android/src/test/java/com/bytedance.framework..../TTEncrypt.java
,如果不报错就是成功了
src/main
下面是框架的源码
unidbg入门
代码基本框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| TTEncrypt(boolean logging) { this.logging = logging;
emulator = AndroidEmulatorBuilder.for32Bit() .setProcessName("com.qidian.dldl.official") .addBackendFactory(new Unicorn2Factory(true)) .build(); final Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(); vm.setVerbose(logging); DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/libttEncrypt.so"), false); dm.callJNI_OnLoad(emulator); module = dm.getModule();
TTEncryptUtils = vm.resolveClass("com/bytedance/frameworks/core/encrypt/TTEncryptUtils"); }
|
如上,会自动调用init
和init_array
,JNI_OnLoad
的调用可以自己选择,如果JNI_OnLoad
里没有重要的函数的话可以不调用,硬要调用的时候可能需要补环境
Inspector.inspect(..., false)
打印Java的byte数组
1 2
| byte[] data = test.ttEncrypt(); Inspector.inspect(data, "ttEncrypt");
|
callStaticJniMethodObject
函数比callFunction
多封装了一些代码,不需要自己寻找函数地址,不需要自己包装函数
1 2 3 4 5 6 7
|
ByteArray array = TTEncryptUtils.callStaticJniMethodObject(emulator, "ttEncrypt([BI)[B", new ByteArray(vm, data), data.length);
return array.getValue();
|
Demo
以com.xxx.ndk
为例,在Java目录下新建package,命名最好就是包名,然后新建Java文件,命名随意,把TTEncrypt的代码全部复制过去,函数名自行修改
对应native的java类如下,注意package名不是app包名,app包名在AndroidManifest.xml
里
1 2 3 4 5 6 7 8 9 10
| package com.xxx.ndk;
public class NativeHelper { public static native int add(int i,int i2,int i3); public static native String encode(); public static native String md5(String str); static { System.loadLibrary("xxxxx"); } }
|
调用add函数
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
| package com.DemoTest.ndk;
import ...;
public class NativeHelper {
private final AndroidEmulator emulator; private final VM vm; private final Module module;
private final DvmClass NativeHelper;
private final boolean logging;
NativeHelper(boolean logging) { this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.DemoTest.app").build(); final Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(); vm.setVerbose(logging); DalvikModule dm = vm.loadLibrary(new File("xxx.so"), false); module = dm.getModule(); NativeHelper = vm.resolveClass("com/DemoTest/ndk/NativeHelper"); }
void destroy() throws IOException { emulator.close(); if (logging) { System.out.println("destroy"); } }
public static void main(String[] args) throws Exception { NativeHelper test = new NativeHelper(true); int retval = test.callFunc(); System.out.println("retval: 0x" + Integer.toHexString(retval)); test.destroy(); }
int callFunc() { int retval = NativeHelper.callStaticJniMethodInt(emulator, "add(III)I", 0x100, 0x200, 0x300); return retval; }
}
|
调用md5函数
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.DemoTest.ndk;
import ...;
public class NativeHelper extends AbstractJni {
private final AndroidEmulator emulator; private final VM vm; private final Module module;
private final DvmClass NativeHelper;
private final boolean logging;
NativeHelper(boolean logging) { this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.DemoTest.app").build(); final Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM();
vm.setJni(this);
vm.setVerbose(logging); DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/DemoTest/ndk/libDemoTest.so"), false); module = dm.getModule(); NativeHelper = vm.resolveClass("com/DemoTest/ndk/NativeHelper"); }
void destroy() throws IOException { emulator.close(); if (logging) { System.out.println("destroy"); } }
public static void main(String[] args) throws Exception { NativeHelper test = new NativeHelper(true); test.callFunc(); test.destroy(); }
void callFunc() {
StringObject md5Result = NativeHelper.callStaticJniMethodObject(emulator, "md5(Ljava/lang/String;)Ljava/lang/String;", new StringObject(vm, "DemoTest")); System.out.println("md5Result: " + md5Result.getValue());
}
@Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { System.out.println("signature: " + signature); if(signature.equals("java/lang/String->getBytes(Ljava/lang/String;)[B")) { String args = (String) dvmObject.getValue(); System.out.println("args: " + args); byte[] strBytes = "unidbg".getBytes(); return new ByteArray(vm, strBytes); } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }
}
|
处理so调用系统Java类
setJni()
的作用,默认情况下,createDalvikVM()
只创建了一个 DalvikVM
实例,并不会自动附加 JNI 处理器。如果不 setJni(jni)
,那么 Unidbg 在执行 JNI 相关方法(如 CallObjectMethod
、FindClass
)时就会报 IllegalStateException
。
需要调用 setJni(jni)
来设置一个 DvmJni
实例,让 Unidbg 处理 JNI
方法。这个 DvmJni
实例可以继承 AbstractJni
,然后你可以重写 JNI 函数。
如上,如果so代码里调用了JNI函数,unidbg实现了大部分的JNI函数,对于没有实现的,需要自己实现,已经实现的,类似callObjectMethodV
,需要自己分析so,做有针对性的重写,可以通过vm.setjni(this)
重写AbstractJni方法,如果某个so文件通过JNI访问比较多的Java类,或者IDA反编译的so,逻辑不是很清楚,可以借助jnitrace来补环境
比如我们使用unidbg提供的类AbstractJni
作为参数去调用setJni()
,setJni()
的参数是Jni类型的,而AbstractJni里面实现了一部分callObjectMethod
调用的JNI方法,只能说大部分,当然还有里面没有的JNI方法,比如我们自己写的某个类里面的某个方法,如果没有,我们就需要自己去重写callObjectMethod
方法,来接管JNI函数
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
| public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature) { case "android/app/Application->getAssets()Landroid/content/res/AssetManager;": return new AssetManager(vm, signature); case "android/app/Application->getClassLoader()Ljava/lang/ClassLoader;": return new ClassLoader(vm, signature); case "android/app/Application->getContentResolver()Landroid/content/ContentResolver;": return vm.resolveClass("android/content/ContentResolver").newObject(signature); case "java/util/ArrayList->get(I)Ljava/lang/Object;": { int index = vaList.getIntArg(0); ArrayListObject arrayList = (ArrayListObject) dvmObject; return arrayList.getValue().get(index); } case "android/app/Application->getSystemService(Ljava/lang/String;)Ljava/lang/Object;": { StringObject serviceName = vaList.getObjectArg(0); assert serviceName != null; return new SystemService(vm, serviceName.getValue()); } case "java/lang/String->toString()Ljava/lang/String;": return dvmObject; case "java/lang/Class->getName()Ljava/lang/String;": return new StringObject(vm, ((DvmClass) dvmObject).getName()); case "android/view/accessibility/AccessibilityManager->getEnabledAccessibilityServiceList(I)Ljava/util/List;": return new ArrayListObject(vm, Collections.<DvmObject<?>>emptyList()); case "java/util/Enumeration->nextElement()Ljava/lang/Object;": return ((Enumeration) dvmObject).nextElement(); case "java/util/Locale->getLanguage()Ljava/lang/String;": Locale locale = (Locale) dvmObject.getValue(); return new StringObject(vm, locale.getLanguage()); case "java/util/Locale->getCountry()Ljava/lang/String;": locale = (Locale) dvmObject.getValue(); return new StringObject(vm, locale.getCountry()); case "android/os/IServiceManager->getService(Ljava/lang/String;)Landroid/os/IBinder;": { ServiceManager serviceManager = (ServiceManager) dvmObject; StringObject serviceName = vaList.getObjectArg(0); assert serviceName != null; return serviceManager.getService(vm, serviceName.getValue()); } case "java/io/File->getAbsolutePath()Ljava/lang/String;": File file = (File) dvmObject.getValue(); return new StringObject(vm, file.getAbsolutePath()); case "android/app/Application->getPackageManager()Landroid/content/pm/PackageManager;": case "android/content/ContextWrapper->getPackageManager()Landroid/content/pm/PackageManager;": case "android/content/Context->getPackageManager()Landroid/content/pm/PackageManager;": DvmClass clazz = vm.resolveClass("android/content/pm/PackageManager"); return clazz.newObject(signature); case "android/content/pm/PackageManager->getPackageInfo(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;": { StringObject packageName = vaList.getObjectArg(0); assert packageName != null; int flags = vaList.getIntArg(1); if (log.isDebugEnabled()) { log.debug("getPackageInfo packageName=" + packageName.getValue() + ", flags=0x" + Integer.toHexString(flags)); } return new PackageInfo(vm, packageName.value, flags); } case "android/app/Application->getPackageName()Ljava/lang/String;": case "android/content/ContextWrapper->getPackageName()Ljava/lang/String;": case "android/content/Context->getPackageName()Ljava/lang/String;": { String packageName = vm.getPackageName(); if (packageName != null) { return new StringObject(vm, packageName); } break; } case "android/content/pm/Signature->toByteArray()[B": if (dvmObject instanceof Signature) { Signature sig = (Signature) dvmObject; return new ByteArray(vm, sig.toByteArray()); } break; case "android/content/pm/Signature->toCharsString()Ljava/lang/String;": if (dvmObject instanceof Signature) { Signature sig = (Signature) dvmObject; return new StringObject(vm, sig.toCharsString()); } break; case "java/lang/String->getBytes()[B": { String str = (String) dvmObject.getValue(); return new ByteArray(vm, str.getBytes()); } case "java/lang/String->getBytes(Ljava/lang/String;)[B": String str = (String) dvmObject.getValue(); StringObject charsetName = vaList.getObjectArg(0); assert charsetName != null; try { return new ByteArray(vm, str.getBytes(charsetName.value)); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } case "java/security/cert/CertificateFactory->generateCertificate(Ljava/io/InputStream;)Ljava/security/cert/Certificate;": CertificateFactory factory = (CertificateFactory) dvmObject.value; DvmObject<?> stream = vaList.getObjectArg(0); assert stream != null; InputStream inputStream = (InputStream) stream.value; try { return vm.resolveClass("java/security/cert/Certificate").newObject(factory.generateCertificate(inputStream)); } catch (CertificateException e) { throw new IllegalStateException(e); } case "java/security/cert/Certificate->getEncoded()[B": { Certificate certificate = (Certificate) dvmObject.value; try { return new ByteArray(vm, certificate.getEncoded()); } catch (CertificateEncodingException e) { throw new IllegalStateException(e); } } case "java/security/MessageDigest->digest([B)[B": { MessageDigest messageDigest = (MessageDigest) dvmObject.value; ByteArray array = vaList.getObjectArg(0); assert array != null; return new ByteArray(vm, messageDigest.digest(array.value)); } case "java/util/ArrayList->remove(I)Ljava/lang/Object;": { int index = vaList.getIntArg(0); ArrayListObject list = (ArrayListObject) dvmObject; return list.value.remove(index); } case "java/util/List->get(I)Ljava/lang/Object;": List<?> list = (List<?>) dvmObject.getValue(); return (DvmObject<?>) list.get(vaList.getIntArg(0)); case "java/util/Map->entrySet()Ljava/util/Set;": Map<?, ?> map = (Map<?, ?>) dvmObject.getValue(); return vm.resolveClass("java/util/Set").newObject(map.entrySet()); case "java/util/Set->iterator()Ljava/util/Iterator;": Set<?> set = (Set<?>) dvmObject.getValue(); return vm.resolveClass("java/util/Iterator").newObject(set.iterator()); }
|
比如需要实现getBytes
方法,那么就可以通过signature
来判断是不是要调用这个方法,注意重写的函数的返回值要是unidbg的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| vm.setJni(new AbstractJni() { @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { System.out.println("signature: " + signature); if(signature.equals("java/lang/String->getBytes(Ljava/lang/String;)[B")) { String args = (String) dvmObject.getValue(); System.out.println("args: " + args); byte[] strBytes = "unidbg".getBytes(); return new ByteArray(vm, strBytes); } return super.callObjectMethodV(vm, dvmObject, signature, vaList); } });
|
处理so调用其他的so
如果被调用的函数,需要先执行JNI_OnLoad
(比如在JNI_OnLoad
里注册的动态函数)或者其他函数,那么就按顺序调用,手动调用JNI_OnLoad
1
| dm.callJNI_OnLoad(emulator);
|
如果so中调用了其他的so,比如导入表里有其他so文件的函数,只需要按顺序加载所需的so即可,单纯加载一下就行,不需要做其他操作
1
| DalvikModule dmA = vm.loadLibrary(new File("unidbg-android/src/test/java/com/xx/ndk/xx.so"), false);
|
dlopen会自己处理,因为unidbg加载了libdl.so
C/C++标准库也会自己处理,因为unidbg加载了libc.so、libc++.so
注意事项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 如果函数在多个类中,就需要resolveClass多个类,也就是说一个函数对应一次resolveClass,不管多个函数是不是在同一个so中
resolveClass其实就是封装了寻址的过程
如果函数在多个so中,就需要加载多个so,按顺序加载
在加载目标so文件之前,unidbg还加载了一些系统so
因此可以使用标准C函数如dlopen等
纯so的代码本来也是可以执行的
JNI函数也实现了一些,有一部分JNI函数需要手动接管
将unidbg日志全开,src/test/resources/log4j.properties中INFO全改成DEBUG
|
通过符号调用函数
第一种是使用module.findSymbolByName
进行调用
1 2 3 4 5 6 7 8
| Symbol symbol = module.findSymbolByName(...);
Number[] numbers = symbol.call(...);
Number[] numbers = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(NativeHelper), 100, 200, 300); int retval = numbers[0].intValue();
|
注意上面int retval = numbers[0].intValue();
并不是直接获取的返回值(除非函数本身返回的是基本类型),否则这个值只是Java虚拟机中的引用,所以在得到Java的int数据后,跟进这个数据去内存中获取对象或者数据
1 2 3 4 5 6
| 比如返回的是Java对象 vm.getObject(retval) //相当于vm.addLocalObject的逆过程 比如返回的是int类型的地址 emulator.getMemory().pointer(retval).getByteArray(...,...); 比如返回的是int类型的长度 emulator.getMemory().getByteArray(...,retval);
|
第二种是使用module.callFunction
进行调用
1
| Number[] numbers = module.callFunction(emulator, "_Z7_strcatP7_JNIEnvP7_jclass", vm.getJNIEnv(), vm.addLocalObject(NativeHelper));
|
模拟callStaticJniMethodObject
的过程
1 2 3 4 5 6 7 8 9
| 找到符号对应地址 包装传入的参数 Java类型的传递 JNIEnv*的获取 vm.getJNIEnv() jclass/jobject的构建 DvmClass xxx = vm.resolveClass(...) DvmObject xxxx = xxx.newObject(null) //如果要传入jobject的话,就调用newObject进行转换 vm.addLocalObject(xxxx) //Java类或者对象以引用的方式传入 其他Java类型的传递 StringObject、ByteArray等 需要包装成DvmObject类,不然识别不了
|
通过偏移调用函数
使用module.callFunction
来通过偏移调用函数,注意是相对偏移,而且注意是否需要+1
1
| Number[] numbers = module.callFunction(emulator, 0x1B4C, vm.getJNIEnv(), vm.addLocalObject(NativeHelper));
|
如果参数有结构体,结构体实际上就是一段连续的内存,而且存在内存对齐,对齐的话不用特别管,直接给个足够大的就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
UnidbgPointer MD5Ctx = emulator.getMemory().malloc(200, false).getPointer(); module.callFunction(emulator, 0x2230, MD5Ctx);
UnidbgPointer plainText = emulator.getMemory().malloc(200, false).getPointer(); byte[] buffer = "DemoTest_unidbg".getBytes(); plainText.write(buffer); module.callFunction(emulator, 0x22A0, MD5Ctx, plainText, buffer.length);
UnidbgPointer cipherText = emulator.getMemory().malloc(200, false).getPointer(); module.callFunction(emulator, 0x3A78, MD5Ctx, cipherText);
byte[] byteArray = cipherText.getByteArray(0, 16); Inspector.inspect(byteArray, "MD5Result");
|
关于类型的转换说明,在 Unidbg 里:
1 2 3
| Java 层的对象(String、Integer、ArrayList 等)不能直接传递给 call() 方法,因为 Unidbg 运行的是 Native 代码(C/C++),而 C 代码里的 JNI 只能处理 jobject、jstring 这类 JNI 对象。
Unidbg 里用 DvmObject 来模拟 JNI 里的 jobject,所以如果要把 Java 数据传入 Native 层,就要先包装成 DvmObject。
|
unidbg中的hook
unidbg支持dobby、hookzz、whale、xhook
hookzz是dobby的前身,hookzz对32位支持较好,dobby对64位支持较好
unidbg支持unicorn自带的各种hook(指令级hook、块级hook、内存读写hook、异常hook)以及unidbg封装后的console debugger
掌握原生unicorn hook以及console debugger即可,原生unicorn hook不容易被检测,console debugger可下多个断点
可以Hook一些系统函数比如pthread等,可以看出app是否调用了某些方法来检测,hook掉一些函数或者指令来过反调试、反hook,unidbg没法处理子线程中的操作,子线程做了什么事是无法模拟的,如果子线程中有重要函数的话,就需要自己去处理了,可以hook相应位置,用Java实现子线程的逻辑,把执行结果返回给寄存器
hookzz
hookzz在32位更稳定,可以通过符号hook,也可以通过地址hook
1 2
| <T extends RegisterContext> void wrap(long functionAddress, WrapCallback<T> callback); <T extends RegisterContext> void wrap(Symbol symbol, WrapCallback<T> callback);
|
hookzz支持函数hook、inline hook(hookzz.wrap、hookzz.instrument)
1 2 3
| new WrapCallback<RegisterContext>() new InstrumentCallback<Arm32RegisterContext>() RegisterContext、Arm32RegisterContext、Arm64RegisterContext
|
hookzz.wrap
hook函数的核心就是使用hookZz.wrap
方法,第一个参数是符号名,第二个参数是匿名类,匿名类中需要实现preCall
和postCall
方法,preCall
是原函数调用之前调用,postCall
是原函数调用之后调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.findSymbolByName("_Z9MD5UpdateP7MD5_CTXPhj"), new WrapCallback<RegisterContext>() { @Override public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) { Pointer md5_ctx = ctx.getPointerArg(0); Pointer plainText = ctx.getPointerArg(1); int length = ctx.getIntArg(2); Inspector.inspect(md5_ctx.getByteArray(0, 64), "preCall md5_ctx"); Inspector.inspect(plainText.getByteArray(0, length), "plainText"); } @Override public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) { Inspector.inspect(md5_ctx.getByteArray(0, 64), "postCall md5_ctx"); } });
|
hookzz.instrument
同样也是可以使用符号或者地址进行hook,主要用于inline hook
1 2 3 4 5 6 7 8
| hookZz.instrument(module.base + 0x1AEC, new InstrumentCallback<Arm64RegisterContext>() { @Override public void dbiCall(Emulator<?> emulator, Arm64RegisterContext ctx, HookEntryInfo info) { System.out.println("w8=0x" + Integer.toHexString(ctx.getXInt(8)) + ", w9=0x" + Integer.toHexString(ctx.getXInt(9))); } });
|
参数的获取
取出Java类型
1 2
| int number = emulator.getContext().getIntArg(1); vm.getObject(number);
|
以内存写入方式传递参数,取参数时就读内存
以addLocalObject
方式传入Java类型的参数,取参数时用getObject
举例,因为有的时候使用ctx.getIntArg()
方法获取的值并不是真正的参数,而是参数在vm中的hashcode,所以还需要调用getObject()
来转换为Java中可以使用的数据类型
1 2 3 4 5
| public void preCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) { int intArg = ctx.getIntArg(1); StringObject str = vm.getObject(intArg); System.out.println("preCall str = " + str.getValue()); }
|
hookzz.replace
用于替换原函数
替换后依然可以调用原函数
不调用原函数的返回设置
1
| HookStatus.LR(emulator, value)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| IHookZz hookZz = HookZz.getInstance(emulator); hookZz.replace(module.findSymbolByName("Java_com_DemoTest_ndk_NativeHelper_md5"), new ReplaceCallback() { @Override public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) { System.out.println("onCall:123456"); return HookStatus.LR(emulator, 100); } });
int md5Result = NativeHelper.callStaticJniMethodInt(emulator, "md5(Ljava/lang/String;)Ljava/lang/String;", new StringObject(vm, "testtttt")); System.out.println("md5Result: " + md5Result);
|
原生unicorn hook
基于原生unicorn API进行hook的时候,不需要考虑是否+1,会自己转换
unicorn原生API进行inline hook
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
|
emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { RegisterContext context = emulator.getContext(); System.out.println(user); if (address == module.base + 0x1FF4){ Pointer md5Ctx = context.getPointerArg(0); Inspector.inspect(md5Ctx.getByteArray(0, 32), "md5Ctx"); Pointer plainText = context.getPointerArg(1); int length = context.getIntArg(2); Inspector.inspect(plainText.getByteArray(0, length), "plainText"); }else if (address == module.base + 0x2004){ Pointer cipherText = context.getPointerArg(1); Inspector.inspect(cipherText.getByteArray(0, 16), "cipherText"); }
} @Override public void onAttach(UnHook unHook) { } @Override public void detach() { } }, module.base + 0x1FE8, module.base + 0x2004, "DemoTest"); StringObject md5Result = NativeHelper.callStaticJniMethodObject(emulator, "md5(Ljava/lang/String;)Ljava/lang/String;", new StringObject(vm, "DemoTest")); System.out.println("md5Result: " + md5Result.getValue());
|
打印调用栈
1
| emulator.getUnwinder().unwind();
|
如果函数栈只有一层的话是打印不出来的
unidbg中的动态调试
基于unicorn的console debugger,同样不用管地址是否+1
attach下断点,要先下断点再调用目标函数,不然触发不了
1 2 3
| Debugger debugger = emulator.attach(); debugger.addBreakPoint(module.base + 0x1AF4); debugger.addBreakPoint(module.base + 0x1AF8);
|
emulator.attach
支持几种调试方式,默认是console debugger
类似IDA动态调试,断点触发之后,会显示寄存器信息,汇编指令
可以通过输入命令进行打印内存、写寄存器、跳到下一个断点、打印函数栈等操作
1 2 3 4 5 6 7
| 回车两下或者随便输错一个指令,就会打印帮助信息
b用于下断点 blr
m用于读取内存 mr0 mx0 m0x40118554 msp
bt用于查看函数栈
|
监控内存读写
将信息输出到文件
1 2
| String traceFile = "yourPath"; PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true);
|
监控内存读
1
| emulator.traceRead(module.base,module.base + module.size).setRedirect(traceStream);
|
监控内存写
1
| emulator.traceWrite(module.base,module.base + module.size).setRedirect(traceStream);
|
比如在字符串中看到了一个字符串,我们想知道哪些地址访问了这个字符串,就可以使用监控,一般是监控整个so文件,然后从结果里面筛选,也可以监控指定地址
unidbg trace
比如一段只有10行的汇编代码,被ollvm混淆之后,可能膨胀到1000行,这1000行代码在实际执行的时候不可能都执行到,那怎么知道在执行so文件的过程中执行了哪些代码呢?就要用到trace
基本unidbg的trace写法
1 2 3
| String traceFile = "yourPath"; PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true); emulator.traceCode(module.base,module.base + module.size).setRedirect(traceStream);
|
unidbg默认的traceCode,只打印汇编指令,不打印寄存器的值,通过修改源代码,可以让其打印每行代码执行过程的寄存器的值
代码修改的话,如果是64位的so,全局搜索”Trace Instruction”字符串,找到
1
| /unidbg-api/src/main/java/com/github/unidbg/arm/AbstractARM64Emulator.java
|
修改里面的printAssemble
方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| private void printAssemble(PrintStream out, Capstone.CsInsn[] insns, long address) { StringBuilder sb = new StringBuilder(); for (Capstone.CsInsn ins : insns) { sb.append("### Trace Instruction "); sb.append(ARM.assembleDetail(this, ins, address, false)); sb.append('\t'); sb.append('\t'); sb.append(ARM.showRegsARM64(this)); sb.append('\n'); address += ins.size; } out.print(sb); }
|
在\unidbg-api\src\main\java\com\github\unidbg\arm\ARM.java
中添加如下代码
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
| public class ARM { private static HashMap<String, Long> allRegs64 = new HashMap<>(); ... public static String showRegsARM64(Emulator<?> emulator) { return MyShowRegs64(emulator, ARM.MyARM64_REGS); } public static String MyShowRegs64(Emulator<?> emulator, int[] regs) { Backend backend = emulator.getBackend(); if (regs == null || regs.length < 1) { regs = ARM.getAll64Registers(); } StringBuilder builder = new StringBuilder(); builder.append(">>>"); for (int reg : regs) { Number number; long value; switch (reg) { case Arm64Const.UC_ARM64_REG_NZCV: Cpsr cpsr = Cpsr.getArm64(backend); if (cpsr.isA32()) { builder.append(String.format(Locale.US, " cpsr: N=%d, Z=%d, C=%d, V=%d, T=%d, mode=0b", cpsr.isNegative() ? 1 : 0, cpsr.isZero() ? 1 : 0, cpsr.hasCarry() ? 1 : 0, cpsr.isOverflow() ? 1 : 0, cpsr.isThumb() ? 1 : 0)).append(Integer.toBinaryString(cpsr.getMode())); } else { int el = cpsr.getEL(); builder.append(String.format(Locale.US, "\nnzcv: N=%d, Z=%d, C=%d, V=%d, EL%d, use SP_EL", cpsr.isNegative() ? 1 : 0, cpsr.isZero() ? 1 : 0, cpsr.hasCarry() ? 1 : 0, cpsr.isOverflow() ? 1 : 0, el)).append((cpsr.getValue() & 1) == 0 ? 0 : el); } break; case Arm64Const.UC_ARM64_REG_X0: number = backend.reg_read(reg); value = number.longValue();
if (allRegs64.containsKey("UC_ARM64_REG_X0")) { long oldValue = allRegs64.get("UC_ARM64_REG_X0"); if(oldValue == value){ break; } } allRegs64.put("UC_ARM64_REG_X0", value);
builder.append(String.format(Locale.US, " x0=0x%x", value)); if (value < 0) { builder.append('(').append(value).append(')'); } else if((value & 0x7fffffff00000000L) == 0) { int iv = (int) value; if (iv < 0) { builder.append('(').append(iv).append(')'); } } break; case Arm64Const.UC_ARM64_REG_X1: number = backend.reg_read(reg); value = number.longValue();
if (allRegs64.containsKey("UC_ARM64_REG_X1")) { long oldValue = allRegs64.get("UC_ARM64_REG_X1"); if(oldValue == value){ break; } } allRegs64.put("UC_ARM64_REG_X1", value);
builder.append(String.format(Locale.US, " x1=0x%x", value)); break; ... } } System.out.println(builder); return builder.toString(); }
|
unidbg中的virtualModule
某些so可能整个加载不起来,或者处理起来很麻烦
比如一个so文件,依赖了十几个so文件,而且只是用到了其他so文件中的某个特定函数,这个时候如果把所有的so都加载进去就很麻烦,可以选择unidbg的虚拟模块功能,可以用来注册虚拟的so,自己实现so中相应要用到的方法
以libDemoTest.so
调用偶尔libDemoTestA.so
的bssFunc
方法为例
找到/src/main/java/com/github/unidbg/virtualmodule/android/AndroidModule
把这个类复制到test路径下的对应目录,修改构造函数里面的so文件名,onInitialize
方法修改方法名(so层的方法名),在handle()
方法中需要实现我们自己的bssFunc
方法的逻辑,就类似于hook
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
| package com.DemoTest.ndk;
import ...;
import java.util.Map;
public class DemoTestAModule extends VirtualModule<VM> {
public DemoTestAModule(Emulator<?> emulator, VM vm) { super(emulator, vm, "libDemoTestA.so"); }
@Override protected void onInitialize(Emulator<?> emulator, final VM vm, Map<String, UnidbgPointer> symbols) { boolean is64Bit = emulator.is64Bit(); SvcMemory svcMemory = emulator.getSvcMemory(); symbols.put("_Z7bssFuncv", svcMemory.registerSvc(is64Bit ? new Arm64Svc() { @Override public long handle(Emulator<?> emulator) { fromJava(emulator, vm); return 0; } } : new ArmSvc() { @Override public long handle(Emulator<?> emulator) { fromJava(emulator, vm); return 0; } }));
}
private static void fromJava(Emulator<?> emulator, VM vm) { System.out.println("libDemoTestA.so"); }
}
|
在hook代码中注册虚拟so
1
| new DemoTestAModule(emulator, vm).register(memory);
|