吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1858|回复: 25
收起左侧

[iOS 原创] [iOS逆向] 某相机 Frida反检测绕过 + VIP解锁 + Theos插件制作 全流程

  [复制链接]
YYYYnb 发表于 2026-3-1 22:07
本帖最后由 YYYYnb 于 2026-3-1 22:48 编辑

[iOS逆向] 某相机 Frida反检测绕过 + VIP解锁 + Theos插件制作 全流程

前言

目标App: 某相机 com.ydgn.dokacamera v1.6.5
测试环境: iPhone 13 Pro Max iOS 16.1 (Dopamine越狱) + macOS + Frida 17.6.2
难度等级: (主要难在反Frida检测的绕过)

本帖记录从 Frida 注入闪退 → 绕过检测 → 追踪请求参数 → VIP解锁 → 最终制作成 Theos 插件的完整过程。

声明:本文仅供技术学习交流,请勿用于商业用途。


1 初步分析

通过抓包发现,App 使用 Alamofire 进行网络请求,请求头中包含一个自定义的 User-Agent-Follow 字段,这个字段中的 deviceUUID 用于标识设备,也是服务器的实际校验依据,并且是完全单一校验,随便改值都能过。但是在抓包工具中每次都改值还是太麻烦了,所以先直接 frida 看他怎么生成的:

{
  "os_type": "ios",
  "Device-ID": "iPhone14,3",
  "deviceUUID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",   #uuid 格式
  "doka_version": "1.6.5",
  "device_model": "iPhone 13 Pro Max",
  "os_version": "16.1"
}

目标:搞清楚 deviceUUID 怎么生成的,以及解锁 VIP。


2 Frida 注入 — 遭遇反检测

直接用 Frida spawn 模式启动,并且写了简单的 bypass.js:

frida -U -f com.ydgn.dokacamera -l bypass.js

结果:App 闪退,闪退,闪退。。。。。。。,后面加入异常捕获处理,控制台刷出大量 access-violation 异常。

检测原理分析

通过多轮测试发现:

  1. 即使脚本为空也闪退 — 说明不是 Hook 触发的,而是检测到 Frida 本身
  2. 崩溃地址固定在 0x???fbec — 位于 App 主二进制的 __mod_init_func
  3. 该构造函数在 main() 之前由 dyld 自动执行
  4. 它位于 Execute-Only Memory(只可执行不可读写),无法 patch

App 的检测逻辑大致为:

构造函数() → 检测 Frida 特征 → 触发 access-violation → 进程崩溃

失败的尝试

方案 结果 原因
Hook strstr/strcmp 过滤 Frida 字符串 卡死 系统调用太频繁,性能崩溃
Memory.patchCode() 直接 NOP 检测代码 失败 Execute-Only 内存,无读权限
Memory.protect("rwx") 修改权限 SIGKILL 触发内核 W^X 保护

3 绕过方案:异常处理器 + 帧指针链回溯

既然不能阻止检测代码运行,那就让它崩,但是我们接住

核心思路

检测代码触发 access-violation
        ↓
Process.setExceptionHandler 捕获异常
        ↓
沿 ARM64 帧指针链 (FP/x29) 向上回溯
        ↓
找到 dyld 中的返回地址
        ↓
修改 PC 跳转到 dyld → 跳过整个检测函数
        ↓
App 正常启动 ✓

关键代码

Process.setExceptionHandler(function(details) {
    // 只处理 access-violation
    if (details.type !== "access-violation") return false;

    var ctx = details.context;
    var fp = ctx.x29;   // ARM64 帧指针
    var mods = Process.enumerateModules();

    // 沿帧指针链向上回溯,找 dyld 的返回地址
    for (var i = 0; i < 20; i++) {
        if (fp.isNull() || fp.compare(ptr(0x1000)) < 0) break;

        try {
            var saved_fp = fp.readPointer();
            var saved_lr = fp.add(8).readPointer();

            // 检查返回地址是否在某个已知模块内
            for (var j = 0; j < mods.length; j++) {
                var base = mods[j].base;
                var end = base.add(mods[j].size);
                if (saved_lr.compare(base) >= 0 && saved_lr.compare(end) < 0) {
                    // 跳转到 dyld 的返回地址,跳过检测
                    ctx.pc = saved_lr;
                    ctx.x29 = saved_fp;
                    ctx.x0 = ptr(0);
                    return true; // 已处理异常
                }
            }
            fp = saved_fp;
        } catch (_) { break; }
    }
    return false;
});

