苹果越狱新漏洞?A12/13BootROM级别漏洞PoC已公开

admin 2026-07-01 04:58:14 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档详细分析了2026年6月公开的AppleA12/A13BootROM级别漏洞usbliter8,该漏洞源于DWC2控制器硬件设计缺陷,通过USBSetup包长度校验漏洞实现内存覆盖。漏洞影响iPhoneXR至11等设备且无法软件修复,目前PoC支持降级模式和启动未签名iBoot,为越狱提供基础但需专用硬件工具。文章深入解析了A12/A13不同的利用链及PAC绕过技术,并给出后利用方案。 综合评分: 86 文章分类: 漏洞分析,二进制安全,移动安全,逆向分析,漏洞预警


cover_image

苹果越狱新漏洞?A12/13 BootROM 级别漏洞 PoC 已公开

原创

i3eg1nner i3eg1nner

SecureNexusLab

2026年6月20日 10:45 北京

在小说阅读器读本章

去阅读

usbliter8 是 Paradigm Shift 团队于 2026 年 6 月 18 日发布的 Apple A12/A13 SecureROM(BootROM)漏洞利用,属于自 checkm8(2019)以来最重要的 BootROM 级别漏洞。

发现这个漏洞的起点是研究人员注意到一个奇怪现象:USB控制器在收到小于标准长度的Setup包时,内存写入位置出现了偏差。顺藤摸瓜,他们定位到DWC2控制器的硬件设计缺陷,环形缓冲的复位逻辑假设所有包都是标准长度,但这个假设不成立。

根据GitHub公开的PoC,目前它提供降级生产模式和启动未签名 iBoot 两项能力——这与 checkm8 为 palera1n 越狱提供的核心原语完全一致,因此理论上是 A12/A13 设备(iPhone XR 至 iPhone 11 系列等数百万台设备)越狱的基石,但由于漏洞位于只读 BootROM 中无法修补、同时需要专用硬件工具且每次重启必须重新利用,从 PoC 到完整越狱工具仍需开发自定义引导加载器、适配内核补丁等大量工程工作。GitHub链接如下:

https://github.com/prdgmshift/usbliter8

USB Setup包的那些事

每次你把iPhone插到电脑上,电脑(host)要给手机(device)发指令,都得走USB的控制传输。控制传输的第一步永远是一个Setup事务。

这个事务里有两个包,都是host发给device的。第一个是Token包,告诉设备地址和端点号。第二个是Data包,里面装着一个8字节的Device Request结构体。

就像这样,

规范规定这个Data包必须正好8字节,格式严格。USB驱动拿到的就是这个结构体,原封不动。

为了后面说清楚,我们先把这个8字节的payload叫做Setup包。

到这里一切都还很正常。问题出在硬件层面。

一个在DWC2控制器里躺了可能十年的bug

苹果SoC上用的USB控制器是Synopsys的DWC2。这个东西的驱动在Linux内核里也有,你可以去看它的实现。

我们要关注的核心是,控制器怎么把收到的数据写到内存里。

AP(应用处理器)配DMA的方式很简单,分配一块内存,然后把物理地址写进控制器MMIO区域的一个叫DOEPDMA的寄存器里。控制器收到SETUP和OUT包的数据后就往这个buffer里塞。

关键细节来了,研究团队观察到,当USB控制器写数据的时候,它会直接递增DOEPDMA寄存器里存的那个地址值。也就是说,这个寄存器不只是个配置项,它实打实地充当了下一次DMA传输的目标地址。

这就埋下了祸根。

DWC2控制器的设计是这样的,它会在内存里连续存最多三个Setup包。

当第四个Setup事务到来时,DMA基地址会被重置到起始位置再写,跟ring buffer的逻辑差不多。

每写完一个收到的包,控制器会把DOEPDMA递增已写入数据的大小。重置操作则是把DOEPDMA直接减24。

看起来很合理的环形缓冲逻辑对吧。

但问题来了,控制器也接受小于标准大小的包(虽然存储时总是按4字节对齐)。

指针递增的量跟固定减24的复位量不匹配了。

结果呢,我们拿到了一个每次12字节步长的buffer underflow原语。

研究团队认为这是USB控制器本身的硬件bug。虽然可能影响很多设备,但这个漏洞只在特定条件下才能被利用。

重点来了,他们确认A12和A13的SecureROM受影响,而A11不受影响。

A11为什么没事?因为A11的USB驱动在每收到一个包后会手动把DMA地址重置回初始值。软件层面的防御把硬件bug给堵上了。

