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(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/libttEncrypt.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
// 加载app里的指定类,类的路径是apk里的路径
TTEncryptUtils = vm.resolveClass("com/bytedance/frameworks/core/encrypt/TTEncryptUtils");
}

如上,会自动调用initinit_arrayJNI_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
// 第二个参数是函数名和函数签名,函数名是java中的名字,不是so中的名字
// 第三个参数开始是对应的参数,注意有些参数不能直接传,哪怕这里data以及定义成了byte[],也需要使用unidbg提供的函数封装之后再传参,数值可以直接传
// 有些版本的unidbg也可以不用封装直接传
ByteArray array = TTEncryptUtils.callStaticJniMethodObject(emulator, "ttEncrypt([BI)[B", new ByteArray(vm, data), data.length); // 执行Jni方法

// 返回的array还需要转换成原生Java类型才能被Java代码识别
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(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("xxx.so"), false); // 加载xxx.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
//dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的 libDemoTest.so 对应为一个模块
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); // 执行Jni方法
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(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

vm = emulator.createDalvikVM(); // 创建Android虚拟机

// 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 = args.getBytes();
// byte[] strBytes = "unidbg".getBytes();
// return new ByteArray(vm, strBytes);
// }
// return super.callObjectMethodV(vm, dvmObject, signature, vaList);
// }
// });
// setJni是为了绑定jni环境,这样如果目标方法里调用了JNI函数才不会报错
// setJni就是来绑定JNI函数的,也完全可以自己手动去接管JNI函数,也就是重写
// 这里使用this的原因是当前类继承了AbstractJni类
vm.setJni(this);

vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/DemoTest/ndk/libDemoTest.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
//dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的 libDemoTest.so 对应为一个模块
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() {
// int retval = NativeHelper.callStaticJniMethodInt(emulator, "add(III)I", 0x100, 0x200, 0x300); // 执行Jni方法
// System.out.println("retval: 0x" + Integer.toHexString(retval));

StringObject md5Result = NativeHelper.callStaticJniMethodObject(emulator, "md5(Ljava/lang/String;)Ljava/lang/String;", new StringObject(vm, "DemoTest")); // 执行Jni方法
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 = args.getBytes();
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 相关方法(如 CallObjectMethodFindClass)时就会报 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")) {
// dvmObject是参数
String args = (String) dvmObject.getValue();
System.out.println("args: " + args);
//byte[] strBytes = args.getBytes();
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); // 手动执行JNI_OnLoad函数

如果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(...);
// call方法的第一个参数是模拟器,第二个参数是参数列表,注意这里调用的是so层的函数
// 所以第一个参数是JNIEnv,第二个参数是JNIObject或者JClass
// 而且传入对象类型的参数的时候需要使用包装之后的类型并调用vm.addLocalObject()
Number[] numbers = symbol.call(...);
//例如,int类型直接传,NativeHelper本身在上面就是unidbg类型不用包装,调用addLocalObject之后再传
Number[] numbers = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(NativeHelper), 100, 200, 300);
int retval = numbers[0].intValue(); //返回Number的数组,要第0个,而且调用intValue将unidbg类型的int转为java的int

注意上面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
//MD5Init 2230
//通过MD5Init来给结构体的内存赋值
UnidbgPointer MD5Ctx = emulator.getMemory().malloc(200, false).getPointer();
module.callFunction(emulator, 0x2230, MD5Ctx);
//MD5Update 22A0
UnidbgPointer plainText = emulator.getMemory().malloc(200, false).getPointer();
byte[] buffer = "DemoTest_unidbg".getBytes();
plainText.write(buffer);
module.callFunction(emulator, 0x22A0, MD5Ctx, plainText, buffer.length);
//MD5Final 3A78
UnidbgPointer cipherText = emulator.getMemory().malloc(200, false).getPointer();
module.callFunction(emulator, 0x3A78, MD5Ctx, cipherText);
// 读取指定地址开始的16字节数据
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方法,第一个参数是符号名,第二个参数是匿名类,匿名类中需要实现preCallpostCall方法,preCall是原函数调用之前调用,postCall是原函数调用之后调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 以hook md5update为例,该函数前两个参数是指针,第三个参数是int
IHookZz hookZz = HookZz.getInstance(emulator);
// RegisterContext是Arm32RegisterContext、Arm64RegisterContext的父类
// 这里也可以是Arm32RegisterContext或者Arm64RegisterContext
hookZz.wrap(module.findSymbolByName("_Z9MD5UpdateP7MD5_CTXPhj"), new WrapCallback<RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
// ctx存储的就是参数
// 前两个参数是指针,使用getPointerArg获取
Pointer md5_ctx = ctx.getPointerArg(0);
Pointer plainText = ctx.getPointerArg(1);
// 第三个参数是int,使用getIntArg获取
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
// 假设要hook的代码是ADD W9,W8,W9
hookZz.instrument(module.base + 0x1AEC, new InstrumentCallback<Arm64RegisterContext>() {
@Override
// dbiCall是当该行汇编代码被执行的时候触发
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
// onCall是替换的函数
// 此外还有postCall方法可以实现,是onCall执行完毕之后执行的代码
// originFunction是原函数的地址
public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
System.out.println("onCall:123456");
//return super.onCall(emulator, context, originFunction);调用原函数
//return HookStatus.RET(emulator, originFunction);调用原函数
// HookStatus.LR是不调用原函数,第二个参数是返回值
return HookStatus.LR(emulator, 100);
}
});
// 因为函数已经被替换为返回值是int了,所以不能用callStaticJniMethodObject
int md5Result = NativeHelper.callStaticJniMethodInt(emulator, "md5(Ljava/lang/String;)Ljava/lang/String;", new StringObject(vm, "testtttt")); // 执行Jni方法
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
// CodeHook是指令级hook
// 第二个参数是从哪里开始hook,第三个参数是从哪里结束hook,都是内存里的实际地址不是相对偏移
// 效果是从开始地址起,到结束地址为止,每执行一行指令都会调用一次hook方法中的代码
// 所以可以在hook方法里进行判断,当执行到目标地址的代码的时候,去打印参数
// 第四个参数是外部传入的数据,可以在hook内部使用
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("DemoTest");
System.out.println(user);
//System.out.println(size);
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")); // 执行Jni方法
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 {
//allRegs64是记录寄存器的值的键值对Map
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();
// 如果键值对中已经有X0,就比较值是否变化,如果变化就更新,没变化就break

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.sobssFunc方法为例

找到/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);