惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
博客园 - 司徒正美
罗磊的独立博客
D
Docker
Last Week in AI
Last Week in AI
爱范儿
爱范儿
M
MIT News - Artificial intelligence
V
V2EX
Google DeepMind News
Google DeepMind News
小众软件
小众软件
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Security Blog
Microsoft Security Blog
T
Tailwind CSS Blog
MyScale Blog
MyScale Blog
V
Visual Studio Blog
博客园 - 叶小钗
B
Blog RSS Feed
A
About on SuperTechFans
F
Fortinet All Blogs
T
The Blog of Author Tim Ferriss
Martin Fowler
Martin Fowler
P
Proofpoint News Feed

看川博客

我被 AI "蒸馏" 了 - 看川博客 距离上线只差一个软件著作权证书 - 看川博客 开发简报(2):开发者需要先进的工具 - 看川博客 比越狱更棘手!iOS vPhone 虚拟机成黑灰产新温床 - 看川博客 开发简报(1):拥抱 AI、别无选择 - 看川博客 糟糕!我的 OpenClaw 中了病毒 - 看川博客 今天你用了多少词元? - 看川博客 闪忆 LiveMem:把回忆做成会动的壁纸 - 看川博客 Swift 从 ObservableObject 迁移到 @Observable 的再讨论 视频帧截图(Video2Frame)专业视频抽帧截图工具 - 看川博客 录音记事本 - 专业安全的录音工具 App - 看川博客 Swift 从 ObservableObject 迁移到 @Observable 从 Apple Distribution Managed 证书提取备案所需公钥和 SHA1 条码助手 iOS 2.0.0 升级指南 - 看川博客 SwiftUI Menu checkmark 文本对齐 - 看川博客 C++ RTTI 信息对二进制体积和安全性的影响 - 看川博客 SwiftUI 系统颜色表查询 - 看川博客 读马伯庸《长安的荔枝》 - 看川博客 SwiftUI List selection 的使用提示 - 看川博客 PHP json_encode UTF-8 中文编码问题 - 看川博客 我为什么重新运营微信公众号 - 看川博客 使用 TrollStore(巨魔) 在非越狱设备上安装任意 IPA - 看川博客 条码助手小程序更新 - 看川博客 iOS 18 中 libdyld.dylib/dyld 的一些变化 涨粉神器?解密“抖音黑科技”云端商城 - 看川博客
iOS 越狱隐藏:从 Dopamine 到 RootHide
2026-09-14 · via 看川博客

〇、选择正确的 Dopamine 项目

讨论 Dopamine roothide 时,三个项目地址经常被混淆。它们实际指向两个不同的项目

地址实际身份RootHide越狱痕迹
github.com/opa334/Dopamine原版 Dopamine 的源码仓库(opa334 开发的 rootless 越狱)固定 /var/jb 路径,任何进程可见
ellekit.space/dopamine原版 Dopamine 的官方网站(下载入口)同上——与源码仓库是同一个项目
github.com/roothide/Dopamine2-roothideroothide 团队对 Dopamine 2.x 的魔改 fork随机 jbroot + 选择性注入

三者的关系:

opa334/Dopamine(原版源码)
        │
        │  官方网站(同一项目,托管在 ElleKit 域名下,
        │  下载按钮直接指向 opa334/Dopamine/releases)
        ▼
ellekit.space/dopamine

opa334/Dopamine(原版源码)
        │
        │  fork + 加入 RootHide(jbroot 随机化、
        │  选择性注入、用户态路径翻译)
        ▼
roothide/Dopamine2-roothide  ← 本文分析对象

结论

  1. 前两个地址是同一个项目opa334/Dopamine 的 README 明确标注 "Official website / download: https://ellekit.space/dopamine/"。官网恰好托管在 ElleKit(Dopamine 默认的 tweak 注入库)的域名下,但它分发的就是原版 IPA,不是 roothide 版本。
  2. 原版 Dopamine 不做任何隐藏:固定 /var/jb 路径全局可见、无选择性注入,第 1.1 节中 access("/var/jb", F_OK) 这一条检测对它即可生效。
  3. 只有 roothide/Dopamine2-roothide 实现了本文描述的全部隐藏机制。需要抗越狱检测能力时,必须从 roothide/Dopamine2-roothide/releases 下载,而不是官网。

