MASTG-TECH-0084: 调试
从 Linux 背景出发,您可能会期望 ptrace
系统调用像您习惯的那样强大,但由于某些原因,Apple 决定使其不完整。诸如 LLDB 之类的 iOS 调试器使用它来附加、单步执行或继续该进程,但它们无法使用它来读取或写入内存(所有 PT_READ_*
和 PT_WRITE*
请求都丢失了)。相反,他们必须获得一个所谓的 Mach 任务端口(通过使用目标进程 ID 调用 task_for_pid
),然后使用 Mach IPC 接口 API 函数来执行诸如暂停目标进程以及读取/写入寄存器状态(thread_get_state
/thread_set_state
)和虚拟内存(mach_vm_read
/mach_vm_write
)之类的操作。
有关更多信息,您可以参考 GitHub 中的 LLVM 项目,其中包含 LLDB 的源代码,以及“Mac OS X 和 iOS Internals: To the Apple's Core” [#levin] 的第 5 章和第 13 章,以及“The Mac Hacker's Handbook” [#miller] 的第 4 章“Tracing and Debugging”。
使用 LLDB 进行调试¶
Xcode 安装的默认 debugserver 可执行文件不能用于附加到任意进程(通常仅用于调试使用 Xcode 部署的自行开发的应用)。要启用对第三方应用的调试,必须将 task_for_pid-allow
授权添加到 debugserver 可执行文件,以便调试器进程可以调用 task_for_pid
来获取目标 Mach 任务端口,如前所述。一种简单的方法是将授权添加到 Xcode 附带的 debugserver 二进制文件。
要获取可执行文件,请挂载以下 DMG 映像
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/<target-iOS-version>/DeveloperDiskImage.dmg
您将在挂载卷上的 /usr/bin/
目录中找到 debugserver 可执行文件。将其复制到临时目录,然后创建一个名为 entitlements.plist
的文件,其中包含以下内容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/ PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>run-unsigned-code</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
</dict>
</plist>
使用 代码签名 应用授权
codesign -s - --entitlements entitlements.plist -f debugserver
将修改后的二进制文件复制到测试设备上的任何目录。以下示例使用 usbmuxd 通过 USB 转发本地端口。
iproxy 2222 22
scp -P 2222 debugserver root@localhost:/tmp/
注意:在 iOS 12 及更高版本上,使用以下过程来签名从 XCode 映像获取的 debugserver 二进制文件。
1) 通过 scp 将 debugserver 二进制文件复制到设备,例如,在 /tmp 文件夹中。
2) 通过 SSH 连接到设备,并创建名为 entitlements.xml 的文件,其中包含以下内容
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>platform-application</key>
<true/>
<key>com.apple.private.security.no-container</key>
<true/>
<key>com.apple.private.skip-library-validation</key>
<true/>
<key>com.apple.backboardd.debugapplications</key>
<true/>
<key>com.apple.backboardd.launchapplications</key>
<true/>
<key>com.apple.diagnosticd.diagnostic</key>
<true/>
<key>com.apple.frontboard.debugapplications</key>
<true/>
<key>com.apple.frontboard.launchapplications</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>com.apple.system-task-ports</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>run-unsigned-code</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
</dict>
</plist>
3) 键入以下命令以使用 ldid 对 debugserver 二进制文件进行签名
ldid -Sentitlements.xml debugserver
4) 通过以下命令验证 debugserver 二进制文件是否可以执行
./debugserver
您现在可以将 debugserver 附加到设备上运行的任何进程。
VP-iPhone-18:/tmp root# ./debugserver *:1234 -a 2670
debugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-320.2.89
for armv7.
Attaching to process 2670...
使用以下命令,您可以通过在目标设备上运行的 debugserver 启动应用程序
debugserver -x backboard *:1234 /Applications/MobileSMS.app/MobileSMS
附加到已运行的应用程序
debugserver *:1234 -a "MobileSMS"
您现在可以从主机连接到 iOS 设备
(lldb) process connect connect://<ip-of-ios-device>:1234
键入 image list
会列出主可执行文件和所有依赖库的列表。
调试发布应用¶
在前一节中,我们了解了如何在 iOS 设备上使用 LLDB 设置调试环境。在本节中,我们将使用此信息并学习如何调试第三方发布应用程序。我们将继续使用 iOS UnCrackable L1 并使用调试器解决它。
与调试版本相比,为发布版本编译的代码经过优化,以实现最大性能和最小二进制文件大小。作为一般的最佳实践,大多数调试符号都已从发布版本中剥离,这在对二进制文件进行逆向工程和调试时增加了一层复杂性。
由于缺少调试符号,回溯输出中缺少符号名称,并且无法通过简单地使用函数名称来设置断点。幸运的是,调试器还支持直接在内存地址上设置断点。在本节的后面部分,我们将学习如何做到这一点,并最终解决 crackme 挑战。
在使用内存地址设置断点之前,需要进行一些基础工作。它需要确定两个偏移量
- 断点偏移量:我们要在其中设置断点的代码的地址偏移量。此地址是通过在像 Ghidra 这样的反汇编程序中执行代码的静态分析获得的。
- ASLR 偏移量:当前进程的ASLR 偏移量。由于 ASLR 偏移量是在应用程序的每个新实例上随机生成的,因此必须为每个调试会话单独获取它。这是使用调试器本身确定的。
iOS 是一种现代操作系统,实施了多种技术来缓解代码执行攻击,其中一种技术是地址空间随机化布局 (ASLR)。在每次执行应用程序时,都会生成一个随机 ASLR 偏移量,并且各种进程的数据结构都会按此偏移量进行移动。
要在调试器中使用的最终断点地址是以上两个地址的总和(断点偏移量 + ASLR 偏移量)。此方法假定反汇编程序和 iOS 使用的映像基地址(稍后讨论)相同,这在大多数情况下都是正确的。
当在像 Ghidra 这样的反汇编程序中打开二进制文件时,它通过模拟相应操作系统的加载器来加载二进制文件。加载二进制文件的地址称为映像基地址。此二进制文件中的所有代码和符号都可以使用距此映像基地址的常量地址偏移量进行寻址。在 Ghidra 中,可以通过确定 Mach-O 文件开头的地址来获得映像基地址。在这种情况下,它是 0x100000000。
隐藏字符串的值存储在设置了 hidden
标志的标签中。在反汇编中,此标签的文本值存储在寄存器 X21
中,通过 mov
从 X0
存储,偏移量为 0x100004520。这是我们的断点偏移量。
对于第二个地址,我们需要确定给定进程的ASLR 偏移量。可以使用 LLDB 命令 image list -o -f
确定 ASLR 偏移量。输出如下面的屏幕截图所示。
在输出中,第一列包含图像的序列号 ([X]),第二列包含随机生成的 ASLR 偏移量,而第三列包含图像的完整路径,在结尾处,括号中的内容显示了将 ASLR 偏移量添加到原始图像基地址之后的图像基地址 (0x100000000 + 0x70000 = 0x100070000)。您会注意到 0x100000000 的图像基地址与 Ghidra 中的相同。现在,要获得代码位置的有效内存地址,我们只需要将 ASLR 偏移量添加到 Ghidra 中标识的地址即可。设置断点的有效地址将为 0x100004520 + 0x70000 = 0x100074520。可以使用命令 b 0x100074520
设置断点。
在上面的输出中,您可能还会注意到,列为图像的许多路径并不指向 iOS 设备上的文件系统。相反,它们指向运行 LLDB 的主机上的某个位置。这些图像是系统库,主机上提供了调试符号,以帮助应用程序开发和调试(作为 Xcode iOS SDK 的一部分)。因此,您可以通过使用函数名称直接为这些库设置断点。
放入断点并运行应用程序后,一旦命中断点,执行将停止。现在您可以访问和探索进程的当前状态。在这种情况下,您从之前的静态分析中知道寄存器 X0
包含隐藏字符串,因此让我们探索它。在 LLDB 中,您可以使用 po
(print object) 命令打印 Objective-C 对象。
瞧,在静态分析和调试器的帮助下,crackme 可以轻松解决。LLDB 中实现了大量功能,包括更改寄存器的值,更改进程内存中的值,甚至可以使用 Python 脚本自动化任务。
Apple 官方建议使用 LLDB 进行调试,但 GDB 也可在 iOS 上使用。上面讨论的技术在使用 GDB 调试时也适用,前提是将 LLDB 特定命令更改为 GDB 命令。