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:] 加密的方法:

img

只需要把这个 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 函数可以正确打印出来加密的结果:

img

姿势二:

    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,下面来举例看下:

比如我们想在这行汇编进行调试

img

对应这行伪代码:

img

这句伪代码的意思是在加密完以后将 byte 数组转成 iOS 中的 NSData 类型对象的方法,我们希望在这里断点来看下内存中的密文数据;在上面的代码中添加以下这两句代码:

        emulator.attach(DebuggerType.CONSOLE);
        emulator.attach().addBreakPoint(module.base + 0x10021A4DCL);

此时运行代码在执行到 0x10021A4DC 的时候会停下来

img

unidbg 里内置了很多调试命令,按回车会直接显示,此时我们使用 mx2 来查看第二个寄存器的值

img

第一行的 B7 FF... 就是我们加密完的密文数据,我们也可以把前面 base64 后的密文解码对比下这段数据:

img

由于目前对于 iOS 的模拟执行还不成熟,在使用过程中遇到了许多坑,希望后面能把坑填上再来分享进一步的使用心得。