一、传统越狱的检测困境

1.1 经典越狱(Classic Jailbreak)

早期越狱(如 evasi0n、unc0ver、checkm8 系列)的特征非常明显:

固定路径:
/var/jb/                          ← 越狱根目录
/var/jb/usr/lib/libsubstrate.dylib ← Substrate 注入库
/Library/MobileSubstrate/          ← Tweak 配置目录
/var/jb/usr/lib/TweakInject/       ← Tweak 注入目录

检测只需:

// 检查固定路径
if (access("/var/jb", F_OK) == 0) { /* 越狱存在 */ }
if (access("/Library/MobileSubstrate/MobileSubstrate.dylib", F_OK) == 0) { /* 越狱存在 */ }

// 检查固定进程
if (pidof("substituted") > 0) { /* 越狱存在 */ }

// 检查 mach 服务
mach_port_t port;
if (bootstrap_look_up(bootstrap_port, "cy:com.saurik.Cydia", &port) == 0) { /* 越狱存在 */ }

问题:越狱痕迹以固定路径、固定名称存在于文件系统和进程空间中,任何进程都可以检测。

1.2 无根越狱(Rootless Jailbreak)

Dopamine(1.x/2.x)、palera1n-rootless 等引入了"无根"概念。需要澄清动机:rootless 是为了适配 iOS 只读系统卷(SSV),不是为了对抗越狱检测——iOS 15+ 系统分区签名只读、无法写入,越狱文件只能放到用户可写区域,tweak 也必须重编译为 rootless 格式。

原版 Dopamine 的实际布局(源码 DOEnvironmentManager.m / DOBootstrapper.m):

真实 jbroot:/private/preboot/<UUID>/dopamine-<6位随机>/procursus/   ← 随机目录名
/var/jb → 指向 jbroot 的全局 symlink                                ← 固定存在,任何进程可见
// 原版 DOBootstrapper.m:每次安装都固定重建全局 symlink
// Remove /var/jb as it might be wrong
[self deleteSymlinkAtPath:@"/var/jb" error:&error];
...
error = [self createSymlinkAtPath:@"/var/jb" toPath:JBROOT_PATH(@"/") ...];

对检测者而言,原版 rootless Dopamine 的检测面一目了然:

  • /var/jb symlink 固定存在且全局可见:access("/var/jb", F_OK) 一条检测即可命中,根本不需要知道真实 jbroot 在哪里
  • 无选择性注入:systemhook 注入所有进程,遍历 _dyld_get_image_name() 即可发现(对原版最可靠的流程级指纹)
  • bind mount:原版用 bind mount 保护系统文件、在 /usr/lib 上做伪装(源码 DOJailbreaker.mapplyProtection,UI 日志即 "Applying Bind Mount"),getmntinfo 中可见挂载项

问题:rootless 解决的是"往哪里写文件",而不是"如何不被发现"——固定 symlink、全进程注入、bind mount 对检测者全部可见。


二、RootHide 解决了什么问题

RootHide 的核心目标是:让不同进程看到不同的系统环境

┌─────────────────────────────────────────────────┐
│                    同一台设备                      │
│                                                  │
│   被注入的进程              未被注入的进程          │
│   (如 mediaserverd)        (如银行 App)           │
│                                                  │
│   ┌──────────────────┐    ┌──────────────────┐   │
│   │ 看到越狱环境      │    │ 看到正常环境      │   │
│   │ tweak 生效        │    │ 无任何越狱痕迹    │   │
│   │ 路径被翻译        │    │ 路径正常          │   │
│   │ 文件系统被修改    │    │ 文件系统正常      │   │
│   └──────────────────┘    └──────────────────┘   │
│                                                  │
│   两者运行在同一内核上,但用户态环境完全隔离        │
└─────────────────────────────────────────────────┘

这解决了两个核心矛盾:

  1. 越狱用户想正常使用敏感 App:银行/支付/游戏类 App 不注入 tweak,看到的系统和未越狱一致
  2. 越狱功能仍然可用:系统级进程(如 mediaserverd)可以被注入,实现虚拟相机、UI 定制等功能

三、RootHide 为什么难以检测

3.1 三重防护机制

