macOS 应用逆向:用 REA 分析 Mach-O、Swift 符号和 NIB 界面
本页目录
一个 macOS 应用其实是一棵目录树:一个或多个 Mach-O 可执行文件、嵌套的 framework 和 helper、各种 plist、编译后的 Interface Builder 文件,还有资源目录(asset catalog)。打开反汇编器之前,你已经能弄清楚很多事:应用是怎么组装起来的、链接了哪些库、声明了哪些 entitlement、界面怎么连到代码上。REA(morluto 维护的开源项目)把这些层逐一读成 Evidence,再把具体的函数交给 Ghidra 或 Hopper 深入分析。
适合谁读: 排查自家构建的 macOS 开发者、想了解开源 Mac 应用结构的人,以及需要一套可复现静态流程的安全学习者。需要熟悉终端,见过 .app 包的内部结构。
你会学到:
- 清点一个
.app或.dmg,并投影出它的 bundle 结构 - 读取
Info.plist、Mach-O 头、架构和代码签名元数据 - 追踪 dyld 如何解析每个
@rpath加载 - 还原(demangle)Swift 符号,读取 Objective-C 和 Swift 的派发元数据
- 解码编译后的 NIB 和 storyboard、keyed archive 和资源目录
- 在 Ghidra 或 Hopper 里从一个界面动作找到它的处理函数
遵守许可协议。 只分析开源应用、你自己编译的应用,或许可协议允许分析的应用。很多商业软件的 EULA 限制逆向工程,动手前先读 EULA,并在它允许的范围内操作。本文只读取元数据和代码,不涉及修改、重签名、打补丁、去除保护或绕过授权,所有步骤都是只读分析。涉及运行时的步骤会以你的用户权限启动目标,只在你有权使用的应用和机器上做。参见 REA 的 DISCLAIMER.md。
示例目标: Rectangle,一个用 Swift 编写、MIT 许可的窗口管理工具,从它的 GitHub 页面下载发布版即可。换成任何开源 Mac 应用或你自己用 Xcode 编译的项目,流程都一样。
哪些工具在哪里能跑
REA 在 macOS 上有一部分工具调用 Apple 的命令行工具,另一部分用 TypeScript 自己解析文件,任何主机都能跑。
| 工具 | 依赖 | 能否在非 macOS 上运行 |
|---|---|---|
inspect-artifact、extract-artifact、project-apple-application-graph |
无额外依赖(DMG 内部清单需要 macOS) | 可以,DMG 内容除外 |
trace-dylib-resolution |
无额外依赖,纯头部解析 | 可以 |
decode-interface-builder、inspect-keyed-archive |
无额外依赖,静态解析归档 | 可以 |
inspect-macho、list-architectures、inspect-signature、inspect-plist、demangle-swift |
通过 xcrun 查找并调用 Xcode 命令行工具(lipo、otool、nm、dyld_info、vtool、codesign、plutil、swift-demangle) |
不行,报 unsupported_host |
inspect-native-dispatch-metadata |
需要 macOS 上的 Mach-O 字节读取器(不调用外部工具) | 只能通过 Ghidra 等引擎拿到基于符号名的元数据 |
inspect-asset-catalog |
macOS 的 assetutil |
不行 |
decompile、search、trace-native-ui-action 等函数级分析 |
Ghidra 或 Hopper(或 IDA) | 取决于所用引擎 |
前置条件
- REA 6.4.0 或更新版本。
npm install --global rea-agents,或在命令前加npx -y rea-agents@latest。在 Claude Code / Cursor / Codex 里使用,运行npx rea-agents setup(见 安装与接入)。 - macOS 上的 Xcode 命令行工具:
xcode-select --install。 - Ghidra 或 Hopper,第 9、10 步要用。setup 能检测已有的 Ghidra,也可以在你同意后安装 Hopper(独立的商业软件,有免费演示模式)。用
rea doctor --provider ghidra --json(或hopper)检查某个引擎是否就绪。 jq。这里有几个结果会很大。
先把应用复制到工作目录,比如 ~/re-work/Rectangle.app,这样 /Applications 里的那份更新了也不会影响你的分析。下面所有路径都要用绝对路径。
第 1 步:清点 bundle
rea inspect-artifact ~/re-work/Rectangle.app --json > inspection.json
为什么先做这一步: inspect-artifact 返回按内容寻址的完整清单:每个路径、大小、SHA-256、识别出的格式,外加完整性观测和建议的下一步探查。也可以直接传下载来的 .dmg。在 macOS 上,REA 会以只读方式挂载它、清点卷内容,完成后清理挂载。需要把文件落到磁盘上时,用 rea extract-artifact,REA 会自己选一个新的临时目录并告诉你位置。
第 2 步:投影出 bundle 结构
清单是平铺的列表,project-apple-application-graph 会把它整理成 Apple 应用特有的结构。把 inspect-artifact 结果里嵌套的清单 Evidence 传给它:
jq '{inventory_evidence: [.normalized_result.substeps[].evidence]}' \
inspection.json > apple-input.json
rea project-apple-application-graph ./apple-input.json --json > apple-graph.json
jq '.normalized_result.bundles[] | {path, role, layout, info_plist_path, executable_candidates}' apple-graph.json
jq '.normalized_result.runtime_families' apple-graph.json
你会得到:
- bundle 及其角色,按路径约定判断:
application、app-extension、xpc-service、framework、login-item、helper-application、plug-in、resource-bundle等,每个都带有info_plist_path和可执行文件候选。 - 组件:
Contents/Library/LaunchServices/下的特权 helper、LaunchAgents/和LaunchDaemons/下的 launchd plist、签名文件和符号链接。 - 运行时类型: 根据完整的 framework 路径段给出
react-native、flutter、unity等提示。如果看到Electron Framework.framework和app.asar,真正值得看的代码是 JavaScript,请转到 Electron 那篇。
这些都是基于路径的推断:投影本身不读 plist,也不能证明运行时真的加载了什么。后面几步就是用来核实的。
第 3 步:读取 Info.plist 和其他 plist
rea inspect-plist ~/re-work/Rectangle.app --json
rea inspect-plist ~/re-work/Rectangle.app \
--relative-path Contents/Library/LaunchAgents/EXAMPLE.plist --json
不加 --relative-path 时,REA 读取 bundle 的 Info.plist,给出标识符、可执行文件名、名称和版本,同时返回完整的解码内容:URL scheme、各种权限用途说明、LSUIElement 等。要读 launchd 或 helper 的 plist,就用第 2 步列出的路径(第二条命令里是占位符)。底层调用的是 plutil,所以需要 macOS。
第 4 步:Mach-O 头、架构切片和依赖
rea list-architectures ~/re-work/Rectangle.app --json
rea inspect-macho ~/re-work/Rectangle.app --json > macho.json
传 .app 路径时会自动定位到主可执行文件。也可以直接传 Contents/MacOS/<name>,或第 2 步列出的某个 framework 二进制。
inspect-macho 返回文件类型、标志位、UUID、入口点、架构、构建元数据(平台和 SDK)、load command、依赖、导入、导出和段,每个列表都带 items、total 和 exhaustive 标志。一定要存成文件再切片看。 真实应用的结果可能非常大,issue #2473(目前仍未关闭)实测过:在普通 macOS 应用上,一次 inspect_macho 调用大约有 8 万到 23 万个估算 token,原因是所有导入和导出都内联返回。这远超常见 MCP 客户端的结果上限,不要直接塞进 agent 的上下文。比如:
jq '.normalized_result.dependencies.items' macho.json
jq '.normalized_result.build_metadata' macho.json
jq '.normalized_result.imports.total, .normalized_result.exports.total' macho.json
对于通用二进制(universal binary),architectures.items 会列出每个切片及其在容器文件里的偏移和大小。6.4.0 的结果里没有字段标明 REA 实际读的是哪个切片;仓库文档里提到的 selected_architecture 字段只存在于 main 分支,npm 6.4.0 发布包里没有。
第 5 步:代码签名元数据
rea inspect-signature ~/re-work/Rectangle.app --json
这一步用 codesign 读取现有签名:标识符、团队 ID、CDHash、签发链、指定需求(designated requirement)、时间戳、是否启用 hardened runtime,以及声明的 entitlement。entitlement 往往是了解“这个应用被允许做什么”最快的途径:沙盒、网络、Apple Events、摄像头等等。REA 只报告签名信息,从不移除或替换签名。
第 6 步:追踪 dylib 解析
rea trace-dylib-resolution ~/re-work/Rectangle.app --json > dylibs.json
rea trace-dylib-resolution ~/re-work/Rectangle.app \
--root Contents/MacOS/Rectangle --architecture arm64 --json
REA 对 bundle 里的每个可执行文件按 dyld 的规则模拟解析:按“依赖优先”的顺序,沿 LC_RPATH 栈展开 @executable_path、@loader_path 和每一个 @rpath。每个候选路径都有结果,每次加载被判定为 resolved、conditional(bundle 外更早的候选,比如 /usr/lib/swift,如果存在就会优先)、unresolved 或 undetermined。派生发现会标出无法解析的必需加载,以及缺失的更早 @rpath 候选。系统库保持 outside-target,因为 macOS 11 起它们在 dyld shared cache 里。整个过程不执行任何东西,在 Linux 上也能跑。上面的 root 路径假设可执行文件名是 Rectangle,实际名称以第 3 步读出的为准。
第 7 步:Swift 符号和派发元数据
Swift 符号是被 mangle 过的。把想看懂的符号传进去,比如第 4 步 imports 或 exports 里的:
rea demangle-swift ~/re-work/Rectangle.app '$s4main3FooV' --json
第一个参数把 Evidence 绑定到这个应用,后面每个参数是一个符号。REA 自己的测试用例正是用这个符号,期望 $s4main3FooV 还原为 main.Foo。不是 mangle 形式的符号会原样返回,状态为 unchanged。
想看名字背后的结构:
rea inspect-native-dispatch-metadata ~/re-work/Rectangle.app --json > dispatch.json
在 macOS 上它用一个经过校验的 Mach-O 字节读取器(在其他主机上,macOS 原生 provider 不可用,Ghidra 等引擎只能给出基于符号名的元数据),解码 Objective-C 的类、元类、父类、方法、实例变量、属性、category 和 protocol,以及简单的 Swift 协议一致性(conformance)、witness 槽位和非泛型、非 resilient 的类 vtable。每条记录都带精确的地址和文件偏移。泛型、resilient 和 async 的表会标为 unsupported 或 partial,不会去猜。
第 8 步:NIB、storyboard、归档和资源
rea decode-interface-builder ~/re-work/Rectangle.app --json > ib.json
它静态解析编译后的 .nib 文件和 storyboard 场景,生成一张对象、层级和连接图,包括 outlet、action、控制器名和类名。documents 列出每个归档及其对象数和连接数。识别不了的类、找不到的处理函数都会明确标为未知。默认上限是 64 个文档、20000 个对象、40000 条连接(--max-documents、--max-objects、--max-connections)。
其他和界面相关的资源:
rea inspect-keyed-archive ~/re-work/Rectangle.app Contents/Resources/SOME_ARCHIVE.plist --json
rea inspect-asset-catalog ~/re-work/Rectangle.app --limit 200 --json
inspect-keyed-archive 按原始对象表读取任意 NSKeyedArchiver plist,不会实例化任何类,共享引用和循环引用都会保留。用 --root 选根对象,用 --offset、--limit 分页。上面的归档路径是占位符,请换成清单里真实存在的文件。inspect-asset-catalog 通过 assetutil 读取 Assets.car 的元数据,不导出图片字节。
第 9 步:用 Ghidra 或 Hopper 反编译
现在用前面得到的信息挑函数:
rea search ~/re-work/Rectangle.app "applicationDidFinishLaunching" --provider ghidra --json
rea decompile ~/re-work/Rectangle.app \
'--procedure=-[AppDelegate applicationDidFinishLaunching:]' --provider ghidra --json
Objective-C 方法名以 - 或 + 开头,会被当成命令行参数,所以要用 --procedure= 的写法。上面的 selector 只是示意,请从搜索结果或派发元数据里复制精确名称(包括命名空间)。Swift 方法以 mangle 后的名字或引擎还原后的名字出现。通过 MCP 时,agent 可以对已打开的二进制调用 get_objc_classes 和 analyze_swift_types,基于引擎的名称表得到类和类型清单。这两个工具在 6.4.0 里只能通过 MCP 调用:发布包把它们列在 MCP_TOOLS_WITHOUT_DEDICATED_CLI 里,没有 rea get-objc-classes 这样的命令。完整的 Ghidra 流程见 /zh/rea-ghidra-native-binary/。
第 10 步:从按钮找到处理函数
trace-native-ui-action 把第 8 步和第 9 步接了起来。给它一个来自 NIB 图的 action selector 或对象 ID,它会顺着界面里配置的连接找到 Objective-C 处理函数,再沿静态调用往下走:
rea trace-native-ui-action ~/re-work/Rectangle.app 'ACTION_SELECTOR:' \
--provider ghidra --max-depth 4 --json
selector 必须唯一。有类型的直接调用、已解析的间接调用、有歧义的候选和没有目标的调用点会分开列出,结果也不会声称这条路径一定会执行。要确认某次运行里实际发生了什么,可以用 observe-native-calls 在 LLDB 下按显式断点启动应用。这属于运行时操作:应用会真正运行,可能修改文件或访问网络,需要事先取得授权。启用了 hardened runtime 的目标需要 get-task-allow entitlement,所以实际上只适合用在你自己编译的应用上。详见 native investigation。
让 agent 来驱动
用 REA 分析 ~/re-work/Rectangle.app:先清点,投影 Apple 应用图,
读取 Info.plist 和签名里的 entitlement,再追踪主可执行文件的 dylib 解析。
总结 bundle 结构和所有无法解析的加载。不要粘贴 inspect_macho 的原始结果,
只挑需要的字段。每个结论附 Evidence ID,并列出 unknown。
关于 inspect_macho 的那句提醒很重要:不加的话,agent 可能会尝试读完整份记录,把上下文撑爆(issue #2473)。
常见问题与排错
inspect-macho、inspect-plist、demangle-swift 报 unsupported_host。 这些工具需要 macOS。在 Linux 上改用 trace-dylib-resolution、decode-interface-builder、inspect-keyed-archive 和 Ghidra。
提示 “is unavailable through xcrun”。 用 xcode-select --install 安装或修复命令行工具,然后运行 rea doctor --json。
Active artifact is not Mach-O。 你传入的是脚本、plist 或其他非 Mach-O 资源。请传 .app,或 executable_candidates 里的文件。
DMG 只有根哈希。 读取 DMG 内部内容需要 macOS 上的只读挂载。在其他主机上,先在 Mac 上把应用取出来,或者直接分析 .app。
Interface Builder 结果是 partial。 所有归档加起来超出了 32 MiB 的总输入预算或 256 MiB 的总解码预算。前面已经解码的文档仍然可用,被跳过的路径会列出来。
Hopper 弹出了窗口。 在 macOS 上 REA 驱动的是 Hopper 应用本身,首次运行可能弹出对话框。需要完全无界面的分析时,请用 Ghidra。
下一步
- 用 Ghidra 深入函数:/zh/rea-ghidra-native-binary/
- 发现是 Electron 应用:/zh/reverse-engineer-electron-app/
- 对比同一应用的两个版本:/zh/rea-evidence-snapshots-compare/
- 在 REA、GhidraMCP 和 IDA Pro MCP 之间做选择:/zh/rea-vs-ghidramcp-ida-pro-mcp/
- 想用 IDA Pro 代替 Ghidra 或 Hopper 做引擎:IDA Pro MCP 配置:用 REA 接入 IDA
- 参考文档:Apple application analysis、native investigation