Unidbg 在 iOS 平台的随笔小记
By dwj1210 at
unidbg 是一个基于 Unicorn 的逆向工具,而 Unicorn 则是一个轻量级、跨平台、支持多架构的 CPU 模拟器框架。所以 unidbg 可以实现黑盒调用 Android 和 iOS 中的 so 文件。
之前看到一些关于使用 unidbg 模拟执行去除 ollvm 混淆的文章,由于一直没有这方面的研究需求,所以一直没关注过这个工具,但是最近在考虑黑产如果脱机调用客户端加密算法时,经同事大佬提醒 unidbg 可以模拟执行 Android so 中的 native 函数,由此打开思路。所以尝试下使用 unidbg 脱机黑盒调用 iOS 函数。
我们拿网上下载的开源 demo 来做下测试,比如想使用 unidbg 来调用这个 +[UTDIDAES AES256EncryptWithKey:forKey:]
加密的方法:
只需要把这个 demo 的编译好的二进制文件放到项目 xx 目录下,然后通过 emulator.loadLibrary 来加载这个二进制
编写 Java 的调用代码,新建 CustomLoaderTest 类
public class CustomLoaderTest extends EmulatorTest<ARMEmulator<DarwinFileIO>> {
private void processXpcNoPie() {
// 加载要模拟执行的二进制文件
Module module = emulator.loadLibrary(new File("unidbg-ios/src/test/resources/example_binaries/AliBCTradeDemo"));
ObjC objc = ObjC.getInstance(emulator);
// 创建一个 oc 的 NSString 字符串
ObjcObject obj_str = objc.getClass("NSString").callObjc("stringWithCString:encoding:", "1111", 4);
// 将字符串转成 NSData 类型
// 因为从上图伪代码中可以看到方法第一个入参是 NSData 类型
// 当然这些是需要在编写调用代码前,需要用动态或静态分析去确定参数类型
ObjcObject obj_data = obj_str.callObjc("dataUsingEncoding:",4);
// 构造加密需要的 key
NSString key = objc.newString("00000000000000000000");
// 调用 +[UTDIDAES AES256EncryptWithKey:forKey:] 方法
ObjcObject obj = objc.getClass("UTDIDAES").callObjc("AES256EncryptWithKey:forKey:", obj_data,key);
// 从上图伪代码第 32 行可以确定返回值也是 NSData 类型,我们这里转成 NSString 方便打印
ObjcObject result = obj.callObjc("base64EncodedStringWithOptions:", 0);
System.out.print("encrypt result -> " + result.getDescription());
}
public static void main(String[] args) throws Exception {
CustomLoaderTest test = new CustomLoaderTest();
test.setUp();
test.processXpcNoPie();
test.tearDown();
}
@Override
protected LibraryResolver createLibraryResolver() {
return new DarwinResolver();
}
@Override
protected ARMEmulator<DarwinFileIO> createARMEmulator() {
return DarwinEmulatorBuilder.for64Bit().build();
}
}
运行 main 函数可以正确打印出来加密的结果:
姿势二:
private void processXpcNoPie2() {
// 加载要模拟执行的二进制文件
Module module = emulator.loadLibrary(new File("unidbg-ios/src/test/resources/example_binaries/AliBCTradeDemo"));
ObjC objc = ObjC.getInstance(emulator);
// 创建一个 oc 的 NSString 字符串
ObjcObject obj_str = objc.getClass("NSString").callObjc("stringWithCString:encoding:", "1111", 4);
// 将字符串转成 NSData 类型
// 因为从上图伪代码中可以看到方法第一个入参是 NSData 类型
// 当然这些是需要在编写调用代码前,需要用动态或静态分析去确定参数类型
ObjcObject obj_data = obj_str.callObjc("dataUsingEncoding:", 4);
// 构造加密需要的 key
NSString key = objc.newString("00000000000000000000");
ObjcObject clz = objc.getClass("UTDIDAES");
NSString sel = objc.newString("AES256EncryptWithKey:forKey:");
// 方法 +[UTDIDAES AES256EncryptWithKey:forKey:] 的偏移是 0x10021A338
Number result = module.callFunction(emulator, 0x10021A338L, clz, sel, obj_data, key);
// 获取指针
UnidbgPointer pointer = UnidbgPointer.pointer(emulator, result.longValue());
// 读取内存中的 byte[]
byte[] dataBytes = pointer.getByteArray(0, 16);
// Base64 编码
String base64Data = Base64.encodeBase64String(dataBytes);
System.out.print("encrypt result -> " + base64Data.toString());
}
根据 Arm64 的调用规则,x0-x7 通用寄存器可以用来传递参数,所以我们可以模拟调用 oc 方法:x0 寄存器传调用方,x1 寄存器传方法名,后面依次传入参数。
unidbg 同时支持二进制的断点、调试及汇编级的代码 trace,下面来举例看下:
比如我们想在这行汇编进行调试
对应这行伪代码:
这句伪代码的意思是在加密完以后将 byte 数组转成 iOS 中的 NSData 类型对象的方法,我们希望在这里断点来看下内存中的密文数据;在上面的代码中添加以下这两句代码:
emulator.attach(DebuggerType.CONSOLE);
emulator.attach().addBreakPoint(module.base + 0x10021A4DCL);
此时运行代码在执行到 0x10021A4DC 的时候会停下来
unidbg 里内置了很多调试命令,按回车会直接显示,此时我们使用 mx2
来查看第二个寄存器的值
第一行的 B7 FF... 就是我们加密完的密文数据,我们也可以把前面 base64 后的密文解码对比下这段数据:
由于目前对于 iOS 的模拟执行还不成熟,在使用过程中遇到了许多坑,希望后面能把坑填上再来分享进一步的使用心得。