A12和A13的情况是,USB DART被配置成了bypass模式,这意味着我们可以随意覆盖SRAM里的数据。A14及之后,SecureROM里DART被正确配置了,所以这个漏洞虽然硬件层面可能还存在,但利用路径被掐断了。

这就解释了为什么漏洞影响范围刚好卡在A12到A13这一代。

A12上拿PC控制,简单粗暴

A12上拿到PC控制简直不要太直接。

原因很巧妙,USB控制器的DMA buffer分配在堆上,而且刚好紧挨着USB任务的栈。

最简单的做法就是,用buffer underflow一路往上覆盖,把栈上保存的LR干掉。等调度器切回USB任务的时候,PC就是你的了。

没有PAC保护的年代,ROP链就是这么朴实无华。

A13上,PAC来了但故事没结束

A13的SecureROM引入了PAC(Pointer Authentication),情况一下子复杂了很多。研究团队观察到,PAC似乎只对栈上保存的LR做签名。但这已经足够防住A12上那个直接覆盖栈LR的套路了。

而且一路上还有不少防御要绕。堆元数据有校验和,在堆操作的时候会验证。上下文切换时LR要验签名,USB任务每次被唤醒处理数据包时都会触发。经过一些迭代之后,他们搞出了一个多步骤的技术来拿PC控制。我尽量把这个链路的逻辑讲清楚。

「第一步」,覆盖USB控制器DMA buffer前面紧挨着的DART相关数据。这会给他们一些很有限的写原语,在退出DFU循环的时候触发一次。

他们利用了某些清理函数里可控的数据,把全局的DART分配指针给清零了。这一步很关键,不然后面损坏的分配在释放时触发堆校验检查就会panic。

这里有一段代码,你可以感受一下,

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linevoid dart_stop(unsigned int dart_id){
&nbsp; dart = darts[dart_id];&nbsp; mmio_base = dart->info->mmio_base;&nbsp; v4 =&nbsp;16&nbsp;* dart->ctx->field_11 +&nbsp;0x200;&nbsp;&nbsp;for&nbsp;(&nbsp;int&nbsp;i =&nbsp;0; i <&nbsp;16; i +=&nbsp;4&nbsp;) {
&nbsp; &nbsp; *(_DWORD *)(mmio_base + v4 + i) =&nbsp;0;&nbsp; }&nbsp; dart_flush_maybe(mmio_base);}

「第二步」,同一个清理路径里,他们利用一个0xf的写原语覆盖了全局的panic计数器。

这样做的效果是,下一次panic的时候CPU不会重启,而是进入一个无限循环。

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linevoid&nbsp;__noreturn&nbsp;panic(){
&nbsp;&nbsp;if&nbsp;( ++panic_cnt >=&nbsp;3&nbsp;)&nbsp; &nbsp;&nbsp;spin();
}

「第三步」是个精细活。他们必须避免破坏USB任务的上下文,特别是LR和SP寄存器,这些在上下文切换时会被保存和恢复到任务结构体里。

做法是掐准时间,让DMA写发生在USB任务处于活跃状态的时候。这样任务yield出去的时候,正确的寄存器值会覆盖掉之前被破坏的。

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Task structure&nbsp;0x000 &nbsp;┌─────────────────────┐&nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; │ Task state, &nbsp; &nbsp; &nbsp; &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; │ scheduler data, &nbsp; &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; │ crit.&nbsp;section&nbsp;depth │&nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; │&nbsp;0x030 &nbsp;├─────────────────────┤&nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp; Other registers &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; │ <──┐&nbsp; &nbsp; &nbsp; &nbsp; ├──────────┬──────────┤ &nbsp; &nbsp;│ This area needs&nbsp;to&nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp; &nbsp;LR &nbsp; &nbsp;│ &nbsp; &nbsp;SP &nbsp; &nbsp;│ &nbsp; &nbsp;│ be overwritten while&nbsp; &nbsp; &nbsp; &nbsp; ├──────────┴──────────┤ &nbsp; &nbsp;│ USB task is running&nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; │ <──┘&nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp;Safe&nbsp;to&nbsp;overwrite &nbsp;│&nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; │&nbsp;0x1b0 &nbsp;└─────────────────────┘

「第四步」,现在可以安全地覆盖任务结构体里一个追踪临界区深度的字段了。让它在IRQ开着的情况下触发panic,这样执行会进入第一步设好的那个无限循环,但中断服务程序仍然可以运行。

而且此时USB控制器还保持在可以继续往内存写数据的状态。