为什么这样做?

ARM64 的栈帧结构:

高地址
┌──────────────┐
│ 返回地址 (LR) │ ← FP + 8
├──────────────┤
│ 上一级 FP     │ ← FP (x29)
├──────────────┤
│ 局部变量      │
└──────────────┘
低地址

沿 FP 链一直往上追,找到第一个属于 dyld 模块的返回地址,然后让 PC 跳过去——相当于告诉 CPU:这个函数已经执行完了,回去吧。


4 补充防护:Anti-Debug + 隐藏 Frida 痕迹

绕过异常后,还需要防止 App 的其他检测手段:

Anti-Debug(反调试)

// 1. ptrace — 阻止 PT_DENY_ATTACH
Interceptor.attach(Module.findExportByName(null, "ptrace"), {
    onEnter: function(args) {
        if (args[0].toInt32() === 31) args[0] = ptr(0); // PT_DENY_ATTACH = 31
    }
});

// 2. sysctl — 清除 P_TRACED 标志位
Interceptor.attach(Module.findExportByName(null, "sysctl"), {
    onLeave: function(retval) {
        // 在 kinfo_proc 结构体中清除调试标志
    }
});

// 3. getppid — 返回 1 (launchd),伪装非调试状态
Interceptor.attach(Module.findExportByName(null, "getppid"), {
    onLeave: function(retval) { retval.replace(ptr(1)); }
});

隐藏 Frida 痕迹

// 重命名 frida-agent.dylib
Interceptor.attach(Module.findExportByName(null, "_dyld_get_image_name"), {
    onLeave: function(retval) {
        var name = retval.readUtf8String();
        if (name && name.indexOf("frida") !== -1) {
            retval.replace(Memory.allocUtf8String("/usr/lib/libSystem.B.dylib"));
        }
    }
});

阻止进程自杀

Hook exitabortkill 等函数,阻止 App 自我终止。


5 追踪请求参数生成

成功进入 App 后,用 Frida Hook 追踪 User-Agent-Follow 的生成过程。

核心 Hook 点

// 监控 HTTP 请求头设置
Interceptor.attach(
    ObjC.classes.NSMutableURLRequest["- setAllHTTPHeaderFields:"].implementation, {
    onEnter: function(args) {
        var headers = new ObjC.Object(args[2]);
        if (headers.toString().indexOf("User-Agent-Follow") !== -1) {
            console.log("捕获到请求头:", headers.toString());
        }
    }
});

发现

