ANZEN 研究:我们如何通过 USB 漏洞攻破 Keystone

要点总结
• USB SDK 对主机可控长度缺少校验,导致固定缓冲区发生越界读写
• 攻击者可通过劫持 USB class 回调表,将越界写转化为 MCU 任意代码执行
• 错误的 MPU 配置使 SRAM 中的 shellcode 可以直接执行
• 获取 Passcode、OTP 与安全芯片中的加密材料后,可重建 AES Key 并提取 BIP39 助记词
• 第三方 SDK 同样属于硬件钱包的核心安全边界,必须纳入完整审计
安全更新(2026 年 7 月): 本文讨论的问题由 OneKey Anzen 在 2026 年 3 月与 Keystone 开展联合安全审计时发现,影响集成了 MCU 供应商 USB SDK 的 Keystone 3 Pro 固件。Keystone 已于 2026 年 4 月 1 日发布固件 v2.4.0,完整修复该问题;截至联合公告发布时,双方没有发现该漏洞被用于攻击 Keystone 用户的证据。成功利用需要攻击者同时实体持有设备、知道正确 PIN、解锁设备、获得用户对 USB 连接的批准,并将设备连接至攻击者控制的电脑。运行低于 v2.4.0 固件的用户应立即升级至最新版;使用隔空 QR 签名可以避开这一特定 USB 攻击路径,但仍建议完成升级。详见 Keystone 与 OneKey Anzen 联合安全更新。
8 月初,在拉斯维加举行了 2026 年的Blackhat USA,由于签证原因,我们的议题改成了 on-demand 的形式。这个议题来源于我们对 Keystone 钱包的一次审计,发现了芯片供应商的 USB SDK 的严重问题,最终提取出了助记词。
事实上,USB 的协议栈出现问题也不是第一次了,最近十多年来,已经有无数设备比较严重的攻击都与 USB 的协议栈有关。从 2014 年的 USB 驱动漏洞与远程重定向,到 Fusée Gelée、checkm8、Kamakiri,再到 2026 年公开的 usbliter8。
USB 协议漏洞频出
2014:USB 漏洞真的必须物理接入吗?
2014 年,NCC Group 在 Black Hat Asia 发表了《USB Attacks Need Physical Access Right? Not Any More...》。
当时人们往往把 USB 漏洞归为“本地物理攻击”:攻击者必须把恶意设备插到目标机器上,通过畸形 descriptor 或 class-specific data 触发驱动漏洞。
而在这个议题中,研究人员利用 RemoteFX USB Redirection,将低层 USB 设备重定向到了远程 Windows Server。恶意 USB 设备物理上连接在攻击者自己的客户端,但真正解析恶意 USB 数据的驱动运行在远程服务器上。
2018,Fusée Gelée:把请求长度当成对象长度
2018 年公开的 Fusée Gelée 是 USB BootROM 漏洞中最经典的案例之一。漏洞位于 NVIDIA Tegra 的 Recovery Mode,也就是 RCM。设备在极早期的 BootROM 环境中启动 USB 栈,等待主机上传恢复镜像。而漏洞点出现在一个看似普通的 GET_STATUS 控制请求中。
按照 USB 语义,一个 status 对象只有 2 字节。主机通过 wLength 表示自己最多愿意接收多少数据,设备应当返回 min(主机请求长度, 实际可用数据长度)
但在处理 endpoint recipient 时,BootROM 错误地执行了类似逻辑:
status = get_usb_endpoint_status(index);
/* 错误:使用主机给出的请求长度 */
size_to_tx = setup_packet.length;
memcpy(dma_buffer, &status, size_to_tx);
也就是说,2 字节对象的实际长度没有被使用,攻击者控制的 wLength 反而直接成为 memcpy 的长度,理论上可达到 65,535 字节。
不仅如此,RCM 在校验签名前,已经允许攻击者把一个较大的 payload 放入内存;该 payload 区域又恰好位于 BootROM 活动栈附近。通过选择 DMA buffer 并构造内存布局,越界复制最终可以把攻击者可控数据覆盖到执行栈上。
而当时的 BootROM 环境没有常见的 stack canary、ASLR 和有效的内存执行保护。最终结果是:在任何安全锁定和权限降低发生之前,攻击者已经获得了 BootROM 上下文中的任意代码执行。
这和本文后面的硬件钱包漏洞有非常直接的相似性。
2019,checkm8:攻击者控制的不只是数据,还有“未完成”
2019 年公开的 checkm8 是 Apple SecureROM 漏洞中影响最深远的案例之一。它覆盖了多代 iOS 设备,使攻击者能够在 DFU 模式下获得 BootROM 上下文中的代码执行,进而转储 SecureROM、解密固件 keybag,或者将设备切换到允许 JTAG 的 demotion 状态。由于漏洞位于芯片制造时固化的 BootROM 中,已经出货的设备无法通过 iOS 更新消除这个漏洞。
和 Fusée Gelée 不同,checkm8 不是一个简单的长度检查错误。它的核心是一处发生在 Apple DFU USB 状态机中的 use-after-free:一个 USB 数据阶段没有正常结束,保存数据阶段状态的全局变量因此没有被清理;但底层 DFU buffer 却在 USB 栈退出时被释放了。
正常情况下,BootROM 收到带 DATA 阶段的 DFU_DNLOAD 请求后,会保存接收 buffer 和 wLength。数据收完,完成回调负责清空这些状态;退出 DFU 时,再释放对应的 I/O buffer。
DFU_DNLOAD(setup)
{
ep0_buffer = dfu_io_buffer;
ep0_remaining = setup.wLength;
}
DATA_complete()
{
ep0_buffer = NULL;
ep0_remaining = 0;
}
DFU_exit()
{
free(dfu_io_buffer);
}
而 checkm8 利用的就是这几步之间的空隙,攻击者先发起一个带 DATA 阶段的请求,让 BootROM 记录 buffer 指针和剩余长度,随后故意不把数据发完。通过取消传输或制造超时,DATA 完成回调不会执行;接着触发 DFU 状态切换或 USB reset,让 USB 栈退出并释放原来的 buffer。
这时,内存已经释放,接收状态里却还留着旧指针:
free(dfu_io_buffer);
ep0_buffer = old_dfu_io_buffer; // 悬空指针
接下来还需要整理 heap。公开利用会泄漏一批 USB request 对象,改变后续分配位置,让新的 DFU buffer 落到别处,同时让旧地址被 usb_device_io_request 一类对象重新占用。
之后再次发送 USB 数据,BootROM 仍会沿着残留的 ep0_buffer 写入,实际覆盖的已经是新对象。利用通过修改其中的 callback、next 等字段,把回调地址指向提前放好的 payload。下一次 USB 请求完成时,SecureROM 就会跳入攻击者代码。
2020,Kamakiri:USB 请求如何变成一次间接跳转
Kamakiri 针对的是一批 MediaTek SoC 的 BootROM 下载模式。它允许攻击者绕过原本限制未授权 Download Agent 的认证路径,在 BootROM 上下文中运行未经签名的第一阶段 payload。获得这一执行能力后,后续 payload 可以继续操作闪存、转储内存、修改启动状态,或者恢复原本已经无法正常启动的设备。
Kamakiri 的利用链很短,因为 MediaTek BootROM 本身已经提供了攻击者需要的第一项能力:通过下载协议向一段已知 SRAM 区域上传数据。 攻击者把第一阶段 payload 放进去,再通过一个特殊的 USB control request 触发漏洞。
漏洞正好位于 USB control request 的分派处理中。独立逆向分析将相关逻辑概括为类似下面的形式:
handler = handler_array[value * 13];
handler();
关键问题在于,来自 USB 请求的选择值在缺少充分范围检查的情况下参与了函数指针表索引。攻击者只要找到一个合适的控制值,使表项读取结果落到之前上传的 SRAM payload 地址,就能把一次 USB 请求变成对 payload 的间接调用。不同 SoC 的 BootROM 布局并不完全一致,因此具体索引值和 payload 地址可能需要适配或枚举。
公开 PoC 在上传 stage 1 后发送了这样一次控制传输:
ctrl_transfer(
0xA1, # class request, device-to-host, interface recipient
0,
0,
10, # wIndex
0
)
2026,usbliter8:USB 控制器把 DMA 写指针移到了 buffer 之前
2026 年 6 月公开的 usbliter8,再次把不可通过软件修补的 BootROM 利用扩展到了 Apple A12、A13 以及 S4、S5 SoC。
一旦利用成功,攻击者可以在 SecureROM 的 EL1 上下文中执行代码,修改 DFU 行为、临时 demote 芯片的 production mode,或者绕过签名检查启动未经签名的原始 iBoot。这也意味着应用处理器的启动信任链被突破,而且无法通过后续 iOS 更新修复。
usbliter8 与前面的几个案例不太一样,它主要利用的是 Synopsys DWC2 USB 控制器自身处理 SETUP transaction 时的 DMA 指针语义。
按照 USB 协议,SETUP transaction 的数据部分固定为 8 字节。DWC2 控制器允许连续接收三个 SETUP 包,并通过 DMA 将它们依次写入内存。三个正常 SETUP 包一共占用 24 bytes。
当第四个 SETUP transaction 到来时,控制器会把 DOEPDMA 写指针固定减去 24 字节,以此将地址退回这组 SETUP buffer 的起点。对于正常的 8 字节 SETUP 包,这套逻辑刚好闭合。
但控制器同时接受短于 8 字节的异常 SETUP 数据,并按照实际写入长度推进 DMA 指针;内部写入粒度为 4 字节。攻击者反复发送 4 字节短包时,前三个包只会让地址一共向前移动 12 字节,而第四个包到来时,控制器仍然固定回退 24 字节。于是,下一轮 SETUP 数据不再写入原来的 buffer,而写到 buffer 起点之前 12 字节。不断重复这个过程,DMA 写入位置就会以可预测的步长向低地址移动,最终形成一个稳定的 buffer underflow。
通常来说,USB 控制器的越界 DMA 不一定能够破坏任意系统内存。现代 SoC 通常会通过 IOMMU 限制 USB 控制器能够访问的地址范围。但 A12 和 A13 的 SecureROM 环境中,USB DART 处于 bypass 状态,控制器的 DMA 因而缺少有效的地址隔离。这使得 underflow 不再局限于某个狭小的 SETUP buffer,而能够继续覆盖其他 SRAM 对象。
A12 上,USB DMA buffer 与 USB task 的栈相邻,利用可以覆盖保存的 link register,并在任务切换时取得程序计数器控制。A13 加入了 Pointer Authentication,直接覆盖返回地址不太行,因此公开利用采用了更复杂的内存破坏链,最后改写 BSS 中的 USB interrupt handler 指针,由下一次 USB 中断进入攻击者代码。两条路径最终都获得 SecureROM EL1 执行。
从USB越界读写到任意执行
而本次 Black Hat USA 分享的漏洞根因其实也在 USB SDK 上。
CDC 中的 line-coding 请求只需要一个很小的固定结构,但集成的 USB SDK 为它准备了一个固定 8 字节的 CmdBuff,随后直接将主机提供的 req->wLength 传递给底层发送或接收函数。而代码没有做长度检查,也没有根据具体请求要求它等于协议规定的固定长度。结果造成了越界读和写。
USB SDK 越界读写漏洞代码
当然拥有越界读写只是第一步,还只是拿到了一个内存破坏原语,最终我们还是想拥有任意执行能力。
由于漏洞发生在 USB CDC 路径中,我先沿着当前请求的分派流程检查 USB 协议栈。在分析 g_usbDev 时,我注意到了其中的 dev.class_cb。它并不是普通数据指针,而是 USB class driver 的运行时回调表;USB core 在处理请求时,会通过这张表调用 Setup、DataIn 和 DataOut 等函数。只要能够改写 class_cb,后续一次正常的 USB 请求就会经过我们控制的函数指针,因此它是一个很合适的劫持目标。
整个利用可以拆成两次 USB 请求。
第一次请求使用 SET_LINE_CODING 触发越界写。在这次写入中,我需要同时完成三件事:把 shellcode 放进 CmdBuff,在附近的 SRAM 中布置一张伪造的回调表,最后将 class_cb 改为这张假表的地址。
随后,我再发送一个普通的 GET_LINE_CODING 请求。USB core 会像往常一样处理这个请求,并通过 class_cb->Setup(...) 调用 class driver 的 Setup 回调。区别在于,此时 class_cb 已经不再指向原始回调表,而是指向我们伪造的表;假的 Setup 又指向 CmdBuff 中的 shellcode。于是,第二次 USB 请求会把 PC 直接带到我们写入 SRAM 的代码中。
shellcode 位于 SRAM,本来还需要考虑数据区不可执行的问题。但固件配置 MPU 时使用了错误的 size 格式,最终得到的保护范围为 0,原本用于限制 BSS 执行权限的 MPU region 实际上没有生效。因此,这段位于 CmdBuff 中的代码可以直接执行。
劫持 USB 回调表执行 shellcode
理论可行后就开始实操,首先我测试了一下实际越界读写的范围。我构造了大小为 64 字节的控制传输,越界读取可以持续泄漏大约 24 KB 的 SRAM,但越界写到 4,352 字节的位置时会稳定 crash。
之后检查发现,在这个位置越界数据覆盖了CmdBuff + 0x1130 处仍在使用的指针,EP0 随即失去继续接收后续数据的能力。
越界写导致设备崩溃的位置
解决这个问题也比较简单,由于我们已经拥有越界读的能力,直接越界读取真实的状态,再将 dump 出的状态数据加载到 shellcode 即可。这样就构成了一套完整的任意代码执行逻辑。
现在已经能够覆盖 class_cb,但我不希望这次劫持把 USB 通道一起破坏。长写会先把 shellcode 和伪造回调表放入 SRAM,并暂时让 class_cb 指向假表;随后发送 GET_LINE_CODING,USB core 便会经由 fake Setup 跳入 shellcode。任务完成后,shellcode 将结果写入 CmdBuff 后方,恢复原始 class_cb,再调用原来的 Setup,让本次 EP0 传输正常结束。这样每次劫持都只是一次短暂的 detour,USB 仍然可用,也可以继续注入下一项任务。
固件原本还试图通过 MPU 将 BSS 设置为不可执行,但配置代码使用了错误的 size 格式,最终计算出的保护范围为零。
从 MCU任意执行到助记词提取
获得 MCU 任意执行后,并不意味着可以提取助记词。硬件钱包的具体结构如下,总的来说,助记词的加密信息保存在 DS28S60 SE 中,而解密这个信息的 ASE KEY 分别有 OTP 和ATECC608 、 DS28S60两颗 SE 参与。
硬件钱包安全架构
那么这个AES KEY 生成过程如下所示
AES Key 生成流程
而参与SE 交互、生成 AES 的凭证主要由钱包的 passcode(解锁密码)和 OTP 生成,流程如下:
Passcode 与 OTP 凭证生成流程
而 Passcode 可以通过获取 password hash 后爆破得到,那么最终只需要拿到 OTP 的 salt,即可完成 AES key 的生成。对于 OTP 的提取,shellcode 与正常固件运行在相同的特权级,所以可以通过 USB 漏洞写入 MPU 寄存器并关闭 MPU,进而读取 OTP。
最终利用方法如下,拿到 AES KEY之后即可解密拿到BIP39 entropy 重建助记词。
从 USB 漏洞到助记词提取的完整攻击链
总结
在这次研究过程中,大量使用了 AI 参与到漏洞分析和 exploit 流程。AI 的出现大大提高了安全研究员的利用效率。AI 也在改变这类漏洞的研究成本。过去发现漏洞与利用往往需要投入大量人工时间才能走到稳定利,而如今,代码理解、结构匹配、脚本生成和失败原因排查都能明显提速。
而无论是 USB SDK 还是其他的第三方组件,也是产品安全审查中比较容易忽略的问题,而往往就是由于这块短板,最终整个产品安全沦陷。