「最终步」,所有铺垫做完之后,他们可以一路覆盖内存,直到抵达存放USB IRQ handler指针的全局变量。覆盖它,就拿到了PC控制。

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line00000000&nbsp;struct&nbsp;irq_handler_ctx&nbsp;// sizeof=0x1800000000&nbsp;{00000000&nbsp; &nbsp; &nbsp;void&nbsp;(*handler)(void&nbsp;*arg);00000008&nbsp; &nbsp; &nbsp;__int64 *arg;...00000018&nbsp;};
void&nbsp;handle_irq(){&nbsp; ...&nbsp; &nbsp; &nbsp; handler = irq_list[irq_num__].handler;&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;( handler )
&nbsp; &nbsp; &nbsp; &nbsp; handler(irq_list[irq_num__].arg);&nbsp; ...}

到这里,A13上PC控制的整个链路就走通了。比A12多绕了五个弯,但每一步的逻辑都咬得很紧,没有任何”巧合能过”的侥幸感。

拿到代码执行之后呢

PC控制只是开始。SecureROM运行在EL0模式,这意味着我们不能访问受保护的内存区域,比如MMU页表和ROM相关的元数据,也不能动SCTLR、TTBR这些特殊寄存器。

SecureROM可以切到EL1,通过执行SVC 0指令。这不是syscall,handler只是简单地在EL1模式下回到SVC之后的那条指令继续执行,之后很快会执行ERET回到EL0。

ROM里做这个切换的地方很少。研究团队选了负责把执行权交给下一阶段iBoot的函数里的一个位置。

A12和S4/S5的后利用

没有PAC的SoC上,就是经典的ROP。

链很短,就几帧,

  1. 往USB MMIO做一次32位写入,改DMA目标地址
  2. 睡大概400毫秒
  3. 睡醒后跳到0x1000088B0

DMA目标被设到了boot trampoline内部的某个位置。这块内存在EL0下当然不能写(甚至EL1也不行,它是只执行区域),但DMA嘛,不受这个限制。

boot trampoline总是在同一个地址。

400毫秒的延时给了exploit足够的时间把shellcode写进boot trampoline。

延时结束,执行跳转。固件里其实有检查,防止你跳到函数中间执行。它维护了一系列状态标志,每步更新。如果前一步的标志没设,固件就panic。

有意思的事情发生了。编译器为这些检查生成了下面这段代码,

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line...1000088C8:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ADRP&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;X9,&nbsp;#0x19C014033@PAGE1000088CC:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;LDRB&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;W10, [X9,#0x19C014033@PAGEOFF]1000088D0:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;CBNZ&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;W10, loc_10000891C1000088D4:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;STRB&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;W8, [X9,#0x19C014033@PAGEOFF]1000088D8:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;LDRB&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;W8, [X24,#0x19C014032@PAGEOFF] &nbsp;# 危险的检查... 走的X24!1000088DC:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;CBZ&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;W8, loc_10000891C...

X24是callee-saved寄存器,它的值存在栈上。就是那个我们完全控制的栈。

所以这一步直接绕过了。

拿到EL1权限后要做的清理工作不少,

  1. 把原始的boot trampoline拷回原位
  2. 恢复所有被破坏的堆分配,还好数量不多而且都是静态数据
  3. 注入自己的USB控制请求handler,放在boot trampoline区域里未用的空间,覆盖BSS里对应的回调指针
  4. 往USB序列号字符串里注入PWND
  5. 仔细修复usb_task()的栈帧,把执行还回去

DFU模式现在带着我们的自定义handler在跑了。

A13的后利用,PAC让事情更有趣了

A13不能直接ROP。但巧的是boot trampoline区域刚好夹在BSS和堆之间,所以在exploit过程中他们可以覆盖它。

跳转函数有个和A12类似的问题,步骤验证用的地址存在栈上(X22),但在A13上不太好直接控制。