RootHide 的抗检测能力来自三个层面的协同:

第一重:随机 jbroot 路径
    ↓
    你不知道越狱文件在哪

第二重:iOS App Sandbox
    ↓
    你不能枚举父目录去找

第三重:选择性注入
    ↓
    目标 App 根本没被注入,进程空间完全干净

3.1.1 随机 jbroot 路径

从源码 generate_sandbox_extensions() 可以看到 jbroot 的实际路径格式:

// BaseBin/libjailbreak/src/roothider/common.m
snprintf(jbroot_base, sizeof(jbroot_base),
    "/private/var/containers/Bundle/Application/.jbroot-%016llX/",
    jbinfo(jbrand));
  • %016llX 是一个 64 位随机值(jbrand),每次越狱重新生成
  • 每次重新越狱,目录名都会变化
  • 目录以 .jbroot- 为前缀,伪装成隐藏文件

示例:

/private/var/containers/Bundle/Application/.jbroot-CFB53A009C72E54B/
/private/var/containers/Bundle/Application/.jbroot-A46D62A9AA6B0DCE/

3.1.2 Sandbox 阻止枚举

iOS App Sandbox 限制了 App 对文件系统的访问:

普通 App 只能看到:
/private/var/containers/Bundle/Application/
    └── <自己的UUID>/          ← 只能看到自己

不能看到:
/private/var/containers/Bundle/Application/
    ├── <其他App UUID>/
    ├── <其他App UUID>/
    └── .jbroot-XXXXXXXX/      ← 看不到这个

readdir() 对父目录会返回空或错误。App 无法枚举兄弟目录来发现 .jbroot-*

3.1.3 选择性注入

从源码 roothide_launchd___posix_spawn_prehook() 可以看到注入决策逻辑:

// BaseBin/launchdhook/src/roothider.m
bool roothideBlacklisted = isBlacklistedPath(path);
if (roothideBlacklisted)
{
    // 黑名单 App:不注入 systemhook
    // 进程完全干净
    envbuf_unsetenv(&envc, "_SafeMode");
    envbuf_unsetenv(&envc, "_MSSafeMode");
    ret = __posix_spawn_orig_wrapper(blacklistedPidp, path, desc, argv, envc);
}
else
{
    // 非黑名单 App:正常注入
    return __posix_spawn_hook(pidp, path, desc, argv, envp);
}

注入决策由 isBlacklistedPath(path) 做出(上方代码块所示)——它检查用户在 RootHide 管理器中手动配置的 RootHideConfig.plistappconfig 字典)。配置文件不存在时返回 false = 默认所有进程都被注入,只有用户手动添加的 App 才走原版 posix_spawn 不注入。

is_safe_bundle_identifier() 不参与注入决策,它的实际用途是在 xpc_hook.m 中对已被黑名单的进程隐藏越狱相关的 job/coalition 信息:

bool is_safe_bundle_identifier(const char* identifier)
{
    // com.apple.* 开头的是安全的(Apple 自家应用)
    if (string_has_prefix(identifier, "com.apple.")) {
        if (!is_apple_internal_identifier(identifier)) {
            return true;
        }
    }
    // 已知的 App Store 应用 Bundle ID
    if ([StoredAppIdentifiers containsObject:@(identifier)]) {
        return true;
    }
    return false;
}

结果:默认配置下所有 App(包括 App Store 应用)都被注入 systemhook;只有用户主动加入黑名单的 App 才不被注入,其进程空间完全干净。

3.2 不做 bind mount

与之前的越狱不同,RootHide 不在内核层做 bind mount

从实际设备的 getmntinfo 输出来看,挂载表完全干净:

apfs    /                           disk1s1
devfs   /dev                        devfs
apfs    /private/preboot            disk1s6
apfs    /private/var                disk1s2
bindfs  /usr/standalone/firmware    preboot/...  ← Apple 自己的
bindfs  /System/Library/Pearl/...   hardware/... ← Apple 自己的

没有 /usr/lib 的 bind mount,没有 .jbroot- 相关路径。

原因:RootHide 的路径翻译在用户态完成(通过 systemhook 注入后的 API hook),不需要内核层 bind mount。

3.3 用户态路径翻译

对于被注入的进程,路径翻译流程如下:

