[macOS] iCollections 9.7.7 逆向分析
> **分析工具:IDA Pro**> **辅助工具:otool、lipo、codesign、plutil、xxd**
> **目标版本:iCollections 9.7.7(Build 97708)**
> **目标架构:x86_64 + arm64**
## 前言
这篇记录的是 iCollections 9.7.7 在 macOS 下的一次完整静态分析过程,主要目的是梳理它的授权判断、激活入口、启动向导和延迟弹窗之间的关系。
这个样本看起来不复杂,但实际上有两个容易绕进去的地方:
1. App 是 Universal Mach-O,x86_64 和 arm64 两套代码都要处理。
2. 启动时看到的窗口不只有 `Wizard.nib`,后面还有一个延迟调用的 `Register.nib`。
我一开始也走了弯路,直接处理 `EnterLicenseDialog` 函数本体后,窗口确实没有了,但程序启动几秒后会自己退出。后面顺着调用链重新梳理,才确定正确的做法是保留函数本体,只跳过对它的调用点。
本文仅记录逆向分析和 Mach-O 调试方法,用于技术交流与研究。
---
## 一、样本信息
| 项目 | 内容 |
|---|---|
| App | iCollections |
| 版本 | 9.7.7 |
| Build | 97708 |
| Bundle ID | `com.naarak.Collections` |
| 主程序 | `iCollections.app/Contents/MacOS/iCollections` |
| Mach-O | Universal/Fat |
| 架构 | `x86_64` + `arm64` |
为了避免后面的命令里全是长路径,先统一定义几个变量:
```bash
ROOT="$HOME/Desktop/iCollections"
APP="$ROOT/iCollections.app"
BIN="$APP/Contents/MacOS/iCollections"
```
先看基本信息:
```bash
file "$BIN"
lipo -detailed_info "$BIN"
plutil -p "$APP/Contents/Info.plist" | head -n 40
codesign -dvvv "$APP" 2>&1
```
样本是两个 slice 合在一起的 Fat Mach-O,本版的 slice 偏移是:
```text
x86_64 slice offset = 0x4000
arm64slice offset = 0x28C000
```
这两个数要记下来。IDA 里看到的是虚拟地址(VA),真正落到 Fat 文件上时,还要加上对应架构的 slice offset。
动手前先备份,同时记录原始文件哈希:
```bash
mkdir -p "$ROOT/backups"
cp "$BIN" "$ROOT/backups/iCollections.original.unmodified"
shasum -a 256 "$BIN" | tee "$ROOT/backups/iCollections.original.sha256"
```
这一步看起来很普通,但后面试错时非常有用。特别是对函数本体下手后出现闪退,没有干净备份的话,很难判断到底是哪一处改坏了。
---
## 二、IDA 加载双架构
Fat Mach-O 在 IDA 里不要只建一个数据库。我这里分别加载了两次:
```text
iCollections_x86_64.i64
iCollections_arm64.i64
```
打开主程序后,IDA 的 Mach-O loader 会让我们选择架构。第一次选 `x86_64`,第二次选 `arm64`,分别等待自动分析完成。
这个 App 的 Objective-C 元数据保留得比较完整,所以在 IDA 中可以直接看到不少类名和方法名。
可以先搜这些关键词,快速建立整体印象:
```text
NRegister
IsRegistered
LicenseType
DecodeKey
NWizard
Activate:
OpenWizard:
InitWithDelegate:
EnterLicenseDialog
Register.nib
Wizard.nib
```
我的习惯是先从方法名入手,再跟调用引用,不一上来就搜弹窗文字。弹窗文字只能告诉我们“这里会显示什么”,方法和 Xref 才能说明“它为什么会走到这里”。
---
## 三、地址和文件偏移怎么换算
这个样本两个 slice 的 `__TEXT` 基址都是:
```text
0x100000000
```
因此文件偏移的换算公式是:
```text
file offset = slice offset + (VA - 0x100000000)
```
以 x86_64 的 `0x1000F8421` 为例:
```text
slice offset = 0x4000
VA = 0x1000F8421
file offset = 0x4000 + (0x1000F8421 - 0x100000000)
= 0xFC421
```
这里有个很容易犯的错:把 IDA 里的 VA 当成整个 Fat 文件的偏移。这样写进去的位置肯定不对,严重时连另一个 slice 或 load command 都可能被破坏。
每改一组字节,都可以在终端再校验一次:
```bash
xxd -g 1 -s 0xFC421 -l 32 "$BIN"
```
---
## 四、授权状态的入口
先看 `NRegister` 类。在 IDA 的 Functions 或 Names 窗口搜索:
```objc
+
+
+
+
```
`IsRegistered` 返回当前是否处于已注册状态,`LicenseType` 返回授权类型。从后续的使用点看,类型值 `7` 可以走完整功能路径。这里只按样本的实际分支记录,不对这个枚举值强行起名。
### 4.1 x86_64:`IsRegistered`
函数从 `0x1000F841D` 开始,前四个字节是常规栈帧:
```asm
push rbp
mov rbp, rsp
```
原始逻辑会读取一个全局状态,再经过 `test` / `cmp` / `setne` 组合得到布尔值。这里保留栈帧序言,从 `0x1000F8421` 开始改为固定返回 `1`:
```asm
mov eax, 1
pop rbp
ret
```
Patch 信息:
```text
VA : 0x1000F8421
fileoff : 0xFC421
bytes : B8 01 00 00 00 5D C3 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
```
之所以有一串 NOP,是为了覆盖后面的旧逻辑,避免旧指令残留成为误导。
### 4.2 x86_64:`LicenseType`
方法从 `0x1000F843C` 开始,同样保留栈帧序言,从 `0x1000F8440` 处改:
```asm
mov eax, 7
pop rbp
ret
```
```text
VA : 0x1000F8440
fileoff : 0xFC440
bytes : B8 07 00 00 00 5D C3 90
```
### 4.3 arm64:`IsRegistered` 和 `LicenseType`
arm64 没有 x86 那种可变长指令,每条指令固定 4 字节,处理起来反而更直观。
`IsRegistered` 改为:
```asm
mov w0, #1
ret
```
```text
VA : 0x1000ADCA8
fileoff : 0x339CA8
bytes : 20 00 80 52 C0 03 5F D6 1F 20 03 D5 1F 20 03 D5 1F 20 03 D5
```
`LicenseType` 改为:
```asm
mov w0, #7
ret
```
```text
VA : 0x1000ADCC0
fileoff : 0x339CC0
bytes : E0 00 80 52 C0 03 5F D6 1F 20 03 D5
```
到这里只是让程序内部的状态查询回答“已注册”。如果从启动阶段进入,向导和延迟许可证窗口仍然有自己的调用路径,所以还要继续往下跟。
---
## 五、跟进 `-`
在 IDA 里定位:
```objc
-
```
这个方法包含了激活按钮的主要处理逻辑。结合伪代码和汇编分支,可以看到几个明显的失败出口:
```text
输入为空 -> return
长度大于 40 -> fail
trim 后为空 -> fail
DecodeKey 返回 0/8 -> invalid alert
DecodeKey 返回 2 -> return
```
这里我没有整段删掉 `DecodeKey`,而是只处理它后面的失败分支。原因很简单:后续成功流程还会使用前面准备好的对象和状态,如果粗暴地把整个解码过程拿掉,很容易在后面留下空对象或未初始化状态。
### 5.1 x86_64 分支
x86_64 中这几处都是 6 字节的近跳条件分支,使用等长 6 字节 NOP 覆盖:
```text
66 0F 1F 44 00 00
```
| 作用 | VA | file offset | 原始字节 | Patch 后 |
|---|---:|---:|---|---|
| 空输入 return | `0x10001EDCC` | `0x22DCC` | `0F 84 97 03 00 00` | `66 0F 1F 44 00 00` |
| 长度大于 40 | `0x10001EE10` | `0x22E10` | `0F 87 BC 00 00 00` | `66 0F 1F 44 00 00` |
| trim 后为空 | `0x10001EE25` | `0x22E25` | `0F 84 A7 00 00 00` | `66 0F 1F 44 00 00` |
| `DecodeKey` invalid | `0x10001EE49` | `0x22E49` | `0F 84 A1 01 00 00` | `66 0F 1F 44 00 00` |
| `DecodeKey` ret 2 | `0x10001EE59` | `0x22E59` | `0F 84 0A 03 00 00` | `66 0F 1F 44 00 00` |
改完后条件跳转不再发生,流程会继续向下走成功分支。
### 5.2 arm64 分支
arm64 每条分支指令占 4 字节,用标准 NOP 覆盖:
```text
1F 20 03 D5
```
| 作用 | VA | file offset | Patch 后 |
|---|---:|---:|---|
| 空输入/失败分支 | `0x100017578` | `0x2A3578` | `1F 20 03 D5` |
| 失败分支 | `0x1000175AC` | `0x2A35AC` | `1F 20 03 D5` |
| 失败分支 | `0x1000175B8` | `0x2A35B8` | `1F 20 03 D5` |
| 失败分支 | `0x1000175D4` | `0x2A35D4` | `1F 20 03 D5` |
| 失败分支 | `0x1000175DC` | `0x2A35DC` | `1F 20 03 D5` |
arm64 的成功路径后面会到 `0x1000175E0`,成功弹窗回调对应的子程序中又会调用 `RemoveActivation` 和 `Start:`。这也说明不应该只盯着一个判断值,而应该把后面的对象生命周期一起看完。
---
## 六、启动向导不是一个窗口那么简单
激活逻辑处理完以后,普通启动时仍然会遇到向导界面。继续搜索 `NWizard`,可以找到:
```objc
+
-
```
上层的 `WizardOrInitialize` 大致逻辑是:
```text
call +
test return value
返回 0 -> 继续 Initialize
返回非 0 -> 进入 Wizard
```
所以 `OpenWizard:` 应该返回 `0`,让主程序继续初始化,而不是直接把上层启动函数整个截断。
### 6.1 x86_64
`+` 固定返回 `0`:
```asm
xor eax, eax
ret
```
```text
VA : 0x10001D170
fileoff : 0x21170
bytes : 31 C0 C3 90 90 90 90 90 90 90
```
`-` 固定返回 `nil`,防止其它路径再直接创建 `Wizard.nib`:
```asm
xor eax, eax
ret
```
```text
VA : 0x10001D1D9
fileoff : 0x211D9
bytes : 31 C0 C3 90 90 90 90 90 90 90 90 90
```
### 6.2 arm64
`+` 固定返回 `0`:
```asm
mov w0, #0
ret
```
```text
VA : 0x1000162BC
fileoff : 0x2A22BC
original: F4 4F BE A9 FD 7B 01 A9 FD 43 00 91
bytes : 00 00 80 52 C0 03 5F D6 1F 20 03 D5
```
`-` 固定返回 `nil`:
```asm
mov x0, #0
ret
```
```text
VA : 0x100016310
fileoff : 0x2A2310
bytes : 00 00 80 D2 C0 03 5F D6 1F 20 03 D5
```
这一组处理的是 `Wizard.nib`。做完后重新启动,没有立即弹向导窗口,但等几秒又会出现许可证界面。这就是这次分析最容易误判的地方:后面那个窗口其实来自 `Register.nib`,和前面的 Wizard 不是同一条路径。
---
## 七、追踪延迟出现的 `Register.nib`
在 Strings 里搜索:
```text
EnterLicenseDialog
Register.nib
performSelector:withObject:afterDelay:
```
对 `EnterLicenseDialog` 的 selector reference 按 `X` 查看引用,可以看到其中一条路径并不是立即调用,而是通过:
```objc
performSelector:withObject:afterDelay:
```
进行延迟调度。这就解释了为什么 App 的主界面已经出来,许可证窗口还会在后面冒出来。
这里最后采用的原则是:
```text
保留 EnterLicenseDialog 函数本体
保留 + 函数本体
只处理实际触发窗口的 call / jmp / bl 调用点
```
### 7.1 x86_64 的四个调用点
| 位置 | VA | file offset | 原始 | Patch | 说明 |
|---|---:|---:|---|---|---|
| 延迟 `performSelector` | `0x10018D37D` | `0x19137D` | `FF D3` | `66 90` | `call rbx` 改 2-byte NOP |
| 尾调用 | `0x10018F0C1` | `0x1930C1` | `FF E0` | `C3 90` | `jmp rax` 改 `ret; nop` |
| 直接调用 | `0x10018F0DB` | `0x1930DB` | `41 FF D5` | `0F 1F 00` | `call r13` 改 3-byte NOP |
| 直接调用 | `0x10018F480` | `0x193480` | `FF D3` | `66 90` | `call rbx` 改 2-byte NOP |
第一处附近的特征很明显:
```asm
; Objc selector ref: EnterLicenseDialog
; Objc selector ref: performSelector:withObject:afterDelay:
xorpd xmm0, xmm0
call rbx
```
尾调用那一处不能只用 NOP 放任执行流继续跑,因为原本就是函数尾部的 `jmp rax`,所以改成 `ret; nop` 直接结束当前路径。
### 7.2 arm64 的延迟调用点
arm64 中定位到对 `performSelector:withObject:afterDelay:` 的 `bl`:
```asm
; Objc selector ref: EnterLicenseDialog
movi.2d v0, #0
mov x3, #0
bl performSelector:withObject:afterDelay:
```
将这条 `bl` 改为 NOP:
```text
VA : 0x100116AAC
fileoff : 0x3A2AAC
original: 75 E7 00 94
bytes : 1F 20 03 D5
```
### 7.3 这里踩过的坑
之前试过直接让 `EnterLicenseDialog` 函数返回,也试过直接处理 `+`。表面上看弹窗消失了,但程序启动约几秒后会正常退出,终端可看到:
```text
iCollections has been terminated.
```
这不像普通的空指针崩溃,更像某条启动状态链被破坏后走了主动退出。恢复函数本体,只跳过外部调用点后,15 秒存活测试可以正常通过。
这也是这次分析最值得记下来的一点:看到弹窗函数时,不要默认“让它直接返回”就是影响最小的改法。有些函数除了显示界面,还参与了对象创建、状态设置或后续回调。
---
## 八、在 IDA 中写入 Patch
我这里采用的是手工改字节。先在数据库中测试,全部确认后再应用到输入文件。
1. 在 IDA 中按 `G`,跳到对应 VA。
2. 确认当前指令和记录中的原始字节一致。
3. 选择 `Edit -> Patch program -> Change byte...`。
4. 输入等长的 Patch 字节。
5. 通过 `Edit -> Patch program -> Patched bytes...` 检查全部修改。
6. 选择 `Apply patches to input file...` 应用到文件。
x86_64 是可变长指令,原指令几个字节,替换后就必须仍然占几个字节。不足的部分用 NOP 填满,不能让后面的指令整体错位。
本次用到的几个常见模板:
| 架构 | 长度 | 字节 | 说明 |
|---|---:|---|---|
| x86_64 | 2 | `66 90` | 2-byte NOP |
| x86_64 | 3 | `0F 1F 00` | 3-byte NOP |
| x86_64 | 6 | `66 0F 1F 44 00 00` | 6-byte NOP |
| arm64 | 4 | `1F 20 03 D5` | NOP |
| arm64 | 4 | `C0 03 5F D6` | `ret` |
应用后不要只相信 IDA 的提示,再按 file offset 用 `xxd` 抽查几个点:
```bash
xxd -g 1 -s 0xFC421-l 27 "$BIN"
xxd -g 1 -s 0x22DCC-l 6"$BIN"
xxd -g 1 -s 0x339CA8 -l 20 "$BIN"
xxd -g 1 -s 0x3A2AAC -l 4"$BIN"
```
另外,一个架构改完后不要忘了切到另一个 IDB。在 Apple Silicon Mac 上,Finder 中的“使用 Rosetta 打开”、启动方式以及系统环境都可能影响实际跑哪个 slice。只改 arm64,最后却跑到 x86_64,表现就会像“Patch 时好时坏”。
---
## 九、Info.plist 中的架构优先级
为了在 ARM Mac 上优先运行 arm64 slice,样本的 `Info.plist` 中加了:
```xml
<key>LSArchitecturePriority</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
```
可以用 `/usr/libexec/PlistBuddy` 写入:
```bash
PLIST="$APP/Contents/Info.plist"
/usr/libexec/PlistBuddy -c 'Delete :LSArchitecturePriority' "$PLIST" 2>/dev/null || true
/usr/libexec/PlistBuddy -c 'Add :LSArchitecturePriority array' "$PLIST"
/usr/libexec/PlistBuddy -c 'Add :LSArchitecturePriority:0 string arm64' "$PLIST"
/usr/libexec/PlistBuddy -c 'Add :LSArchitecturePriority:1 string x86_64' "$PLIST"
plutil -lint "$PLIST"
```
这不是二进制 Patch 必需的一步,主要是让测试环境更稳定。如果 Finder 简介中手动勾选了“使用 Rosetta 打开”,这个设置仍可能让 App 走 x86_64。
---
## 十、重新签名
Mach-O 和 `Info.plist` 发生变化后,原签名必然失效。这个 App 内部还带了 Framework 和 Login Item,不能只签主程序。
先从原始 App 导出 entitlements 作为参考:
```bash
codesign -d --entitlements :- "$APP" > "$ROOT/original-entitlements.plist" 2>/dev/null
```
本次重签使用的文件名是:
```text
adhoc-entitlements-nolv.plist
```
其中保留了这一项:
```xml
<?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.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
```
之前只签主 App 时,遇到过这个错误:
```text
dyld: Library not loaded: ZipZap.framework
Reason: mapped file and process have different Team IDs
```
原因是主 App 和内嵌 Framework 签名后的 Team ID 关系被破坏。最后的处理方式是清理扩展属性,然后按内层到外层重签:
```bash
ROOT="$HOME/Desktop/iCollections"
APP="$ROOT/iCollections.app"
ENT="$ROOT/adhoc-entitlements-nolv.plist"
xattr -cr "$APP"
codesign --force --deep --sign - \
"$APP/Contents/Library/LoginItems/iCollectionsHelperT.app"
for fw in "$APP/Contents/Frameworks"/*.framework; do
[ -e "$fw" ] || continue
codesign --force --deep --sign - "$fw"
done
codesign --force --deep --sign - --entitlements "$ENT" "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
```
验证成功时应该看到:
```text
iCollections.app: valid on disk
iCollections.app: satisfies its Designated Requirement
```
如果还是报某个 Framework、Helper 或 dylib 签名不一致,就先用下面的命令找出 App 内所有 Mach-O,检查是否有内层组件漏签:
```bash
find "$APP/Contents" -type f -print0 | while IFS= read -r -d '' f; do
file "$f" | grep -q 'Mach-O' && echo "$f"
done
```
---
## 十一、清理旧状态并测试
macOS 会保存窗口恢复状态,App 自己也可能通过 `NSUserDefaults` 记录向导是否完成。调试时如果不清理,很容易出现同一个二进制这次弹窗、下次又不弹的假象。
测试前先清理:
```bash
pkill -9 -x iCollections 2>/dev/null || true
rm -rf "$HOME/Library/Saved Application State/com.naarak.Collections.savedState"
defaults write com.naarak.Collections WizardDone -bool true
defaults write com.naarak.Collections NSQuitAlwaysKeepsWindows -bool false
defaults write com.naarak.Collections ApplePersistenceIgnoreState -bool true
```
普通启动:
```bash
open -n "$APP"
```
除了看界面,还要确认程序不会延迟退出。可以直接从终端启动主程序,连续观察 15 秒:
```bash
BIN="$APP/Contents/MacOS/iCollections"
pkill -9 -x iCollections 2>/dev/null || true
"$BIN" > /tmp/icollections_run.out 2>/tmp/icollections_run.err &
PID=$!
for i in $(seq 1 15); do
sleep 1
if kill -0 "$PID" 2>/dev/null; then
echo "alive $i"
else
echo "exited $i"
break
fi
done
cat /tmp/icollections_run.err
```
本次最终结果是:
```text
alive 1
...
alive 15
```
除了存活时间,还手工检查了下面几项:
```text
App 能正常启动
主界面可以进入
启动时不出现 Wizard.nib
等待后不出现 Register.nib
程序运行 15 秒不自动退出
codesign 深度验证通过
```
---
## 十二、完整 Patch 清单
下面是本版本最终保留的字节,方便自查。合计 23 个修改点:x86_64 共 13 个,arm64 共 10 个。
### x86_64
```text
+
VA 0x1000F8421 / fileoff 0xFC421
B8 01 00 00 00 5D C3 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
+
VA 0x1000F8440 / fileoff 0xFC440
B8 07 00 00 00 5D C3 90
- 失败分支
VA 0x10001EDCC / fileoff 0x22DCC / 66 0F 1F 44 00 00
VA 0x10001EE10 / fileoff 0x22E10 / 66 0F 1F 44 00 00
VA 0x10001EE25 / fileoff 0x22E25 / 66 0F 1F 44 00 00
VA 0x10001EE49 / fileoff 0x22E49 / 66 0F 1F 44 00 00
VA 0x10001EE59 / fileoff 0x22E59 / 66 0F 1F 44 00 00
+
VA 0x10001D170 / fileoff 0x21170
31 C0 C3 90 90 90 90 90 90 90
-
VA 0x10001D1D9 / fileoff 0x211D9
31 C0 C3 90 90 90 90 90 90 90 90 90
EnterLicenseDialog 调用点
VA 0x10018D37D / fileoff 0x19137D / 66 90
VA 0x10018F0C1 / fileoff 0x1930C1 / C3 90
VA 0x10018F0DB / fileoff 0x1930DB / 0F 1F 00
VA 0x10018F480 / fileoff 0x193480 / 66 90
```
### arm64
```text
+
VA 0x1000ADCA8 / fileoff 0x339CA8
20 00 80 52 C0 03 5F D6 1F 20 03 D5 1F 20 03 D5 1F 20 03 D5
+
VA 0x1000ADCC0 / fileoff 0x339CC0
E0 00 80 52 C0 03 5F D6 1F 20 03 D5
- 失败分支
VA 0x100017578 / fileoff 0x2A3578 / 1F 20 03 D5
VA 0x1000175AC / fileoff 0x2A35AC / 1F 20 03 D5
VA 0x1000175B8 / fileoff 0x2A35B8 / 1F 20 03 D5
VA 0x1000175D4 / fileoff 0x2A35D4 / 1F 20 03 D5
VA 0x1000175DC / fileoff 0x2A35DC / 1F 20 03 D5
+
VA 0x1000162BC / fileoff 0x2A22BC
00 00 80 52 C0 03 5F D6 1F 20 03 D5
-
VA 0x100016310 / fileoff 0x2A2310
00 00 80 D2 C0 03 5F D6 1F 20 03 D5
EnterLicenseDialog 延迟调用点
VA 0x100116AAC / fileoff 0x3A2AAC / 1F 20 03 D5
```
---
## 十三、这次遇到的坑
### 1. 只改 arm64,实际却跑到 x86_64
Universal App 不能只凭机器芯片判断运行架构。Finder 的 Rosetta 选项或旧启动配置都可能让 App 跑 x86_64。最稳妥的做法是两个 slice 都分析、都修改。
### 2. 混淆 VA 和 Fat 文件偏移
IDA 中的 `0x100...` 是虚拟地址,不是整个文件的写入偏移。必须结合 slice offset 和 `__TEXT` 基址进行换算。
### 3. x86_64 指令长度没对齐
2 字节 `call` 就用 2 字节 NOP,3 字节 `call` 就用 3 字节 NOP,6 字节条件跳转就用 6 字节 NOP。不能为了省事全写成一个 `90`,后面的残留字节会被重新解码。
### 4. 直接处理 `EnterLicenseDialog` 函数本体
这是本次最关键的坑。窗口是不弹了,但会破坏后面的启动状态,导致程序几秒后自己退出。最终只跳过调用点,保留函数本身。
### 5. 把 `About.nib` 当成授权核心
关于页只是展示层,处理 `NAbout` / `About.nib` 既不能代替授权状态,也不能解决启动窗口。最终已经完全恢复这部分。
### 6. 重签只顾主程序
App 包内的 Helper 和 Framework 也在签名链上。只签外层 App 可能通过表面检查,但运行时会被 dyld 因 Team ID 或 Library Validation 拒绝。
### 7. 没清理 Saved Application State
旧窗口状态会干扰判断。测试一个启动流程时,要么固定用一套干净配置,要么每轮都重置相同的 defaults 和 Saved State,否则前后现象不可比。
### 8. 死记地址
这些 VA 和 file offset 只对 9.7.7 Build 97708 有效。只要上游重新编译,方法顺序和代码布局就可能变化。真正有用的是方法名、selector、字符串和调用关系。
---
## 十四、新版本怎么重新定位
新版本不要照抄上面的地址,可以按下面的顺序重新走一遍。
### 第一步:重新确认 slice
```bash
BIN='/path/to/iCollections.app/Contents/MacOS/iCollections'
file "$BIN"
lipo -detailed_info "$BIN"
```
记录新的 slice offset,再根据 Mach-O segment 信息确认 `__TEXT` 基址,不要默认它永远不变。
### 第二步:从 Objective-C 方法开始
优先定位:
```objc
+
+
+
+
-
+
-
```
命令行也可以用 `otool` 先筛一遍:
```bash
otool -arch x86_64 -ov "$BIN" | \
egrep -A30 -B8 'NRegister|IsRegistered|LicenseType|DecodeKey|NWizard|Activate:|OpenWizard:'
otool -arch arm64 -ov "$BIN" | \
egrep -A30 -B8 'NRegister|IsRegistered|LicenseType|DecodeKey|NWizard|Activate:|OpenWizard:'
```
### 第三步:跟失败分支,不要只找常量
进入 `Activate:` 后,顺着 `DecodeKey` 的返回值往后看,识别哪些分支进入 invalid alert,哪些分支直接 return,哪一条才是成功流程。优化级别变化后,条件跳转的排列可能和这一版完全不同。
### 第四步:用 selector 找延迟窗口
```bash
otool -arch x86_64 -tvV "$BIN" | grep -B8 -A14 'EnterLicenseDialog'
otool -arch arm64-tvV "$BIN" | grep -B8 -A14 'EnterLicenseDialog'
```
重点看:
```text
EnterLicenseDialog
performSelector:withObject:afterDelay:
Register.nib
```
先确认调用者和对象生命周期,然后再决定处理调用点还是返回值。上一版能用的 Patch 模式,不代表下一版仍然是同一条调用链。
---
## 结语
这次分析最开始只是想找一个授权判断,后面才发现它实际上是四块逻辑叠在一起:
```text
授权状态查询
-> 激活按钮的输入和 DecodeKey 分支
-> Wizard.nib 启动向导
-> Register.nib 延迟许可证窗口
```
真正花时间的地方不是把几条指令改成 NOP,而是判断哪个点改完后不会破坏后面的流程。直接封掉窗口函数导致延迟退出,就是一个很典型的例子。
最后再强调一遍:本文中所有绝对地址和文件偏移都只对 iCollections 9.7.7 Build 97708 这一个样本有效。版本变化后,请按方法名、selector、字符串和 Xref 重新定位,不要直接套地址。
本文只是用于交流学习用途,请勿非法使用。
再次也祝吾爱论坛越做越好,蒸蒸日上。 这个是干嘛的
学习大佬操作,感谢 虽然看不懂,但是非常支持,加油 谢谢分享 这个不错,不知道对mac的版本有没有要求 学习了{:1_937:}挺好用的 哇,大佬。学习学习 佬有没有空出一个ios插件逆向教程 Yeah_whr 发表于 2026-7-21 13:54
佬有没有空出一个ios插件逆向教程
iOS的插件逆向教程除了很多了啊
页:
[1]
2