他们找到了一个很漂亮的gadget,

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line# A13 SecureROM...10000A5E8: &nbsp; &nbsp; &nbsp; &nbsp;MOV &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X22, X0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 跳到这里,X22立刻可控10000A5EC: &nbsp; &nbsp; &nbsp; &nbsp;ADRP &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;X10, #__stack_cookie@PAGE10000A5F0: &nbsp; &nbsp; &nbsp; &nbsp;NOP10000A5F4: &nbsp; &nbsp; &nbsp; &nbsp;LDR &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X10, [X10,#__stack_cookie@PAGEOFF]10000A5F8: &nbsp; &nbsp; &nbsp; &nbsp;STUR &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;X10, [X29,#var_38]10000A5FC: &nbsp; &nbsp; &nbsp; &nbsp;LDR &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X10, [X22]10000A600: &nbsp; &nbsp; &nbsp; &nbsp;ADD &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X10, X10, #0xF10000A604: &nbsp; &nbsp; &nbsp; &nbsp;AND &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X10, X10, #0xFFFFFFFFFFFFFFF010000A608: &nbsp; &nbsp; &nbsp; &nbsp;MOV &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X11, SP10000A60C: &nbsp; &nbsp; &nbsp; &nbsp;SUB &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X23, X11, X1010000A610: &nbsp; &nbsp; &nbsp; &nbsp;MOV &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SP, X2310000A614: &nbsp; &nbsp; &nbsp; &nbsp;LDR &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X10, [X22,#0x10] &nbsp; &nbsp;# 来自可控位置的函数指针!10000A618: &nbsp; &nbsp; &nbsp; &nbsp;MOV &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X1, X2310000A61C: &nbsp; &nbsp; &nbsp; &nbsp;MOV &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X2, X910000A620: &nbsp; &nbsp; &nbsp; &nbsp;MOV &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X3, X810000A624: &nbsp; &nbsp; &nbsp; &nbsp;BLRAAZ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;X10 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 跳到jump func,认证是假的...

研究团队提到,固件大量使用带认证的分支指令,但实际上只有IB key被启用了。这个细节对整个利用来说简直是天赐良机。

A12上清理已经够头疼了,A13因为一路覆盖破坏得更加彻底。他们的解决方案很巧妙,重启SecureROM,让它自己把所有东西重新初始化。

唯一的问题是,我们的修改要怎么在重启后存活下来。

做法是把ROM拷到SRAM的最末尾,然后想怎么patch就怎么patch。

但ROM期望从原始地址0x100000000执行,不是从SRAM。它经常做PC相对寻址访问SRAM(0x19C000000),从意外位置跑的话这些访问就会打到空中去。

绕过的办法是配置MMU,让SRAM物理地址上拷贝的代码映射到原始ROM虚拟地址。

MMU从SRAM里ROM代码的最开始就被启用了。过程中的某个时刻,它会创建自己的转换表然后切过去,一切就会坍塌。但可以hook ROM的PTE生成逻辑,让它继续按我们的意愿路由。

补丁列表大概是这样,

  1. 缩小load area,让ROM不会去清除我们藏代码的最后几百KB SRAM
  2. 强制进DFU,不从NAND启动
  3. Hook USB序列号生成,加PWND标识
  4. 注入自定义USB请求handler

搞定。现在的SRAM和load area都足够大,偷几百KB不会造成什么问题。

总结

usbliter8这个exploit说明了一件事,即使是最新一代使用了PAC的SecureROM,一个细微的硬件bug仍然可以导向完整的代码执行和信任链的突破。

BootROM的安全是根基级别的。这个层面的漏洞会影响整个设备的完整性。当然苹果的SEP(安全隔区处理器)在攻击者和用户数据之间加了额外一道安全边界,usbliter8不直接影响SEP本身,但它确实打开了更宽的攻击面来进一步研究Secure Enclave。

公开这个exploit的意义,研究团队说得很克制,他们希望记录这类硬件漏洞的真实影响,为理解现代BootROM安全做出贡献。

BootROM漏洞的稀缺性让它每次出现都像一个仪式,提醒你在这层只读代码里,偶尔还是会有惊喜留给那些愿意花时间深挖的人。

从A11的手动重置DMA地址,到A12/A13的bypass模式DART,再到A14之后正确配置的DART,你几乎能看到苹果的安全团队是如何一代代收紧防线的。但A12和A13已经出厂了,它们的BootROM烧在硅里面,没法改,这些设备将带着这个漏洞走完整个生命周期。

在Paradigm Shift看来,推动安全进步既需要防御性工程,也需要深度的攻击性研究。像usbliter8这样的项目帮助我们理解现有防御的实际局限,提升对复杂攻击面的认知,最终让系统变得更坚韧。

每条gadget的选择、每一步绕过、每一个因为编译器行为而产生的运气,背后都是成百上千个小时的逆向和实验。这才是安全研究迷人的地方,不只是最后那个PWND的字符串,而是你亲眼看到一条链从硬件的细微bug开始,一步步被搭起来,最终撑开一扇门。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:SecureNexusLab i3eg1nner i3eg1nner《苹果越狱新漏洞?A12/13 BootROM 级别漏洞 PoC 已公开》

评论:0   参与:  0