四路由特权接缝与容器原位手术:ichitaso 的 Regitela 如何打通 iOS 26/27 全链路越权文件管理与系统调校
在 iOS 26 与 iOS 27 交替的沙盒攻防窗口期,围绕 worse_query 诞生的工具层出不穷,但绝大多数演示工程都停留在“写死一个路径、硬塞几个键值”的 PoC 阶段。最近我拉取并逐行审计了知名越狱开发者 ichitaso 开源的 Regitela(v1.0.3,commit c8ce4c2)全部源码。从表面看,它只是一款提供“监管模式(Supervised Mode)”、“地区切换(14 个国家/地区)”、“充电上限”、“息屏显示(AOD)”、“轻点唤醒”和系统容器浏览的轻量级工具箱;但剖开工程外壳后,我看到了一套在移动端极为克制却又极其成熟的跨特权域路由架构。
为了同时兼容无越狱 iOS 27、半封闭的 iOS 26.0.x 以及 Rootless 无根越狱设备,Regitela 在 v1.0.3 中抽离出了一个可插拔的 SystemFileAccess 协议接缝,把越狱直通、独立许可的 darksword 内核读写后端、geod class-12 partDomain 路径遍历以及经过二次改造的 worse_query 符号链接逃逸统一收口进同一套句柄空间;同时在写入层放弃了会触发父目录权限校验的原子写,改用 POSIX O_WRONLY | O_TRUNC 原位覆写与硬件防呆校验。这篇文章,我将结合仓库中的每一个核心源码文件,完整还原它的技术链路与工程细节。
[!NOTE] 源码审计基线说明
本文所有符号名称、协议参数、十六进制标志位与异常处理逻辑,均基于我对ichitaso/Regitela仓库v1.0.3(commitc8ce4c2)全部 Swift、Objective-C、C 源码以及Makefile、Regitela.xcodeproj/project.pbxproj、Localizable.xcstrings的逐行审查。
一、从单一 PoC 到五层混合引擎:Regitela 的架构全貌
如果只用一句话概括 Regitela 与原始 worse_query PoC 的本质区别,那就是它用“零第三方库依赖的五层原生系统堆栈”,把一个脆弱的漏洞验证脚本重构成了一套可长期维护的系统工具平台。
通读 Regitela/ 目录下的 21 个核心源文件后,我将它的整体技术堆栈梳理为如下五层垂直结构:
从上到下,每一层都踩在 iOS 系统最贴近底层的边界上:
- L5 交互壳层与双管道日志终端 (
ContentView.swift/Unlock.swift/Console.swift):全应用原生支持 17 种语言(Localizable.xcstrings共 113 条本地化词条)。极有意思的是Console.swift的设计——为了在界面上实时显示 C / Objective-C 漏洞层通过printf打出的寄存器偏移与错误码,作者先忽略SIGPIPE,再用pipe()配合dup2(..., STDOUT_FILENO)接管标准输出流并关闭缓冲(setvbuf(stdout, nil, _IONBF, 0)),同时保留originalOut = dup(STDOUT_FILENO)双向回写到 Xcode 控制台。这样一来,无论是 Swift 层的print()还是底层内核利用的 C 级日志,都会同步流进首页Log标签页与UnlockView的黑底绿字终端框里。 - L4 四大系统调校与文件管理器 (
Supervised.swift/Region.swift/GestaltFeature.swift/FileExplorer.swift):封装高危系统配置修改逻辑,引入重启状态对照、备份快照以及4,000,000字节纯文本安全编辑上限。 - L3 可插拔权限接缝与原位写入引擎 (
AccessBackend.swift/Escape.swift/ContainerPlist.swift):整套工程的心脏。负责将四种截然不同的越权通道抽象成统一的Int64句柄生命周期,并由SandboxSession的deinit自动释放。 - L2 容器盲扫、LaunchServices 私有 SPI 与无头文件压缩桥接 (
pathleak.m/AppNames.m/Archive.m):在没有父目录列举权限时靠fsgetpath()暴力扫 inode,再用私有接口还原 App 名称与40pt圆角图标,甚至为系统 SDK 中缺失头文件的libarchive.tbd手写了 C ABI 声明。 - L1 越权原语与双模打包底座 (
SandboxEscape.m/KernelBackend.m/Makefile):支持 Theos + Xcode 26 单命令产出两套形态——面向常规侧载的.ipa与面向 Rootless 无根越狱环境的.deb。
二、核心突破:四级特权路由与句柄空间隔离机制
在早期 v1.0.0 ~ v1.0.2 中,Regitela 的各项系统调校只能在 iOS 27 上生效,iOS 26 用户一打开就只能看到容器探测报错。原因很简单:苹果在 iOS 26 与 iOS 27 上的 containermanagerd 沙盒授权边界并不相同——在 iOS 26 上,基于 App Group(class 7)的 worse_query 根本无法穿透到 SystemGroup 系统组容器。
为了在 v1.0.3 中攻克 iOS 26.0.x 的限制,ichitaso 在 Escape.swift、AccessBackend.swift、KernelBackend.m 和 SandboxEscape.m 中连续落下四枚关键棋子,构成了一条严谨的四级特权决策链:
我把这四级路由的底层实现与边界条件逐一拆解如下:
2.1 Tier 0:无沙盒 Rootless 越狱直通 (isUnsandboxed)
在 Escape.swift 初始化阶段,程序首先探测 FileManager.default.contentsOfDirectory(atPath: "/private/var/containers/Shared/SystemGroup")。如果用户通过 Makefile 打出的 .deb 包安装到了 Rootless 越狱环境(/var/jb/Applications/Regitela.app),由于打包阶段调用 ldid -SRegitela/jailbreak.entitlements 注入了四项核心特权:
com.apple.private.security.no-containercom.apple.private.security.storage.AppDataContainerscom.apple.private.security.no-sandboxplatform-application
此时进程本身已经脱离沙盒约束,isUnsandboxed 为 true。issueSandboxExtension(for:) 会直接返回哨兵句柄 unsandboxedHandle = Int64.max,全站所有 Browse、Containers 与 Tweaks 均无需触发任何漏洞即可直接读写系统路径。
2.2 Tier 1:可插拔内核后端 KernelAccess 与硬件防 Panic 名单
为什么在引入了 geod 遍历路由之后,Regitela 依然要在 iOS 26.0.x 上接入 darksword 内核读写后端(基于 wh1te4ever 的 darksword-kexploit-fun 与 opa334 的 XPF/ChOma)?答案直接写在 KernelAccess.swift 第 11 行注释里:
“geod is read-only here: ConfigurationProfiles EPERMs outright and MobileGestalt writes are denied, so claim both groups for the kernel.”
也就是说,在 iOS 26 上,即便靠 geod_query() 拿到了系统组路径的访问扩展,在面对 systemgroup.com.apple.configurationprofiles(监管模式)时写入会直接报错 EPERM,面对 systemgroup.com.apple.mobilegestaltcache(区域与硬件开关)同样被拒绝写入!
于是 ichitaso 在 AccessBackend.swift 中设计了一个极干净的 SystemFileAccess 协议接缝,并让公开仓库通过 #if __has_include("kexploit/offsets.h") 实现源码级解耦:闭源或独立授权的内核利用文件存放在 .gitignore 忽略的 /kernel/ 目录中;如果公开编译时不带该目录,整个 KernelBackend.m 自动降级编译为纯 false 桩函数,丝毫不影响 iOS 27 用户构建 IPA。
当内核后端编入二进制后,Regitela 还做了两道令人称道的工程风控:
- 严格的 SoC 芯片黑名单与系统版本锁 (
KernelBackend.m:15-43):regitela_kernel_available()不仅把系统版本严格锁定在17.0 <= systemVersion < 26.1,还通过sysctlbyname("hw.cpufamily", ...)主动识别并拒绝 A18(0x204526d0)、A18 Pro(0x75d4acb9)和 M4(0x6f5129ac)设备,防止在这些机型上强跑 exploit 引发内核 Panic 重启。 - Dopamine 风格显式解锁门禁 (
Unlock.swift&KernelAccess.swift):即便是支持的 iOS 26.0.x 机型,点击任何一个 Tweak 也绝不会在后台静默触发内核漏洞。KernelAccess.acquire(_:)在未解锁时只会硬性返回-6;只有当用户主动进入首页的Unlock Kernel Access界面点击解锁按钮后,后台队列才会执行一次且仅执行一次的kexploit_opa334()+patch_sandbox_ext()(带g_attempted幂等锁),把当前进程沙盒永久修补为可读写状态,并返回隔离区句柄kernelHandleBase = 0x5000000000。
2.3 Tier 2:iOS 26 专用 geod Class-12 路径遍历 (geod_query)
对于不需要写保护目录、但在 iOS 26 上又无法被 App Group 触及的 /private/var/containers/Shared/SystemGroup 常规读取场景,Escape.swift 会路由给 SandboxEscape.m 中的 geod_query()(源自 0xjohnny 的 Geod-MCM-PoC)。
它的构造方式极其精巧:
- 将查询类别设为
12(system-data container),目标标识符指定为"com.apple.geod",Part 设为3(即Library/Caches),操作标志位为0x0000008100000000ULL。 - 将目标路径剥离
/private/var/或/var/前缀后,在前面硬拼六级父目录回溯../../../../../../,传入container_query_operation_set_part_domain()——正好从com.apple.geod/Library/Caches穿回/private/var根目录再向下精准落入目标路径。 - 最关键的生命周期差异:源码注释明确指出,iOS 26 上该通道没有可分离的沙盒令牌(No detachable token),如果像
worse_query那样拿完 Token 就释放容器查询对象,授权会瞬间失效!因此geod_query()调用container_object_copy()复制对象并执行container_object_sandbox_extension_activate(object, false),随后将活体对象指针寄存进一个容量为 32 的静态指针槽位数组geod_objects[32],向外返回0x4000000000 + slot编码句柄,直到releaseSandboxExtension()识别到句柄区间后调用geod_release()才真正执行container_object_free()。
2.4 Tier 3:worse_query Class-7 符号链接与“为什么 Sideloadly 必挂”
当走到底层兜底分支 worse_query() 时,我发现它解释了一个困扰很多小白用户的玄学问题:为什么同一份 Regitela IPA,用 Impactor 安装就能用,换 Sideloadly 安装就一定报错 no accessible app group?
秘密就在 SandboxEscape.m:250-280 的 worse_query_group() 里:
- Regitela 并不依赖写死的 App Group 名称,而是在运行时通过
dlopen加载Security.framework,调用私有 C 函数SecTaskCreateFromSelf与SecTaskCopyValueForEntitlement(..., CFSTR("com.apple.security.application-groups"), NULL),动态自省自身二进制签名里实际被授予了哪个 App Group(再辅以"group.com.ichitaso.regitela"兜底)。 - Sideloadly 在免费个人证书重签名时会剥离
com.apple.security.application-groups权限,导致worse_query_group()返回nil,进而连临时符号链接的宿主目录都找不到。而 Impactor(或 Xcode 勾选对应 Team 的 App Group)会保留或重写为当前开发者账号可用的任意 App Group,Regitela 都能自动适配。 - 拿到合法
groupRoot后,worse_query()删除其中的Library/Caches并替换为指向target的符号链接,随后向libsystem_containermanager.dylib发起class = 7、part = 3的查询,消费container_copy_sandbox_token返回的令牌并立刻clean_symlink(group)销毁痕迹。
[!TIP] 源码里一个极易被一眼扫过的十六进制标志位细节
我在对比Escape.swift第 47 行与SandboxEscape.m第 114–118 行时注意到一个耐人寻味的细节:Escape.swift专门判断了当系统主版本等于 26 且目标包含/var/mobile/Containers/Shared/AppGroup时传入alt_flags = true。表面看if (alt_flags)与else两行代码长度完全一致,但仔细逐位比对十六进制数:true分支是0x0000000800000000ULL(bit 35),而false分支是0x0000008000000000ULL(bit 39),整整差了 4 个比特位!这也印证了在 iOS 26 上访问共享 AppGroup 容器时,containermanagerd的标志位校验已经与通用容器路径产生了微妙分歧。
三、容器盲扫与界面分离:pathleak 如何绕过“父目录不可读”?
拿到沙盒逃逸原语后,为什么 Regitela 的首页把目录拆分成了 Browse 和 Containers 两个完全不同的板块?
答案藏在 ContentView.swift 第 40–55 行的预设权限矩阵 presetPaths 中:
| 预设系统路径 | iOS 27 专属 | 父目录可直接拿扩展 (parentAccess) |
首页归属板块 |
|---|---|---|---|
/private/var/containers/Data/System/ |
是 | 是 | Browse(直接浏览) |
/private/var/mobile/Containers/Shared/AppGroup/ |
否 | 是 | Browse(直接浏览) |
/private/var/containers/Shared/SystemGroup/ |
是 | 否 | Containers(pathleak 扫描) |
/private/var/mobile/Containers/Data/Application/ |
否 | 否 | Containers(pathleak 扫描) |
/private/var/mobile/Containers/Data/InternalDaemon/ |
否 | 否 | Containers(pathleak 扫描) |
/private/var/mobile/Containers/Data/PluginKitPlugin/ |
否 | 否 | Containers(pathleak 扫描) |
在非越狱环境下,对于 parentAccess == false 的容器父目录(例如 Data/Application/),系统拒绝针对父目录本身签发扩展令牌,只允许对里面的单个 UUID 子目录签发扩展——但你连父目录都 ls 不出来,又怎么知道这些随机生成的 UUID 目录名是什么?
Regitela 集成的 pathleak.m 用了一招极其“朴素却暴力有效”的 XNU 系统调用绕过方案:
- 锚定文件系统上界 (
ContainerBrowser.swift:114-127):先在应用自己的temporaryDirectory创建一个临时文件.baseline_<UUID>,用stat()读取它的st_ino作为baselineInode——同一文件系统上所有已存在的容器目录 inode 必然小于这个刚刚分配的新 inode。 - 获取卷标识并线性爆破 (
pathleak.m:31-55):对目标父路径调用statfs()拿到fsid_t,随后从inode = 1一直遍历到baselineInode,逐个调用内核私有/半公开路径反查 syscallfsgetpath(buffer, PATH_MAX, &fsid, inode)。 - 精准匹配直系子目录并补全图标:只保留以目标前缀开头、且之后不再包含第二个
/的直接子目录路径;对命中的每一个 UUID 目录临时申请扩展读取扩展属性com.apple.containermanager.identifier(getxattr),再结合AppNames.m里的三层回退(LSApplicationWorkspace→LSApplicationRecord→LSApplicationProxy+Info.plist)以及[UIImage _applicationIconImageForBundleIdentifier:format:scale:],把原本冰冷的 UUID 目录列表变成带有40pt圆角真实图标和中文应用名的可视化容器清单(结果在进程内缓存于scanCache)。
四、系统配置的“原位手术”:绕开原子写、保护键位不变量与硬件防呆
如果说前两层解决的是“进得去”,那么 ContainerPlist.swift、Supervised.swift、Region.swift 和 GestaltFeature.swift 展示的则是如何在极其受限的权限下“改得稳、不炸机、可无损回滚”。我将这套跨泳道安全协同流程绘制如下:
4.1 为什么这里绝对不能用 Swift 的 .atomic 写入?
在日常 Swift 开发中,写文件习惯用 data.write(to: url, options: .atomic)。但在 ContainerPlist.swift 第 39–51 行,作者特意留下了这样一段注释和底层实现:
// In place: an atomic write would also need write access to the directory
func write(_ dict: [String: Any], format: PropertyListSerialization.PropertyListFormat) throws {
let data = try PropertyListSerialization.data(fromPropertyList: dict, format: format, options: 0)
let fd = open(path, O_WRONLY | O_TRUNC)
if fd < 0 { throw posixError("open") }
defer { close(fd) }
let written = data.withUnsafeBytes { buffer in
Darwin.write(fd, buffer.baseAddress, buffer.count)
}
if written < 0 { throw posixError("write") }
}
原因非常硬核:.atomic 写入的底层机制是先在同级目录下创建一个临时文件,写完后再执行 rename() 替换原文件——这要求进程同时拥有父目录的写权限。而在沙盒逃逸或受限文件扩展场景下,目标目录往往只开放了已有文件本身的覆写权限而禁止新建/重命名目录项;此外在 Supervised.swift 中,整颗容器根目录的扩展甚至写不进 ConfigurationProfiles,因此必须把扩展精确申请到 .../Library/ConfigurationProfiles 子目录,再通过 open(path, O_WRONLY | O_TRUNC) 打开文件描述符直接原位截断覆写,同时原封不动保留读出时的 PropertyListFormat(二进制或 XML 格式)。一旦写入失败,diagnostics() 还会立刻打印出目标文件与父目录的 uid、gid、mode、st_flags 标志位以及当前生效的 extension class,把排错成本降到最低。
4.2 iOS 27 MobileGestalt 的一条隐形红线:只改已有 Key,绝不盲目新增
在 Region.swift 中,Regitela 支持一键切换 14 个国家与地区的销售区域(涵盖 US LL/A、JP J/A、CN CH/A、HK ZP/A、TW TA/A、KR KH/A、GB B/A、DE D/A、FR FN/A、CA C/A、AU X/A、SG ZA/A、IN HN/A 与 VN VN/A)。
但在修改 CacheExtra 字典里的 RegionInfo 时,作者踩过一个极深的 iOS 27 坑:苹果不同硬件设备上的 RegionInfo 可能存储在 zHeENZu+wbg7PUprwNwBWg 或 yK+xavymRGZ3xWc1tb8XDg 这两个混淆键之一。Region.swift:33-39 明确强制要求:
- 只通过
regionKeys.first { extra[$0] != nil }更新设备原本就存在的那一个键; - 严禁把原本不存在的那个键一并写入——因为在 iOS 27 上,往
CacheExtra塞入一个原本没有的RegionInfo键,会直接触发系统在下次开机时判定缓存异常并整份重建com.apple.MobileGestalt.plist,让所有修改瞬间归零。
与此同时,Region.swift 还在首次修改前自动把未经触碰的完整原始 plist 字节备份到应用沙盒的 Documents/MobileGestalt-original.plist,并在 UserDefaults 中用 "" 哨兵值记录每个键修改前的原始状态(区分“原值”与“原不存在”),甚至用 sysctl([CTL_KERN, KERN_BOOTTIME]) 读取设备上次开机时间与 plist 的 modificationDate 同屏对比,让用户一眼确认改完配置后究竟有没有重启生效。
4.3 链接 -lMobileGestalt 的自隐藏防呆机制
同样克制的工程素养也体现在 GestaltFeature.swift 管理的三个隐藏硬件特性上:
- 充电上限 (
Charge Limit):37NVydb//GP/GrhuTN+exg(DeviceSupports80ChargeLimit) - 息屏显示 (
Always On Display):2OOJf1VhaM7NxfRok3HbWQ(DeviceSupportsAlwaysOnDisplay)+j8/Omm6s1lsmTDFsXjsBfA(DeviceSupportsAlwaysOnTime) - 轻点唤醒 (
Tap to Wake):yZf3GTRMGTuwSV/lD7Cagw(DeviceSupportsTapToWake)
因为 project.pbxproj 在链接参数里直接加上了 -lMobileGestalt,应用启动时可直接调用系统原生的 MGGetBoolAnswer() 函数。GestaltFeature.isVisible 据此做了一层极其聪明的过滤:如果当前 iPhone 原生就已经支持某项能力,且不是本应用自己加上去的,菜单里直接把该条目隐藏;而只有通过本应用写入的开关(记录了 addedDefaultsKey),才允许点击 Disable 删除对应键位,彻底杜绝老机型误删原生硬件能力键位的风险。
五、文件管理、无头文件 libarchive.tbd 桥接与双模构建链路
除了几项热门系统调校,Regitela 的 Browse 与 Advanced 模块本身就是一套五脏俱全的系统级文件管理器(支持新建、重命名、剪贴板跨目录复制/移动、4MB 内 UTF-8 文本直编、ShareLink 导出与 ZIP 压缩解压)。
我在翻阅 Archive.m 时发现了一个很体现老牌越狱开发者功底的小技巧:iOS SDK 虽然自带了 libarchive.tbd 动态库,却刻意没有提供对应的 C 头文件。ichitaso 没有为了一个解压缩功能去引入笨重的 SwiftPM 第三方压缩库,而是直接在 Archive.m 顶部前向声明了 struct archive 与 struct archive_entry 的 C 函数原型,直接链接系统原生 libarchive:
- 解压时强制开启
ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_SECURE_SYMLINKS | ARCHIVE_EXTRACT_SECURE_NODOTDOT标志位,自动跳过 macOS 生成的__MACOSX/资源分支目录,并从内核库层面免疫“Zip Slip”跨目录覆盖漏洞与恶意符号链接穿透; - 在 UI 交互层,
extract()会先将压缩包解入.extract-<UUID>临时沙盒暂存目录,若内含单个顶层项目则直接提升移出,若含多个散落文件则自动包裹进与压缩包同名的文件夹中,保持目标系统目录整洁。
在工程交付侧,Makefile 将 Theos 与 Xcode 26 结合得天衣无缝:一行 make package 既能打好供普通用户通过 Impactor 侧载的 Regitela_<version>.ipa,又能紧接着复制 Staging 产物、换用 jailbreak.entitlements 重新 ldid 签名并打包成带 uicache 安装/卸载脚本的 Rootless com.ichitaso.regitela_<version>_iphoneos-arm64.deb。
六、横向对比与工程启发
为了更直观地看清 Regitela 在同类工具中的技术定位,我将它的设计取舍总结如下:
| 维度 | 早期 worse_query 演示工程 |
Regitela (v1.0.3) 工程化实践 |
|---|---|---|
| 特权路由模型 | 单一 class 7 App Group 符号链接 |
四级混合接缝(无沙盒直通 → darksword 内核 → geod class-12 → worse_query class-7) |
| 系统版本兼容 | 主要面向单一越权窗口 | 覆盖 iOS 27.0-27.2(用户态完整调校与浏览)、iOS 26.0.x(内核后端补齐配置写入)与 iOS 17+ Rootless |
| 文件写入语义 | 常见 Swift 原生写操作,受限于目录权限 | 针对受保护系统容器采用 **POSIX `O_WRONLY |
| MobileGestalt 防护 | 直接覆写字典,易破坏新系统缓存或误删原键 | 冷备原始 plist + RegionInfo 单已有键不变量 + MGGetBoolAnswer 自隐藏防误删 |
| 安全解耦机制 | 漏洞代码与业务混写 | AccessBackend 协议 + #if __has_include 编译期剥离闭源内核件,兼顾合规开源与可拓展性 |
通读完 ichitaso/Regitela 的代码,我最大的收获并不是又记住了一两个 MobileGestalt 的混淆 Key,而是看到了老牌 iOS 底层开发者在面对系统不断收紧沙盒边界时的工程克制与模块化分层思维:不把所有鸡蛋放进单个漏洞篮子里,而是用 SystemFileAccess 这样的极简接口把特权获取与业务逻辑彻底解耦;不依赖庞大的外部依赖,而是把 fsgetpath、libarchive.tbd、SecTask 和 pipe/dup2 这些系统自带原语组合到极致。
如果你正在研究 iOS 26/27 下的 containermanagerd 沙盒机制、MobileGestalt 运行时行为,或者想学习如何把一个底层 PoC 工程化为安全可控的系统工具,Regitela 的源码绝对值得逐行研读。
[!WARNING] 实战提醒
修改系统容器与MobileGestalt缓存具有不可逆的系统稳定性风险。切勿在已受企业 MDM 管控的设备上开启监管模式,也切勿使用会剥离 App Group 权限的签名工具安装侧载版本;操作前请务必做好整机备份。
原文链接与参考资料
- GitHub 官方源码仓库:ichitaso/Regitela - A file explorer and tweak toolbox for iOS
- 底层用户态沙盒逃逸与路径枚举原语:forcequitOS/worse_query
- iOS 26.0.x 内核读写参考项目:wh1te4ever/darksword-kexploit-fun
- 推荐保留 App Group 权限的侧载签名工具:claration/impactor Releases