App 调用 open("/usr/lib/libsubstrate.dylib", ...)
    ↓
systemhook 拦截 open()
    ↓
判断路径是否需要翻译
    ↓
翻译为 /private/var/.../jbroot-XXX/usr/lib/libsubstrate.dylib
    ↓
调用真实的 open()
    ↓
返回文件描述符

对于未被注入的进程:

App 调用 open("/usr/lib/libsubstrate.dylib", ...)
    ↓
没有 systemhook,直接走真实系统调用
    ↓
文件不存在(因为 libsubstrate.dylib 在 jbroot 里,不在 /usr/lib/)
    ↓
返回 ENOENT

关键:未被注入的 App 调用 access("/usr/lib/libsubstrate.dylib") 会得到 ENOENT,和正常设备一致。


四、RootHide 的实现细节

4.1 整体架构

┌─────────────────────────────────────────────────────────┐
│                      内核层                              │
│  ┌──────────────────────────────────────────────────┐   │
│  │ • 内核漏洞利用(物理内存读写)                      │   │
│  │ • Trust Cache 修改(允许未签名代码执行)            │   │
│  │ • AMFI patch(代码签名验证绕过)                    │   │
│  │ • sysctl OID 交换(隐藏 developer_mode_status)    │   │
│  └──────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────┤
│                      launchd hook                        │
│  ┌──────────────────────────────────────────────────┐   │
│  │ • posix_spawn hook(进程启动拦截)                 │   │
│  │ • 黑名单判断(是否注入 systemhook)                │   │
│  │ • DYLD_INSERT_LIBRARIES 设置                      │   │
│  │ • Sandbox extension 发放                          │   │
│  └──────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────┤
│                      systemhook(被注入的进程)           │
│  ┌──────────────────────────────────────────────────┐   │
│  │ • 文件系统 API hook(open/access/stat/readlink)   │   │
│  │ • 路径翻译(真实路径 ↔ jbroot 路径)               │   │
│  │ • Substrate/Substitute 加载                       │   │
│  │ • Tweak 注入                                      │   │
│  └──────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────┤
│                      未被注入的进程                       │
│  ┌──────────────────────────────────────────────────┐   │
│  │ • 完全干净                                        │   │
│  │ • 无任何 hook                                     │   │
│  │ • 看到正常系统环境                                 │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

4.2 jbroot 路径生成

jbroot 路径在越狱激活时生成并存储在内核信息中:

// BaseBin/libjailbreak/src/jbroot.h
extern char *get_jbroot(void);

// BaseBin/libjailbreak/src/jbroot.c
char *get_jbroot(void) {
    return jbinfo(rootPath);  // 从内核信息中读取
}

路径格式固定为:

/private/var/containers/Bundle/Application/.jbroot-{jbrand}/

其中 jbrand 是 64 位随机数,每次越狱重新生成。

4.3 进程注入机制

4.3.1 systemhook 注入

对于非黑名单进程,launchd 在 spawn 时设置 DYLD_INSERT_LIBRARIES(注入决策逻辑见 §3.1.3):

// BaseBin/systemhook/src/common.c
// 注入路径格式:
// /usr/lib/systemhook-{jbrand}.dylib

envbuf_setenv(&envc, "DYLD_INSERT_LIBRARIES", HOOK_DYLIB_PATH);

systemhook 加载后,hook 文件系统 API 实现路径翻译。

4.4 路径翻译

systemhook 通过 hook 以下函数实现路径翻译:

  • open() / openat()
  • access() / faccessat()
  • stat() / lstat() / fstatat()
  • readlink() / realpath()
  • opendir() / readdir()

翻译逻辑:

// 伪代码
const char* translated_path = path;

// 检查路径是否在 jbroot 内
if (path_is_in_jbroot(path)) {
    // 翻译为真实路径
    translated_path = translate_to_real_path(path);
} else if (path_should_be_hidden(path)) {
    // 路径应该被隐藏(如 /var/jb)
    // 返回 ENOENT
    errno = ENOENT;
    return -1;
}

// 调用原始函数
return orig_open(translated_path, flags);

4.5 黑名单机制

