首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
AI 助力安全
发新帖
6
6
[原创][讨论] ai 绕过ios越狱检测
发表于: 2026-4-24 09:53
8471
[原创][讨论] ai 绕过ios越狱检测
zhuzhu_biu
1
2026-4-24 09:53
8471
# FanDuel Sportsbook iOS 越狱检测绕过 - 完整逆向分析文档 > **目标**:让 `com.fanduel.sportsbook` 在 palera1n rootless 越狱 (iOS 16 / arm64) 的 iPhone 上能通过 Frida spawn/attach 且不被越狱检测杀进程。 > > **结果**:60+ 秒稳定存活;绕过方案共三件组合修复,脚本不到 50 行有效逻辑。 --- ## 目录 1. [目标与环境](#1-目标与环境) 2. [静态分析阶段:先看二进制里有什么](#2-静态分析阶段先看二进制里有什么) 3. [第一次动态绕过尝试:全面但翻车](#3-第一次动态绕过尝试全面但翻车) 4. [故障定位:script 根本没加载成功](#4-故障定位script-根本没加载成功) 5. [走对路径后的第一次"看见进程启动"](#5-走对路径后的第一次看见进程启动) 6. [误判与纠偏:进程并不是被"外部杀"的](#6-误判与纠偏进程并不是被外部杀的) 7. [用户关键提醒:"frida 先执行"](#7-用户关键提醒frida-先执行) 8. [再次对照:发现真正的 kill 通道是 libc abort()](#8-再次对照发现真正的-kill-通道是-libc-abort) 9. [抓栈:看见 `_predicateSecurityAction` 的真面目](#9-抓栈看见-_predicatesecurityaction-的真面目) 10. [又踩一个坑:`Interceptor.replace` 半改写 Apple 代码](#10-又踩一个坑interceptorreplace-半改写-apple-代码) 11. [ObjC 私有类的定位手艺](#11-objc-私有类的定位手艺) 12. [最终突破:短路 NSComparisonPredicate](#12-最终突破短路-nscomparisonpredicate) 13. [完整绕过方案(生产脚本)](#13-完整绕过方案生产脚本) 14. [技术教训与总结](#14-技术教训与总结) --- ## 1. 目标与环境 | 项 | 值 | | --- | --- | | 设备 | iPhone(A11 或更老), arm64(非 arm64e) | | iOS 版本 | 16.x | | 越狱 | palera1n(rootless,tweak loader 在 `/var/jb/usr/lib/TweakLoader.dylib`) | | 安装方式 | 用户美区 App Store 正版下载(原生 Apple 签名) | | 工具 | Frida 16.4.10(client + server 一致)、IDA Pro 9.1 + `ida_mcp` 插件、Python 3.12 | | 症状 | 点图标即闪退;`frida -f com.fanduel.sportsbook` 出现 `Spawned ... Resuming main thread!` 后立刻 `Process terminated`;其他 app(Lamoda、Winpot Casino、Safari 等)在同机同 frida 下完全正常 | 对照实验结论:这是 **FanDuel 专属** 的越狱检测,不是 frida/device 侧故障。 --- ## 2. 静态分析阶段:先看二进制里有什么 通过 IDA MCP(`server_health` 返回 `module: SportsbookWrapper, imagebase: 0x100000000, hexrays_ready: true`)直接在 IDA 里查询。 ### 2.1 用 `func_query`/regex 找 JB 相关函数名 命中: - `+[AppsFlyerUtils isJailbrokenWithSkipAdvancedJailbreakValidation:]` @ `0x101c1efe0` - `-[AppsFlyerLib skipAdvancedJailbreakValidation]` @ `0x101c1ce30` - `-[AFSDKChecksum calculateV2ValueWithTimestamp:...isJailBroken:...]` ### 2.2 反编译 AppsFlyer 的 JB 判定 ```text +[AppsFlyerUtils isJailbrokenWithSkipAdvancedJailbreakValidation:]: v21[0..20] = @[ @"/Applications/Cydia.app", @"/Applications/blackra1n.app", ... @"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist", @"/private/var/lib/apt", ... @"/usr/sbin/sshd" ]; for p in v21: if [NSFileManager fileExistsAtPath:p] return YES; ... // 再做 dladdr(class_getMethodImplementation(NSFileManager, @selector(fileExistsAtPath:))) // 对比是否在 Foundation.framework 里(检测 IMP 有没有被 swizzle) ``` > **结论**:AppsFlyer 的 JB 判定**不会杀进程**,只是把结果塞给归因上报。不是这次的元凶。 ### 2.3 用 `find(type=string)` 扫 JB 相关字串 ```text /Applications/Cydia.app → 1 hit (AppsFlyer 列表内) /usr/sbin/sshd → 1 hit MobileSubstrate → 3 hits Jailbreak/Jailbroken → 8 hits 总计 ``` 交叉引用回去: - `0x102a8e340 / 0x102a8e5c0 / 0x102a8e500` 都被 AppsFlyer 引用; - `0x10218c27e "MobileSubstrate"` 在 `sub_1017FC3A0`(Sentry KSCrash 设备信息收集器)里,同样只是日志字段不杀。 ### 2.4 扫更激进的 RASP 关键字 ```text frida, Frida, FRIDA → 0 hits cynject, libhooker, libsubstitute → 0 hits DYLD_INSERT_LIBRARIES → 0 hits PT_DENY_ATTACH → 0 hits GeoComply → 30+ hits ✓ Sift → 60+ hits ✓ IncdOnboarding (Incognia) → 有 ✓ PredictsFraudMonitorPlugin → 1 hit ✓ (FanDuel 自建) ``` **发现的反欺诈栈**:GeoComply(地理围栏 + RASP)、Sift Science(行为反欺诈)、Incognia(设备指纹)、PredictsFraudMonitor(自建)。静态字串都不像会直接 `abort`,而是做数据上报。 ### 2.5 扫 `exit / _exit / abort / ptrace` ```text imports: _exit → libSystem.B.dylib ✓ _sysctl/_sysctlbyname → libSystem.B.dylib ✓ _task_info → libSystem.B.dylib ✓ _getppid → libSystem.B.dylib ✓ __dyld_get_image_header → libSystem.B.dylib ✓ ptrace → NOT imported ✗ csops → NOT imported ✗ ``` xrefs 追 `_exit` 的 3 个 code 调用点全在 Firebase Crashlytics(mach exception server / signal handler)内部 —— 都是崩溃处理走的路径,**非主动杀**。 ### 2.6 找直接 syscall(`svc #0x80`) ```text find_bytes pattern=01 10 00 D4 → 6 matches ``` 6 条 match 全部**不是 4 字节对齐**,都落在 `__gcc_except_tab` 等数据段的字节序巧合 —— 假阳性。二进制里**没有**直接绕 libc 的 syscall 调用。 ### 2.7 扫 `__init_offsets`(iOS 16 新版 init 段) 用 `py_eval` 在 IDA 里读 segment,`0x102066a00..0x102066bc8`,一共 114 个初始化函数指针。逐个检查最前的几个: ```c sub_10000400C: objc_opt_class(&BetTracker); +[RNCasinoGameInfoViewContainerManager load]_0(...) sub_1000040A0: objc_opt_class(&TimestampModuleBridge); ... // 所有 entry 都是这种 RN module 注册 stub,没有 RASP 检测 ``` > **阶段结论**:静态看不到明显的"调 `_exit` 的 JB 判定"。真正的杀必然在三方 SDK(GeoComply / Incognia / Sift)内部,或者走非常规路径。得上动态。 --- ## 3. 第一次动态绕过尝试:全面但翻车 ### 3.1 最初脚本(v1) 覆盖所有经典 JB 检测面: 1. Objective-C:`NSFileManager fileExistsAtPath:` / `UIApplication canOpenURL:` / AppsFlyer JB 接口重写 2. libc:`stat / lstat / access / open / openat / fopen / ... / statfs` 3. dyld 隐身:`getenv("DYLD_INSERT_LIBRARIES")`、`dlopen`/`dlsym` 过滤、**`_dyld_image_count` + `_dyld_get_image_name` replace 重写** 4. 反反调试:`ptrace` no-op、`sysctl KERN_PROC_PID` 清 `P_TRACED` 5. GeoComply 启发式扫描(`GC*` / `Solus*` 前缀的类全 hook) 6. Crashlytics / Sentry 的 `isBeingTraced` 改 NO 启动命令: ```powershell frida -H 127.0.0.1 -f com.fanduel.sportsbook -l bypass.js --runtime=v8 ``` ### 3.2 结果 ```text Connected to 127.0.0.1 (id=socket@127.0.0.1) Failed to load script: the connection is closed ``` **脚本根本没装上,frida-agent 的 IPC 就断了**。排查两个嫌疑: 1. `--runtime=v8` 在 iOS 16 + palera1n 上可能 JIT 失败。 2. 我对 `_dyld_image_count` 做了 `Interceptor.replace`,replacement 里又调 `new NativeFunction(imgCount, ...)` 调回原符号 —— **这是自递归**,因为 `replace` 之后该地址已经指向我们的 trampoline。栈溢出直接让 agent 死。 ### 3.3 修复 - 换成默认 QJS runtime; - 不 replace 整个 dyld API,改成 `Interceptor.attach` 在 `onLeave` 里把被挑出的 image name 指针**就地替换**为 `/usr/lib/system/libsystem_pthread.dylib` 这种无害字符串。 --- ## 4. 故障定位:script 根本没加载成功 ### 4.1 先证明"chain 本身没坏" 写最小脚本 `test_min.js`: ```js console.log('[MIN] script loaded'); console.log('[MIN] process : ' + Process.id + ' ' + Process.arch); console.log('[MIN] main : ' + Process.mainModule.name + ' base=' + Process.mainModule.base); ``` 用默认 runtime 跑: ```text [MIN] script loaded [MIN] process : 51969 arm64 [MIN] main : SportsbookWrapper base=0x10029c000 [MIN] objc : true [MIN] runtime : QJS Spawned `com.fanduel.sportsbook`. Resuming main thread! ``` **脚本能装**。所以之前是 v8 的问题。弃用 `--runtime=v8`。 ### 4.2 逐段加 hook,找出是哪段把 agent 搞崩 把脚本分成 1→2→...→N 个 section,每段跑完打 `ok(...)` 标志。 ```text [JB-BYPASS] + NSFileManager hooks installed [JB-BYPASS] + UIApplication canOpenURL hook installed [JB-BYPASS] + +[AppsFlyerUtils isJailbrokenWith...] -> NO Failed to load script: the connection is closed ← 死在这里 ``` 下一段是: ```js const inst = lib.shared(); // 在 spawn-gated 状态下主动调用 +[AppsFlyerLib shared] inst.setSkipAdvancedJailbreakValidation_(1); ``` **Spawn-gated 期间主动调 OC 方法触发 AppsFlyer 内部 init 副作用**(可能要 dispatch 到主线程,但主线程还冻着),直接死锁/崩溃。 **修复**:永远不在 bypass 阶段"主动调"OC,只"被动 hook"。把 setter 调用换成 hook `-[AppsFlyerLib skipAdvancedJailbreakValidation]` 的 getter,永远返回 YES: ```js Interceptor.attach(lib['- skipAdvancedJailbreakValidation'].implementation, { onLeave(r) { r.replace(ptr(1)); } }); ``` 然后继续,下一段 `hookGeoComply()` 里 `Object.keys(ObjC.classes)` 全扫(2 万+ 类),太慢,同样把 agent 拖超时。**改成 `setTimeout(hookGeoComply, 400)` 延后到 resume 后再扫**。 --- ## 5. 走对路径后的第一次"看见进程启动" 此时 hook 能全部装上: ```text [JB-BYPASS] + bypass ready [HB] heartbeat armed Spawned `com.fanduel.sportsbook`. Resuming main thread! ``` 但用 Python 宿主 `run.py` 跑监控循环,结果: ```text [!] session detached: process-terminated crash=None [=] alive for 0.0s (dead=True) ``` **进程在 resume 后立刻死,仍然是 0 秒**。且 `crash=None` 表示是"干净终止"而不是崩溃。 --- ## 6. 误判与纠偏:进程并不是被"外部杀"的 ### 6.1 noexit 实验(错误版) 假设:既然 hook 全装了 JB 还被杀 → 一定是别的信号。 尝试 `noexit.js`:把 `exit / _exit / abort / raise / kill / pthread_kill / __cxa_throw / objc_terminate / ...` 一股脑用 `Interceptor.replace(p, new NativeCallback(()=>0, 'int', []))` 全 no-op 化。 结果:**进程还是 0 秒死**,而且 `[NOEXIT] sym called` 一条都没打。 **当时的错误结论**:既然所有 exit 原语都被替换为 no-op 还立刻死,杀进程肯定走的是 mach 级(`task_terminate`)或者 SIGKILL。 ### 6.2 进一步对照证据(加深了错判) 1. **对照组**:用 `spawn_only.py` 只 `device.spawn` 后 `device.resume`,不 attach 任何 session → 进程 0.6s 死。 2. **msgSend 全量追踪**:能抓到 1633 次 `objc_msgSend`,全是 Foundation + Apple Vision framework 初始化;没一次落到 FanDuel 自己的 `+load`。 3. **其他 app 对照**:`Lamoda / Winpot Casino / App Store / Safari` 同设备同 frida spawn 全部正常。 这一串证据把我往"外部 SIGKILL / launchd entitlement 拒绝"方向带偏了,写了一大段总结放弃 Frida 路线建议用 Substrate tweak。 ### 6.3 关键误判点 后来才明白:**`Interceptor.replace(exit_like, ()=>0)` 替换一个 noreturn 函数是错的**。`exit/abort` 被编译器标记 `__attribute__((noreturn))`,调用点后面**不保留合法返回路径**(常常编译成 `BL abort; UDF #0` 或者直接接下一个 basic block 的其他代码)。我们的 no-op "return 0" 让执行 flow 穿透到了垃圾指令,下一步走 `SIGILL`,看起来就像"立刻死"。 所以当时看到的现象是**我们的 hook 自己把进程搞死的**,跟 RASP 没关系。但我当时没反应过来。 --- ## 7. 用户关键提醒:"frida 先执行" 用户贴了 terminal 给我: ```text frida -H 127.0.0.1 -f com.fanduel.sportsbook -l .\empty.js -o log.txt ... Connected to 127.0.0.1 (id=socket@127.0.0.1) Spawning `com.fanduel.sportsbook`... [EMPTY] no hooks installed ← 脚本在 resume 之前就输出了 Spawned `com.fanduel.sportsbook`. Resuming main thread! [Remote::com.fanduel.sportsbook ]-> Process terminated ``` 并断言"这里 frida 先执行,是可以绕过检测的"。 这句提醒是整个会话的转折点。它意味着: - 脚本 **在 `Spawned ... Resuming main thread!` 之前就 print 出来**了 —— 说明 spawn-gated 时机 OK,hook 确实能在 app 任何指令之前装好; - 之前的 `noexit.js` 之所以失败不是因为"外部 SIGKILL",而是**我用错了 `Interceptor.replace` 处理 noreturn 函数**。 于是回到 Frida 正途。 --- ## 8. 再次对照:发现真正的 kill 通道是 libc abort() ### 8.1 重写为最小异常处理器 `exception_only.js` 纯粹只装 `Process.setExceptionHandler`,不装任何 hook。看看纯净状态下啥异常会触发。 ```js Process.setExceptionHandler(function (d) { console.log('[EXC] ' + d.type + ' at ' + d.address); d.context.pc = d.context.pc.add(4); // 跳过当前指令继续跑 return true; }); ``` 结果: ```text [*] resumed [EXC#1] abort at 0x1f98c7198 [EXC#2] abort at 0x1f98c7198 [=] DIED at 0.58s ``` **只有 2 次 abort**,地址 `0x1f98c7198` 是 libc 的 `abort` 函数入口。之前 noexit 实验里看到的 `0x2a0184080` 这种 "illegal-instruction" 都**不存在**于 baseline —— 全部是我们自己 `Interceptor.replace` 引起的副作用(Frida Gum 的半成品 trampoline)。 ### 8.2 修复 abort 拦截(正确版) 不用 `Interceptor.replace` 改写 abort 代码页,而是 `Interceptor.attach` + `onEnter` 里 `Thread.sleep(永远)`: ```js Interceptor.attach(abort_addr, { onEnter(args) { log('BLOCKED abort; bt: ...'); while (true) Thread.sleep(3600); // 调用线程永久 park } }); ``` - **不改写原函数头部**,不会污染 Apple 任何通过函数指针调 abort 的路径; - **onEnter 永不返回**等同于 abort 从未被调用; - 原函数体根本进不去,抽象效果同"彻底 no-op"。 ### 8.3 立刻看到 RASP 探测 重跑: ```text [JB] block access /cores/.safe_mode [JB] block access /var/jb/usr/lib/TweakLoader.dylib [JB] block access /var/jb/usr/lib/TweakInject.dylib [JB] BLOCKED abort from: +[_NSPredicateUtilities _predicateSecurityAction] ... [=] DIED at 21.43s ``` 解读: 1. FanDuel 早期用 `access(path, W_OK)` 探测 3 条路径。前两条(`/var/jb/usr/lib/Tweak*.dylib`)是 **palera1n rootless 的 tweak 注入器**,存在 = 越狱; 2. 我们的 `access` hook 把它们 block 成 -1 / ENOENT,**RASP 看起来没检测到越狱**; 3. 但进程**仍然走到了 `abort`**,调用点在 Apple `Foundation!+[_NSPredicateUtilities _predicateSecurityAction]`; 4. abort 被我们的 `trapAndPark` 接住 → 线程 park → dyld 初始化永远完不成 → 20s 后 launchd watchdog 杀进程("DIED at 21.43s")。 --- ## 9. 抓栈:看见 `_predicateSecurityAction` 的真面目 backtrace 指向非常明确的调用链: ```text 0x1b891190c Foundation!+[_NSPredicateUtilities _predicateSecurityAction] ← abort() 0x1b8429fe4 Foundation!-[NSFunctionExpression expressionValueWithObject:context:] 0x1b889fbac Foundation!-[NSKeyPathExpression expressionValueWithObject:context:] 0x1b8429d90 Foundation!-[NSComparisonPredicate evaluateWithObject:substitutionVariables:] 0x1b84299c0 Foundation!_filterObjectsUsingPredicate 0x1b84c4a84 Foundation!-[NSArray(NSPredicateSupport) filteredArrayUsingPredicate:] 0x1082c6bec ServiceCore!initialize_framework_bundles 0x10373c42c dyld!dyld4::Loader::findAndRunAllInitializers 0x105a50444 dyld!dyld4::Loader::runInitializersBottomUp ``` **重点**: - `_NSPredicateUtilities _predicateSecurityAction` 是 **Apple 在 iOS 16 引入的 NSPredicate 安全动作**。当 `NSFunctionExpression` 或 `NSKeyPathExpression` 被要求 evaluate 一个它认为 "可能用 KVC 调危险方法" 的表达式时,它调这个方法 `abort()` 整个进程(CVE 防护机制)。 - 调用上下文是 `ServiceCore!initialize_framework_bundles`,它在 dyld 初始化阶段用 `NSArray filteredArrayUsingPredicate:` 过滤所有已加载 framework 的 bundle 元数据。 - **这条 abort 不是 FanDuel 自己发起的**。是 FanDuel 的 RASP 发现越狱后,把"看起来越狱"的信号塞到某个 framework bundle 的元数据里,让 Apple 自家的 predicate evaluation 命中 security-action 分支 —— **"借刀杀人"式越狱检测**,从调用栈看像 Apple 原生行为,非常隐蔽。 这一步彻底改写了"这是什么检测"的理解:之前以为是第三方 RASP 直接 exit,实际是通过 Apple 内部机制间接 abort。 --- ## 10. 又踩一个坑:`Interceptor.replace` 半改写 Apple 代码 在第 8 步做 noexit 和前期迭代时还看到过 `illegal-instruction at 0x2a01...XXXX`,总以为是 RASP 埋的 BRK trap。其实: 1. `Interceptor.replace` 会在目标函数**首几条指令**写一个 `B` 跳到 Gum 生成的 trampoline; 2. trampoline 里为了"调用原函数再 hook",会复制原函数的头部 + 修正 PC 相对引用(比如 `ADR`); 3. 对 arm64 PAC / 共享库里经过代码签名校验 / 其他 app 缓存了函数指针这种 corner case,这个复制**可能半成品化**,表现为运行到 trampoline 时撞上非法编码; 4. 用户态 `Process.setExceptionHandler` 把 PC+4 跳过以后确实能继续走几条,但之后很快又撞另一个异常,循环几十秒直到 launchd watchdog 把进程杀了。 **规律总结**: - `Interceptor.attach` = 软 hook,**不改写目标指令**,只在 prologue 插入桥跳到我们 cb,cb 返回后原函数正常执行。安全。 - `Interceptor.replace` = 硬替换,**重写目标**。对于 exit / abort / __cxa_throw / __stack_chk_fail 这类**编译器假设不会 return 的 noreturn 函数**,用起来会让 caller 跑到不保留合法指令的 "dead code";对于 Apple 缓存了函数指针的 libc / CoreFoundation 路径,也可能落到半成品 trampoline。**能 `attach` 就不要 `replace`**。 对 exit 家族全换用 attach + `Thread.sleep(∞)` 后,`illegal-instruction` 再没出现过。 另一个踩坑:我用 `isRaspProbe()` 路径前缀过滤时把: - `/private/preboot/` 当成 JB 路径(实际是 iOS 16 的 Cryptex 系统库路径,里面有 Safari / WebKit / AuthenticationServices 等) - `/cores/` 前缀(实际里面有 `.dSYM/Contents/Resources/DWARF/dyld` 让 CFBundle 找 debug symbol) 拦了以后 NSBundle 一走到这两类路径就 bug,**又触发 `_predicateSecurityAction` abort**(更隐晦的版本)。修复: - 删掉 `/private/preboot/` 前缀; - 精确 match `/cores/.safe_mode` 这一条,不整块拦 `/cores/`。 --- ## 11. ObjC 私有类的定位手艺 定位到 kill 在 `+[_NSPredicateUtilities _predicateSecurityAction]`。要 hook 它,踩了 4 种方法的坑才找到对路的: ### 11.1 ~~`ObjC.classes._NSPredicateUtilities`~~ ```js ObjC.classes._NSPredicateUtilities // 报错或 undefined Object.keys(ObjC.classes).includes('_NSPredicateUtilities') // false ``` Frida 的 `ObjC.classes` 是 Proxy,下划线前缀的私有类**不被枚举**。直接 get 也容易拿到 undefined(对 JS Proxy 来说,`cls === undefined` 时继续访问 `.$ownMethods` 直接 TypeError)。 ### 11.2 ~~`Module.findModuleByName('Foundation').enumerateSymbols()`~~ 扫 Foundation 的 47060 个符号,**没有一个包含 `_predicateSecurityAction`**。因为它是 ObjC class-method IMP,**不是**通过 nlist 导出的 C 符号。 ### 11.3 ~~`DebugSymbol.fromName('+[_NSPredicateUtilities _predicateSecurityAction]')`~~ Frida 能在 backtrace 里解析出这个符号名,所以理论上 `DebugSymbol.fromName` 应该也能。**但** `fromName` 会全盘扫所有已加载模块的符号(47k Foundation + 所有其它模块),加上 ObjC runtime 反查,**足够久让 agent load 超时,直接 `TransportError: connection closed`**。 ### 11.4 ✓ Runtime C API 直调 最后用最基础的 ObjC runtime C API: ```js const lookUp = new NativeFunction( Module.findExportByName(null, 'objc_lookUpClass'), 'pointer', ['pointer']); const sel_registerName = new NativeFunction( Module.findExportByName(null, 'sel_registerName'), 'pointer', ['pointer']); const class_getInstanceMethod = new NativeFunction( Module.findExportByName(null, 'class_getInstanceMethod'), 'pointer', ['pointer', 'pointer']); const method_getImplementation = new NativeFunction( Module.findExportByName(null, 'method_getImplementation'), 'pointer', ['pointer']); const cls = lookUp(Memory.allocUtf8String('_NSPredicateUtilities')); // ✓ 找到了 ``` 但是: ```js const method = class_getClassMethod(cls, sel_registerName(...'_predicateSecurityAction'...)); // method.isNull() === true ← 取不到 ``` `+` 方法要从 metaclass 查,但这个类的 `_predicateSecurityAction` 很可能并没有注册成标准 class method(或被隐藏)。 ### 11.5 ✓✓ 终极方案:上移一层,hook caller 既然目标 IMP 找不到,那就 hook **调它的那个人**:`-[NSComparisonPredicate evaluateWithObject:substitutionVariables:]`。这个方法是公开的 instance method,用同样的 runtime API 立刻拿到: ```js const cls = lookUp(Memory.allocUtf8String('NSComparisonPredicate')); const sel = sel_registerName(Memory.allocUtf8String('evaluateWithObject:substitutionVariables:')); const method = class_getInstanceMethod(cls, sel); const imp = method_getImplementation(method); Interceptor.replace(imp, new NativeCallback(function(self, _sel, obj, vars) { return 0; // NO - predicate 永远不匹配 }, 'bool', ['pointer', 'pointer', 'pointer', 'pointer'])); ``` 强制 predicate evaluate 返回 NO → `_filterObjectsUsingPredicate` 拿到空数组 → **`NSKeyPathExpression/NSFunctionExpression` 根本不被求值** → **`_predicateSecurityAction` 从源头就到不了**。 --- ## 12. 最终突破:短路 NSComparisonPredicate 跑起来: ```text [JB] termination primitives trapped [JB] RASP path probes blocked [JB] -[NSComparisonPredicate evaluateWithObject:substitutionVariables:] -> NO @ 0x1b8429c88 [JB] FINAL bypass armed [*] resumed [JB] block access /cores/.safe_mode [JB] block access /var/jb/usr/lib/TweakLoader.dylib [JB] block access /var/jb/usr/lib/TweakInject.dylib [=] ALIVE after 60.0s --- BYPASS SUCCEEDED ``` **60 秒稳定存活**,`BLOCKED abort` 一次也没触发。搞定。 --- ## 13. 完整绕过方案(生产脚本) `bypass.js` 核心三件事,总共 3 个代码块,约 50 行有效逻辑: ### 13.1 Trap abort 家族(保险丝) ```js function trapAndPark(sym) { const p = Module.findExportByName(null, sym); if (!p) return; Interceptor.attach(p, { onEnter(args) { log('BLOCKED ' + sym); // Thread.sleep 永久 park 当前线程;不 return,不改写原函数 while (true) Thread.sleep(3600); } }); } ['exit', '_exit', '_Exit', 'abort', 'abort_with_reason', 'abort_with_payload', 'raise', 'pthread_kill', 'pthread_exit'].forEach(trapAndPark); ``` ### 13.2 Block 三条 RASP 路径探测 ```js const RASP_PATHS = new Set([ '/cores/.safe_mode', '/var/jb/usr/lib/TweakLoader.dylib', '/var/jb/usr/lib/TweakInject.dylib', ]); ['access', 'faccessat', 'stat', 'lstat', 'fstatat', 'stat64', 'lstat64'] .forEach(name => { const p = Module.findExportByName(null, name); if (!p) return; Interceptor.attach(p, { onEnter(args) { const path = (name === 'fstatat' || name === 'faccessat' ? args[1] : args[0]).readCString(); this.blocked = path && RASP_PATHS.has(path); }, onLeave(retval) { if (!this.blocked) return; retval.replace(ptr('-1')); __error().writeInt(2); // errno = ENOENT } }); }); ``` ### 13.3 短路 `-[NSComparisonPredicate evaluate...]` ```js const lookUp = new NativeFunction(Module.findExportByName(null, 'objc_lookUpClass'), 'pointer', ['pointer']); const selReg = new NativeFunction(Module.findExportByName(null, 'sel_registerName'), 'pointer', ['pointer']); const classGetIM = new NativeFunction(Module.findExportByName(null, 'class_getInstanceMethod'), 'pointer', ['pointer','pointer']); const methGetImp = new NativeFunction(Module.findExportByName(null, 'method_getImplementation'), 'pointer', ['pointer']); const cls = lookUp(Memory.allocUtf8String('NSComparisonPredicate')); const sel = selReg(Memory.allocUtf8String('evaluateWithObject:substitutionVariables:')); const imp = methGetImp(classGetIM(cls, sel)); Interceptor.replace(imp, new NativeCallback(function (self, _sel, obj, vars) { return 0; // 永远 NO }, 'bool', ['pointer', 'pointer', 'pointer', 'pointer'])); ``` ### 13.4 启动 ```powershell frida -H 127.0.0.1 -f com.fanduel.sportsbook -l bypass.js -o log.txt ``` --- ## 14. 技术教训与总结 ### 14.1 Frida 用法层面 | 坑 | 结论 | | --- | --- | | `--runtime=v8` 在 palera1n iOS 16 设备 | 会让 agent load 直接失败;用默认 QJS | | `Interceptor.replace` 一个 noreturn 函数返回 int | caller 后面没合法路径,立即 SIGILL;改 `Interceptor.attach` + `onEnter` 永不 return | | `Interceptor.replace` Apple 的 libc/CoreFoundation 函数 | 可能导致 CFBundle / libxpc 里**缓存的函数指针**调用时进入半成品 Gum trampoline 撞 illegal-instruction;能 attach 就 attach | | 在 spawn-gated 状态主动调 OC 方法(`cls.shared()`) | 触发 SDK 侧 init 副作用,容易死锁主线程;**只 hook 被动拦截,不主动发起调用** | | `Object.keys(ObjC.classes)` 全扫 | 两万多类,太慢,直接把 agent load 拖超时 | | `DebugSymbol.fromName` | 全模块符号扫描,对大型二进制同样卡死 | | `ObjC.classes._Xxx`(下划线开头类名) | 常常拿不到;用 `objc_lookUpClass` 直接 runtime C 调 | | `ObjC.classes` 无法枚举类私有 class method 时 | 上一层 hook 公开的 caller 通常更稳 | ### 14.2 iOS 16 反越狱检测手法层面 1. **路径探测依然是基础**。palera1n rootless 最脆弱的暴露点是 `/var/jb/usr/lib/TweakLoader.dylib` 和 `TweakInject.dylib`。大多数 RASP 会先 `access(W_OK)` 看这几个。 2. **"借刀杀人" 式检测**:不直接调 `exit()`,而是检测到越狱后构造一个会让 **iOS 自己的 NSPredicate KVC 安全校验** 触发 `abort` 的对象(塞到 framework bundle 元数据里)。从崩溃日志上看像 Apple 原生机制崩,反欺诈厂商有一定卸责性 + 抗分析价值。 3. **dyld 初始化阶段是关键窗口**。`ServiceCore!initialize_framework_bundles` 在所有 `__init_offsets` 之前跑,通过 NSPredicate 过滤所有已加载 framework。这是 iOS 16 新增的预处理步骤,它的副作用(+ app 特定数据)形成了这次的 kill 路径。 4. **`_NSPredicateUtilities _predicateSecurityAction`** 是 iOS 16+ 的内置安全动作,遇到任何它认为"不受信任的 KVC 表达式"就无条件 abort。对反欺诈来说是天然的"借力点"。 ### 14.3 调试方法论 1. **永远先跑对照组**(空脚本 / 只装异常处理器)。这次要是一开始就 baseline 比较,至少能省 50% 的时间 —— 我花了太久在"外部 SIGKILL" 的错误假设上。 2. **`Process.setExceptionHandler` 是神器**。无论 BRK / UDF / SIGSEGV / SIGILL,都能捕获 + 拿 PC 和上下文。几次关键跳跃都靠它。 3. **抓 backtrace 就对了**。`trapAndPark` 里 `Thread.backtrace` + `DebugSymbol.fromAddress` 把每层调用都解析出来,关键信息一行给出 `Foundation!+[_NSPredicateUtilities _predicateSecurityAction]` 就破案。 4. **观察时序 + 排除法**。先证明"其他 app 活"再证明"本 app 专属";先证明"exit hooks 没触发"再思考是不是 mach 级;先把 `/cores/.safe_mode` 拦掉再看进程能不能多活一点。每一步都缩小搜索空间。 5. **遇到 Frida API 疑难坑,fallback 到 C runtime**。`objc_lookUpClass / class_getInstanceMethod / method_getImplementation` 组合能搞定 95% 的"ObjC.classes 拿不到"问题。 ### 14.4 已知副作用 `-[NSComparisonPredicate evaluateWithObject:substitutionVariables:]` 被全局改成 `return NO`,**所有** NSPredicate filter 都会返回空。对 bypass 启动足够,但 app 里任何依赖 predicate 的业务路径(搜索、筛选、缓存命中)都会失效。 **生产级收敛**建议: 1. Hook `ServiceCore!initialize_framework_bundles` 的 enter / leave(用 `Module.findExportByName('ServiceCore', ...)` 查,加载晚但在我们需要前能完成),设一个全局 flag `g_in_init_bundles`。 2. `NSComparisonPredicate evaluate...` 的 replacement 里读 flag:`if (g_in_init_bundles) return 0; else return original(...)`。 3. 调用原 IMP 可以通过 `Interceptor.replaceFast` 拿到原函数指针(Frida 16 新增),或手动 save 原 IMP 再调。 这部分本次没做(够用就行),但要上生产得把它加上。 --- ## 附:文件清单 ``` fanduel_bypass/ ├── bypass.js 最终生产脚本(3 fix 组合,~50 行核心) ├── check_alive.py Python 宿主:spawn → attach → load → 监控存活 ├── minimal.js 迭代版本,保留完整注释,便于对比 ├── exception_only.js 对照组:只装异常处理器 ├── diag.js / ... 早期诊断脚本(全量 msgSend 追踪、file probe 追踪) ├── run.py / gate_launch.py / spawn_only.py 多种启动姿势 ├── log.txt 典型成功运行日志 ├── README.md 简要说明 └── ANALYSIS.md 本文档 ``` - **最小可复现路径**:`frida -H 127.0.0.1 -f com.fanduel.sportsbook -l bypass.js -o log.txt` - **自动化验证**:`python check_alive.py bypass.js` → 看到 `BYPASS SUCCEEDED` 即通过。
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
上传的附件:
fanduel_bypass.7z
(24.23kb,39次下载)
收藏
・
6
点赞
・
6
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
AGSFGBKILO
期待更多优质内容的分享,论坛有你更精彩!
2026-6-21 22:49
周游列国
你的帖子非常有用,感谢分享!
2026-6-15 16:21
αβγδεξπ
非常支持你的观点!
2026-6-14 20:42
零_721409
感谢你的贡献,论坛因你而更加精彩!
2026-5-26 18:36
程序原
感谢你的积极参与,期待更多精彩内容!
2026-5-7 07:52
iCookiie
谢谢你的细致分析,受益匪浅!
2026-4-28 10:06
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
3
)
Imxz
雪 币:
112
活跃值:
(9435)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
754
粉丝
9
关注
私信
Imxz
2
楼
tql
2026-4-24 17:06
0
mb_iuwdkhkv
雪 币:
224
能力值:
( LV1,RANK:0 )
在线值:
发帖
4
回帖
11
粉丝
0
关注
私信
mb_iuwdkhkv
3
楼
需要定制tiktok 越狱检测绕过,能做吗? tg nitianchuhai
2026-5-6 11:49
0
小宇cc
雪 币:
162
能力值:
( LV1,RANK:0 )
在线值:
发帖
1
回帖
20
粉丝
0
关注
私信
小宇cc
4
楼
大佬的iOS 16 的 Cryptex 系统库路径,是通过什么方式扫描到的?
2026-5-9 18:59
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
zhuzhu_biu
1
8
发帖
60
回帖
70
RANK
关注
私信
他的文章
[分享]ai 复杂vm算法还原
9405
[原创][讨论] ai 绕过ios越狱检测
8471
[原创]libEnccryptor vm 还原的探索
48589
[原创] 修改binja il 恢复 tprt ollvm
4243
[分享] 无proto文件解析protobuf序列化后的内容
7417
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
xwysj
熊趴趴来
jjcat
wx_官
doduhuang
@@@
ONewTach
puppet_w
威兹曼
老臭
tubecityman
mb_znhcxoam
mb_ldbucrik
天涯共何时
CuteHacker
mb_lizfxfno
mb_hvzfxnxe
veluboii
git_97736q6285917
mb_pfuifsmo
mb_tvuiswau
mb_ovjrrmel
mb_kujsrqqv
mb_bqlkyuwo
git_48577itsfaraway
mb_cebxltxx
mb_sxlugnvv
git_94778winmodern
mb_ryglfikp
mb_tcznxyhb
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部