iOS 应用分析常见对象是 IPA、Mach-O 可执行文件、Framework、动态库、资源文件和运行时 Objective-C / Swift 元数据。
这里把“砸壳”作为概念解释:App Store 分发的应用可执行文件通常带有 FairPlay 加密保护,静态分析前需要有合法授权的未加密样本,例如自有 App 的 Debug / Ad Hoc 包、企业内测包、开发者导出的符号文件,或经过明确授权的测试样本。
本文不提供绕过 App Store DRM、破解商业软件或分发解密应用的操作步骤。实务中优先使用自有 App、授权样本和调试构建进行分析。
iOS 分析对象
| 对象 | 说明 |
|---|---|
| IPA | 本质是 zip 包,包含 Payload/*.app |
| Mach-O | iOS 可执行文件、dylib、framework 的二进制格式 |
| Framework | 动态库和资源封装 |
| dSYM | 调试符号文件,能帮助符号化崩溃和函数名 |
| Info.plist | 应用配置、权限、URL Scheme |
| Entitlements | 签名权限,如 Keychain、Associated Domains |
| Assets.car | 图片资源包 |
解包 IPA:
1 | unzip app.ipa -d app_unzip |
找主程序:
1 | plutil -p app_unzip/Payload/*.app/Info.plist | grep CFBundleExecutable |
基础命令
查看文件格式:
1 | file Payload/App.app/App |
查看架构:
1 | lipo -info Payload/App.app/App |
查看 Mach-O 头:
1 | otool -hv Payload/App.app/App |
查看依赖库:
1 | otool -L Payload/App.app/App |
查看加载命令:
1 | otool -l Payload/App.app/App | less |
查看符号:
1 | nm -m Payload/App.app/App | head |
提取字符串:
1 | strings Payload/App.app/App | grep -i token |
判断是否加密
Mach-O 的 LC_ENCRYPTION_INFO / LC_ENCRYPTION_INFO_64 里有 cryptid 字段。
1 | otool -l Payload/App.app/App | grep -A5 LC_ENCRYPTION_INFO |
常见含义:
| 字段 | 说明 |
|---|---|
cryptoff |
加密区域偏移 |
cryptsize |
加密区域大小 |
cryptid 1 |
通常表示加密 |
cryptid 0 |
通常表示未加密 |
如果是自有 App,建议直接导出未加密的 Debug / Ad Hoc 分析包,而不是从 App Store 包开始。
IPA 结构检查脚本
1 | from pathlib import Path |
Info.plist 关注点
1 | plutil -p Payload/App.app/Info.plist |
常看字段:
| 字段 | 用途 |
|---|---|
CFBundleIdentifier |
包名 |
CFBundleExecutable |
主可执行文件 |
CFBundleURLTypes |
URL Scheme |
LSApplicationQueriesSchemes |
可查询的 Scheme |
NSAppTransportSecurity |
ATS 网络策略 |
UIBackgroundModes |
后台能力 |
NSCameraUsageDescription |
相机权限说明 |
NSLocationWhenInUseUsageDescription |
定位权限说明 |
提取 URL Scheme:
1 | import plistlib |
Entitlements
查看签名权限:
1 | codesign -d --entitlements :- Payload/App.app/App |
关注:
application-identifierkeychain-access-groupscom.apple.developer.associated-domainsaps-environment- App Groups
这些字段能帮助理解 Keychain、Universal Links、推送和共享容器。
Objective-C 类信息
如果二进制保留 Objective-C 元数据,可以用 strings 或工具查看类名、方法名。
简单搜索:
1 | strings Payload/App.app/App | grep -E "ViewController|Manager|Service" | head |
Mach-O 中常见段:
| 段/节 | 说明 |
|---|---|
__objc_classlist |
Objective-C 类列表 |
__objc_methname |
方法名 |
__objc_classname |
类名 |
__objc_const |
类元数据 |
使用 otool:
1 | otool -ov Payload/App.app/App | less |
如果有 class-dump 类工具,可以导出头文件用于阅读。对 Swift 应用,符号和类型信息会更复杂,常需要结合 demangle。
Swift 符号 demangle:
1 | swift-demangle '$s4Demo11UserServiceC5loginyyF' |
批量:
1 | nm Payload/App.app/App | awk '{print $3}' | swift-demangle | head |
Ghidra 分析 iOS Mach-O
导入主程序后:
- 确认架构是 ARM64。
- 自动分析。
- 从字符串、Objective-C 方法名、导入函数开始。
- 搜索 URL、接口路径、错误提示。
- 根据方法名和调用链重命名函数。
常见导入:
| 符号 | 说明 |
|---|---|
objc_msgSend |
Objective-C 方法派发 |
objc_retain / objc_release |
ARC 引用计数 |
CC_MD5 / CC_SHA256 |
CommonCrypto |
SecItemCopyMatching |
Keychain 读取 |
NSURLSession 相关 |
网络请求 |
Objective-C 伪代码里 objc_msgSend 很多,要结合 selector 看含义。
Frida 观察 Objective-C
列出类:
1 | if (ObjC.available) { |
Hook Objective-C 方法:
1 | if (ObjC.available) { |
Hook 类方法:
1 | const method = ObjC.classes.TokenManager['+ currentToken']; |
注意:
args[0]是self。args[1]是 selector。- 从
args[2]开始才是方法参数。
Frida 观察 NSURLSession
1 | if (ObjC.available) { |
如果 App 用第三方网络库,仍然可以从 NSURLRequest、NSURLSession 或底层 CFNetwork 观察。
Keychain 观察方向
Keychain 常见 API:
| API | 说明 |
|---|---|
SecItemAdd |
添加 |
SecItemCopyMatching |
查询 |
SecItemUpdate |
更新 |
SecItemDelete |
删除 |
Frida 观察调用:
1 | const secItemCopyMatching = Module.findExportByName('Security', 'SecItemCopyMatching'); |
CoreFoundation 对象打印比较复杂,实际分析时可以配合 Objective-C 层封装函数或 lldb。
自有 App 的推荐分析路线
如果你能拿到源码或构建权限,优先使用这种方式:
- Xcode 构建 Debug 包。
- 保留 dSYM。
- 导出 IPA 或直接拿
.app。 - 用 Ghidra / Hopper / IDA 做静态分析。
- 用 Frida / lldb 做运行时观察。
- 用 Wireshark / Charles 分析网络。
这样比分析 App Store 加密包更稳定,也更合规。
完整案例:分析自有 App 的登录接口与 Keychain 读取
场景:你有一个自有 iOS 测试 App,Bundle ID 是 com.demo.iosapp。目标是找出登录接口 URL、token 存储位置,以及运行时哪个方法读取 token。
1. 解包并确认主程序
1 | unzip Demo.ipa -d Demo_unzip |
假设主程序是:
1 | Demo_unzip/Payload/Demo.app/Demo |
记录:
1 | - Bundle ID:com.demo.iosapp |
2. 检查是否适合静态分析
1 | otool -l Demo_unzip/Payload/Demo.app/Demo | grep -A5 LC_ENCRYPTION_INFO |
如果看到:
1 | cryptid 0 |
说明这个样本可以继续静态分析。如果自有 App 出现 cryptid 1,优先重新导出 Debug / Ad Hoc 包。
3. 提取 URL 和关键字符串
1 | strings Demo_unzip/Payload/Demo.app/Demo | grep -E "https?://|/api/|token|login" | head -n 80 |
可能输出:
1 | https://api.test.local |
记录:
1 | - Base URL:`https://api.test.local` |
4. Frida 枚举类
1 | if (ObjC.available) { |
预期:
1 | LoginService |
5. Hook 登录方法
假设枚举方法得到:
1 | - loginWithUsername:password:completion: |
Hook:
1 | if (ObjC.available) { |
这里不打印明文密码,只打印长度,避免日志泄露敏感信息。
6. Hook Keychain 封装类
1 | if (ObjC.available) { |
观察底层 Keychain API:
1 | const fn = Module.findExportByName('Security', 'SecItemCopyMatching'); |
7. Hook NSURLSession 请求
1 | if (ObjC.available) { |
预期输出:
1 | [request] POST https://api.test.local/api/v1/login |
8. 复盘记录
1 | ## 登录与 token 链路 |
完整案例:确认 URL Scheme 暴露面
目标:检查自有 App 的 URL Scheme 是否可能被外部 App 调起敏感页面。
1. 从 Info.plist 提取 Scheme
1 | import plistlib |
输出:
1 | demoapp |
2. 搜索路由处理方法
1 | strings Demo_unzip/Payload/Demo.app/Demo | grep -i "openURL\\|handleURL\\|route" |
可能出现:
1 | application:openURL:options: |
3. Frida Hook 路由入口
1 | if (ObjC.available) { |
4. 测试调起
在授权测试模拟器上:
1 | xcrun simctl openurl booted "demoapp://profile?id=10001" |
记录:
1 | - Scheme:demoapp |
常见问题
IPA 解压后找不到可执行文件
先看 Info.plist:
1 | plutil -p Payload/*.app/Info.plist | grep CFBundleExecutable |
可执行文件通常在:
1 | Payload/AppName.app/AppName |
otool 看到 cryptid 1
说明二进制通常处于加密状态,不适合直接静态分析。对于自有 App,请导出未加密调试包或使用授权样本。
Swift 符号很长
用 swift-demangle:
1 | swift-demangle '符号名' |
Frida 找不到 Objective-C 类
可能原因:
- 类名是 Swift mangled 名。
- 类还没加载。
- App 使用动态 Framework。
- 目标是纯 C/C++ 函数。
可以先枚举:
1 | for (const name in ObjC.classes) { |
Hook 方法后崩溃
常见原因:
- 参数不是 Objective-C 对象,却用
new ObjC.Object()包装。 - 方法签名看错。
- 返回值为空。
- Hook 过早,类未初始化。
先只打印 selector 和指针:
1 | console.log(args[0], ObjC.selectorAsString(args[1]), args[2]); |
确认类型后再包装对象。
分析记录模板
1 | ## 样本 |
案例三:定位 iOS App 登录请求参数来源
场景
自有测试 App 登录接口请求:
1 | POST /v1/login |
目标:找出 X-Sign 在哪个 Objective-C 方法里生成,输入是什么。
1. 静态查字符串
解包自有调试 IPA 后:
1 | strings Payload/Demo.app/Demo | rg 'X-Sign|Device|login|/v1/login' |
可能得到:
1 | X-Sign |
2. 查 Objective-C 方法名
1 | otool -ov Payload/Demo.app/Demo | rg -A 4 'buildLoginSign|Login|Network' |
看到:
1 | name 0x... -[DMNetworkSigner buildLoginSignWithPath:body:] |
3. Frida Hook 目标方法
1 | if (ObjC.available) { |
运行:
1 | frida -U -f com.demo.ios -l ios-sign.js --no-pause |
触发登录,输出:
1 | [buildLoginSign path] /v1/login |
4. Hook 请求发送确认 Header
1 | if (ObjC.available) { |
5. 复盘
1 | - URL:`/v1/login` |
案例四:检查 Keychain 是否保存明文 token
场景
App 卸载重装后仍能识别设备,怀疑 Keychain 保存了设备 ID 或 token。目标是找 Keychain 读写点。
1. Hook SecItemAdd
1 | const SecItemAdd = Module.findExportByName('Security', 'SecItemAdd'); |
2. Hook SecItemCopyMatching
1 | const SecItemCopyMatching = Module.findExportByName('Security', 'SecItemCopyMatching'); |
3. 触发动作
- 首次启动。
- 登录。
- 杀进程重启。
- 退出登录。
- 卸载重装后再启动。
可能看到:
1 | [SecItemAdd] { |
4. 结论
1 | - Keychain service:`com.demo.app` |
案例五:检查 URL Scheme 是否暴露敏感动作
场景
自有 App 支持:
1 | demoapp://open?path=... |
要确认外部 App 或 Safari 能否通过 scheme 打开敏感页面。
1. 静态查 scheme
1 | plutil -p Payload/Demo.app/Info.plist | rg -A 20 CFBundleURLTypes |
输出:
1 | "CFBundleURLSchemes" => [ |
2. 模拟器打开
1 | xcrun simctl openurl booted 'demoapp://open?path=profile' |
再试敏感路径:
1 | xcrun simctl openurl booted 'demoapp://open?path=pay' |
3. Hook URL 处理入口
旧式 AppDelegate:
1 | if (ObjC.available) { |
SceneDelegate:
1 | if (ObjC.available && ObjC.classes.SceneDelegate) { |
4. 观察路由函数
如果日志里只有 URL,还要继续找业务路由类:
1 | for (const name in ObjC.classes) { |
Hook 例如:
1 | const Router = ObjC.classes.DMDeepLinkRouter; |
5. 复盘
1 | - Scheme:`demoapp://` |