字段 来源
device_model Device-ID 硬编码映射表(iPhone14,3iPhone 13 Pro Max
os_version UIDevice.systemVersion
Device-ID utsname().machine
doka_version Info.plistCFBundleShortVersionString
deviceUUID KeyChain 钥匙串存储(见下文)

deviceUUID 来源分析

通过 Hook SecItemCopyMatching 发现:

SecItemCopyMatching({
    acct = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",   // 账户名 (MD5 哈希)
    class = "genp",                                // 通用密码
    svce = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",    // 服务名
    pdmn = "ck",                                   // 保护域
})

关键发现UIDevice.identifierForVendor 从未被调用!UUID 是 App 首次安装时随机生成并存入 KeyChain 的。由于 KeyChain 数据在卸载后仍保留,即使重装 App UUID 也不变——实现了设备级持久标识。


6 VIP 解锁

只是为了看着舒服,谁不想装一波呢?实际每次构图请求修改--deviceUUID--即可。
当然也是为了防止每次启动只有五次 ai 构图次数,用完又得重启 app,有了 vip 标志后,我们除开服务器的校验, app 本身的使用限制就小了很多。

方法1:修改 JSON 响应

Hook NSJSONSerialization 修改服务器返回的 VIP 状态:

Interceptor.attach(
    ObjC.classes.NSJSONSerialization["+ JSONObjectWithData:options:error:"].implementation, {
    onLeave: function(retval) {
        var result = new ObjC.Object(retval);
        if (result.toString().indexOf("is_vip") === -1) return;

        var mutableData = new ObjC.Object(result.objectForKey_("data")).mutableCopy();
        var mutableRoot = result.mutableCopy();
        mutableData.setObject_forKey_(ObjC.classes.NSNumber.numberWithBool_(true), "is_vip");
        mutableData.setObject_forKey_(
            ObjC.classes.NSString.stringWithString_("2099-12-31 23:59:59"), "expire_time");
        mutableData.setObject_forKey_(ObjC.classes.NSNumber.numberWithInt_(9999), "remaining_count");
        mutableRoot.setObject_forKey_(mutableData, "data");
        retval.replace(mutableRoot);
    }
});

结果:只有次数修改成功了,vip 和时间都没有成功显示。

方法2:覆写本地缓存(关键!)

只改 JSON 不够!App 还在 NSUserDefaults 中缓存 VIP 状态。

通过诊断监控发现了精确的 key:

[Defaults] objectForKey("VipManager.expiryDate") = 0001-01-01 00:00:00 +0000   ← 已过期的时间
[Defaults] objectForKey("VipManager.originalTransactionId") = nil               ← 无购买记录凭证
[Defaults] integerForKey("VipManager.freeUseCount") = 0                         ← 无次数

精准覆写这三个 key:

Interceptor.attach(ObjC.classes.NSUserDefaults["- objectForKey:"].implementation, {
    onEnter: function(args) { this.key = new ObjC.Object(args[2]).toString(); },
    onLeave: function(retval) {
        if (this.key === "VipManager.expiryDate") {
            retval.replace(ObjC.classes.NSDate.dateWithTimeIntervalSince1970_(4102444799.0));
        }
        if (this.key === "VipManager.originalTransactionId") {
            retval.replace(ObjC.classes.NSString.stringWithString_("530000123456789"));
        }
    }
});

7 UUID 随机化

每次请求都随机生成全新设备身份,确保每次 AI 构图使用不同的 UUID,无限次数:

function newRandomDevice() {
    var device = pickRandom(DEVICES); // 15 款 iPhone 随机选
    return {
        deviceUUID:  randomUUID(),          // 全新 UUID
        deviceID:    device.id,              // 如 iPhone15,2
        deviceModel: device.model,           // 如 iPhone 14 Pro
        osVersion:   pickRandom(device.versions) // 匹配的 iOS 版本
    };
}

setAllHTTPHeaderFields: 中每次都调用 newRandomDevice() 替换 JSON 字段。每个请求都是一台"新设备",服务器永远不会限制次数。


setAllHTTPHeaderFields: 中替换 JSON 字段,每次启动身份不同但同一次运行内保持一致。


8 制作 Theos 插件

最终将所有功能转换为 Theos Tweak,安装后无需 Frida。

项目结构

dokavip/
├── Makefile          # 构建配置
├── control           # Debian 包信息
├── DokaVip.plist     # 注入过滤(只对目标App生效)
└── Tweak.x           # 核心代码(Logos 语法)

Tweak.x 核心片段

VIP 解锁(Logos 语法比 Frida 简洁很多):

%hook NSJSONSerialization
+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error {
    id result = %orig;
    if (![result isKindOfClass:[NSDictionary class]]) return result;

    NSDictionary *dataDict = result[@"data"];
    if (!dataDict[@"is_vip"]) return result;

    NSMutableDictionary *mData = [dataDict mutableCopy];
    NSMutableDictionary *mRoot = [result mutableCopy];
    mData[@"is_vip"] = @YES;
    mData[@"expire_time"] = @"2099-12-31 23:59:59";
    mData[@"remaining_count"] = @9999;
    mRoot[@"data"] = mData;
    return mRoot;
}
%end

Anti-Debug(用 MSHookFunction Hook C 函数):

%ctor {
    MSHookFunction(dlsym(RTLD_DEFAULT, "ptrace"),  (void *)hook_ptrace,  (void **)&orig_ptrace);
    MSHookFunction(dlsym(RTLD_DEFAULT, "sysctl"),  (void *)hook_sysctl,  (void **)&orig_sysctl);
    MSHookFunction(dlsym(RTLD_DEFAULT, "getppid"), (void *)hook_getppid, (void **)&orig_getppid);
}

UUID 随机化(每次请求新身份):

%hook NSMutableURLRequest
- (void)setAllHTTPHeaderFields:(NSDictionary *)fields {
    if (!fields[@"User-Agent-Follow"]) { %orig; return; }

    // 每次请求生成全新设备身份
    NSDictionary *dev = newRandomDevice();

    // 解析 JSON → 替换 → 序列化回去
    NSMutableDictionary *parsed = [...];
    parsed[@"deviceUUID"]   = dev[@"deviceUUID"];   // 全新随机 UUID
    parsed[@"Device-ID"]    = dev[@"Device-ID"];    // 随机机型标识
    parsed[@"device_model"] = dev[@"device_model"]; // 对应机型名
    parsed[@"os_version"]   = dev[@"os_version"];   // 匹配的 iOS 版本
    %orig(mutableFields);
}
%end

构建与安装

# 构建
cd dokavip && make package

# 安装直接通过巨魔注入对应app即可

9 总结

技术要点

  1. 反 Frida 检测绕过:异常处理器 + ARM64 帧指针链回溯,跳过 Execute-Only Memory 中的检测构造函数
  2. 请求参数追踪:Hook SecItemCopyMatching 发现 UUID 存储在 KeyChain(非 identifierForVendor
  3. VIP 解锁:JSON 响应修改 + NSUserDefaults VipManager.* 本地缓存覆写(两层都要改)
  4. UUID 随机化:在 HTTP Header 设置阶段替换 JSON 字段
  5. 插件化:Frida JS → Theos Logos 转换,注意异常处理器是 Frida 独有 API,在 Substrate 注入模式下不需要

踩坑记录

教训
Hook strstr/strcmp 导致卡死 高频系统函数不能无差别 Hook
Memory.protect("rwx") 被 SIGKILL iOS 内核 W^X 保护不可绕过
只改 JSON 没改 UserDefaults App 有本地缓存,两层都要改
setTimeout(1000) 第一个请求没拦到 setImmediate 保证 Hook 在首请求前安装
NSDateFormattersetImmediate 中崩溃 NSDate.dateWithTimeIntervalSince1970: 替代
Hook 全部 boolForKey: 导致 App 崩溃 NSUserDefaults 调用极其频繁,只能精准匹配 key

工具链:Frida 17.6.2 + Theos + Dopamine 越狱
如有问题欢迎交流讨论 🙏
本文仅供技术学习交流,请勿用于商业用途。

结果展示



免费评分

参与人数 7吾爱币 +13 热心值 +6 收起 理由
正己 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
ipcon + 1 + 1 用心讨论,共获提升!
smileli + 1 + 1 用心讨论,共获提升!
JettyCatR3C0 + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
buluo533 + 1 + 1 用心讨论,共获提升!
江南小虫虫 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

小能维尼 发表于 2026-3-2 17:29
面对IOS这种封闭系统,用户不越狱,根本无法破解吧
那么软件厂商,为什么还要“复杂”的加密呢?
我不知道什么是复杂,什么是简单
在本篇,算不算是有意的加密呢?
babylonmax 发表于 2026-3-2 11:29
JackFlyD 发表于 2026-3-2 13:42
JackyLan 发表于 2026-3-2 14:11
感谢大佬分享详细的分析过程。一起学习
eagleangle 发表于 2026-3-2 14:16
大佬可以出一期Frida 17.6.2 + Theos + Dopamine 越狱调试环境搭建和使用的详细教程吗?
l145191039l 发表于 2026-3-2 15:51
很好 小白需要成品
cksincerely 发表于 2026-3-2 15:55
怎么用呀,膜拜
shyjeai 发表于 2026-3-2 15:56
感谢分享。~逆向要是有问题的话,他们还能给保修嘛,哈哈
gblw 发表于 2026-3-2 16:41
shyjeai 发表于 2026-3-2 15:56
感谢分享。~逆向要是有问题的话,他们还能给保修嘛,哈哈

什么保修?手机吗?
Luckyboy1188 发表于 2026-3-2 17:08
感谢楼主的教学
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-3-18 17:29

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表