跳过内容

MASTG-TECH-0088: 基于仿真的分析

iOS 模拟器

Apple 在 Xcode 中提供了一个模拟器应用程序,它为 iPhone、iPad 或 Apple Watch 提供了一个看起来像真实的 iOS 设备的用户界面。它允许您在开发过程中快速创建原型并测试应用程序的调试版本,但实际上它不是一个模拟器。模拟器和仿真器的区别在“基于仿真的动态分析”部分中已进行了讨论。

在开发和调试应用程序时,Xcode 工具链会生成 x86 代码,可以在 iOS 模拟器中执行。但是,对于发布版本,只会生成 ARM 代码(与 iOS 模拟器不兼容)。这就是为什么从 Apple App Store 下载的应用程序不能用于 iOS 模拟器上的任何类型的应用程序分析。

Corellium

Corellium 是一种商业工具,它提供运行实际 iOS 固件的虚拟 iOS 设备,是有史以来唯一公开可用的 iOS 仿真器。由于它是一种专有产品,因此没有太多关于实现的信息。 Corellium 没有可用的社区许可证,因此我们不会详细介绍其使用。

Corellium 允许您启动设备的多个实例(越狱与否),这些实例可以作为本地设备访问(通过简单的 VPN 配置)。它能够获取和恢复设备状态的快照,还提供了一个方便的基于 Web 的设备 Shell。最后也是最重要的一点,由于其“仿真器”性质,您可以执行从 Apple App Store 下载的应用程序,从而可以像从真正的 iOS(越狱)设备中了解的那样进行任何类型的应用程序分析。

请注意,为了在 Corellium 设备上安装 IPA,必须使用有效的 Apple 开发者证书对其进行解密和签名。有关更多信息,请参见此处

Unicorn

Unicorn 是一个轻量级的、多架构的 CPU 仿真框架,基于 QEMU,并通过添加专为 CPU 仿真设计的有用功能超越了它。 Unicorn 提供了执行处理器指令所需的基础架构。在本节中,我们将使用Unicorn 的 Python 绑定来解决 iOS UnCrackable L1 挑战。

为了使用 Unicorn 的全部功能,我们需要实现所有必要的基础架构,这些基础架构通常可以从操作系统中轻松获得,例如,二进制加载器、链接器和其他依赖项,或者使用另一个更高级别的框架,例如 Qiling,它利用 Unicorn 来模拟 CPU 指令,但了解 OS 上下文。但是,对于这种非常本地化的挑战,仅执行二进制文件的一小部分就足够了,这显得多余。

查看反汇编的本机代码中执行手动分析时,我们确定地址 0x1000080d4 处的函数负责动态生成密钥字符串。正如我们将要看到的,所有必要的代码几乎都包含在二进制文件中,这使其成为使用像 Unicorn 这样的 CPU 仿真器的完美方案。

如果我们分析该函数和随后的函数调用,我们将观察到对任何外部库都没有硬性依赖,也没有执行任何系统调用。仅在函数外部的访问发生在地址 0x1000080f4,其中一个值被存储到地址 0x10000dbf0,该地址映射到 __data 部分。

因此,为了正确地仿真代码的这一部分,除了包含指令的 __text 部分之外,我们还需要加载 __data 部分。

为了使用 Unicorn 解决挑战,我们将执行以下步骤

  • 通过运行 lipo -thin arm64 <app_binary> -output uncrackable.arm64 获取二进制文件的 ARM64 版本(也可以使用 ARMv7)。
  • 从二进制文件中提取 __text__data 部分。
  • 创建并映射要用作堆栈内存的内存。
  • 创建内存并加载 __text__data 部分。
  • 通过提供起始地址和结束地址来执行二进制文件。
  • 最后,转储函数的返回值,在本例中是我们的密钥字符串。

为了从 Mach-O 二进制文件中提取 __text__data 部分的内容,我们将使用 LIEF,它提供了一个方便的抽象来操作多个可执行文件格式。在将这些部分加载到内存之前,我们需要确定它们的基本地址,例如,通过使用 Ghidra、Radare2 或 IDA Pro。

从上表中,我们将使用基本地址 0x10000432c 作为 __text,0x10000d3e8 作为 __data 部分,以便在内存中加载它们。

在为 Unicorn 分配内存时,内存地址应该是 4k 页面对齐的,并且分配的大小应该是 1024 的倍数。

以下脚本模拟了 0x1000080d4 处的函数并转储了密钥字符串

import lief
from unicorn import *
from unicorn.arm64_const import *

# --- Extract __text and __data section content from the binary ---
binary = lief.parse("uncrackable.arm64")
text_section = binary.get_section("__text")
text_content = text_section.content

data_section = binary.get_section("__data")
data_content = data_section.content

# --- Setup Unicorn for ARM64 execution ---
arch = "arm64le"
emu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

# --- Create Stack memory ---
addr = 0x40000000
size = 1024*1024
emu.mem_map(addr, size)
emu.reg_write(UC_ARM64_REG_SP, addr + size - 1)

# --- Load text section --
base_addr = 0x100000000
tmp_len = 1024*1024
text_section_load_addr = 0x10000432c
emu.mem_map(base_addr, tmp_len)
emu.mem_write(text_section_load_addr, bytes(text_content))

# --- Load data section ---
data_section_load_addr = 0x10000d3e8
emu.mem_write(data_section_load_addr, bytes(data_content))

# --- Hack for stack_chk_guard ---
# without this will throw invalid memory read at 0x0
emu.mem_map(0x0, 1024)
emu.mem_write(0x0, b"00")


# --- Execute from 0x1000080d4 to 0x100008154 ---
emu.emu_start(0x1000080d4, 0x100008154)
ret_value = emu.reg_read(UC_ARM64_REG_X0)

# --- Dump return value ---
print(emu.mem_read(ret_value, 11))

您可能会注意到,地址 0x0 处还有一个额外的内存分配,这是围绕 stack_chk_guard 检查的一个简单技巧。如果没有这个,将会出现无效的内存读取错误,并且无法执行二进制文件。有了这个技巧,程序将访问 0x0 处的值并将其用于 stack_chk_guard 检查。

总而言之,使用 Unicorn 在执行二进制文件之前确实需要一些额外的设置,但一旦完成,此工具可以帮助深入了解二进制文件。它提供了执行完整二进制文件或其有限部分的灵活性。 Unicorn 还公开了 API 以将挂钩附加到执行。使用这些挂钩,您可以在执行期间的任何时刻观察程序的状态,甚至可以操作寄存器或变量值,并强制探索程序中的其他执行分支。在 Unicorn 中运行二进制文件的另一个优点是,您无需担心各种检查,例如 root/越狱检测或调试器检测等。