RootHide 的黑名单由用户在 RootHide 管理器中手动配置,存储在 RootHideConfig.plistappconfig 字典中。注入决策由 isBlacklistedPath() 做出(见 §3.1.3):

  • 配置文件不存在时:返回 false → 所有 App 都被注入 systemhook(包括 App Store 应用)
  • 用户手动添加某 App 后:该 App 及其 PlugIns/扩展进程走原版 posix_spawn,不注入

is_safe_bundle_identifier()(§3.1.3)不参与注入决策,它的实际用途是在 xpc_hook.m 中对已被黑名单的进程隐藏越狱信息(job/coalition 等):

// BaseBin/libjailbreak/src/roothider/common.m
bool is_safe_bundle_identifier(const char* identifier)
{
    // Apple 自家应用(com.apple.*)
    if (string_has_prefix(identifier, "com.apple.")) {
        if (!is_apple_internal_identifier(identifier)) {
            return true;
        }
    }
    // App Store 下载的应用(通过扫描 iTunesMetadata.plist 识别)
    if ([StoredAppIdentifiers containsObject:@(identifier)]) {
        return true;
    }
    return false;
}

StoredAppIdentifiers 在越狱激活时通过扫描所有 App 容器构建(提取有 iTunesMetadata.plist 的容器的 Bundle ID)。该列表用于识别 App Store 下载的应用,但仅服务于信息隐藏逻辑,不决定注入与否。

4.6 内核级修改

虽然 RootHide 在用户态做了大量隐藏工作,但以下修改是内核级的、全局生效的:

4.6.1 sysctl OID 交换

为隐藏 developer_mode_status(开发模式标志),RootHide 将其与 launch_env_logging 的 OID 交换。此操作仅在 iOS 16.0+ 执行roothider.m:110__builtin_available(iOS 16.0, *) 守卫,iOS 15 跳过整个隐藏逻辑):

// BaseBin/libjailbreak/src/roothider.common.m
void hideDeveloperMode()
{
    // 从内核内存中读取两个 sysctl_oid 结构体
    uint64_t developer_mode_status_oidp = ksymbol(developer_mode_status) - ...;
    uint64_t launch_env_logging_oidp = ksymbol(launch_env_logging) - ...;

    // 交换 oid_name(名称)
    kwrite64(developer_mode_status_oidp + offsetof(oid_name), launch_env_logging.oid_name);
    kwrite64(launch_env_logging_oidp + offsetof(oid_name), developer_mode_status.oid_name);

    // 交换 oid_number(编号)
    kwrite32(developer_mode_status_oidp + offsetof(oid_number), launch_env_logging.oid_number);
    kwrite32(launch_env_logging_oidp + offsetof(oid_number), developer_mode_status.oid_number);

    // 注意:oid_handler(处理函数)没有交换
}

效果:

  • sysctlbyname("developer_mode_status", ...) 返回 launch_env_logging 的数据
  • sysctlbyname("launch_env_logging", ...) 返回 developer_mode_status 的数据

4.6.2 Trust Cache 修改

RootHide 通过内核漏洞修改 trust cache,允许未签名代码执行:

// BaseBin/libjailbreak/src/trustcache.c
// 将 tweak dylib 的 cdhash 加入 trust cache
trustcache_file_upload_with_uuid(tcFile, uuid);

4.6.3 AMFI Patch

修改 Apple Mobile File Integrity (AMFI) 的行为,放宽代码签名验证。


五、RootHide:反越狱检测的利器

核心结论:App 被用户加入 roothide 黑名单后,其进程内目前没有任何可靠方式能识别出 roothide 越狱——不是"特征还没找到",而是"可观测差异不存在":

  • 零写入:黑名单进程走原版 posix_spawn,不注入 dylib、不 hook 函数、csflags 与正常进程物理一致(roothider.m:378-435
  • 通道封堵:jbs 协议、cy:/lh: mach-lookup 等旁路均按 audit token 精确排除黑名单进程,行为与正常设备一致
  • 沙盒遮蔽:jbroot 目录、mount 表等全局状态虽存在,但 iOS 沙盒不允许普通 App 观测(实测 opendir / KERN_PROC 均 EPERM)

注:默认配置下所有 App(含 App Store 应用)都被注入;黑名单需用户在 RootHide APP 中手动添加,属定向对抗,此时把防线移到进程外(服务端风控、行为分析)是行业共识。