ANZEN 研究:我們如何透過 USB 漏洞攻破 Keystone

更新於 2026年8月25日
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 年的 Black Hat 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 寫入,實際覆蓋的已經是新物件。利用程式透過修改其中的 callbacknext 等欄位,把回調地址指向提前放好的 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 SDK 越界讀寫漏洞代碼

當然擁有越界讀寫只是第一步,還只是拿到了一個記憶體破壞原語,最終我們還是想擁有任意執行能力。

由於漏洞發生在 USB CDC 路徑中,我先沿著當前請求的分派流程檢查 USB 協議棧。在分析 g_usbDev 時,我注意到了其中的 dev.class_cb。它並不是普通數據指標,而是 USB class driver 的運行時回調表;USB core 在處理請求時,會透過這張表調用 SetupDataInDataOut 等函數。只要能夠改寫 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劫持 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 中,而解密這個資訊的 AES KEY 分別有 OTP 和 ATECC608 、DS28S60 兩顆 SE 參與。

硬件錢包安全架構硬件錢包安全架構

那麼這個 AES KEY 生成過程如下所示:

AES Key 生成流程AES Key 生成流程

而參與 SE 互動、生成 AES 的憑證主要由錢包的 passcode(解鎖密碼)和 OTP 生成,流程如下:

Passcode 與 OTP 憑證生成流程Passcode 與 OTP 憑證生成流程

而 Passcode 可以透過獲取 password hash 後暴力破解得到,那麼最終只需要拿到 OTP 的 salt,即可完成 AES key 的生成。對於 OTP 的提取,shellcode 與正常韌體運行在相同的特權級別,所以可以透過 USB 漏洞寫入 MPU 暫存器並關閉 MPU,進而讀取 OTP。

最終利用方法如下,拿到 AES KEY 之後即可解密拿到 BIP39 entropy 重建助記詞。

從 USB 漏洞到助記詞提取的完整攻擊鏈從 USB 漏洞到助記詞提取的完整攻擊鏈

總結

在這次研究過程中,大量使用了 AI 參與到漏洞分析和 exploit 流程。AI 的出現大大提高了安全研究員的利用效率。AI 也在改變這類漏洞的研究成本。過去發現漏洞與利用往往需要投入大量人工時間才能走到穩定利用,而如今,代碼理解、結構匹配、腳本生成和失敗原因排查都能明顯提速。

而無論是 USB SDK 還是其他的第三方組件,都是產品安全審查中比較容易忽略的問題,而往往就是由於這塊短板,最終導致整個產品安全淪陷。

使用 OneKey 保護您的加密之旅

View details for 選購 OneKey選購 OneKey

選購 OneKey

全球最先進嘅硬件錢包。

View details for 下載應用程式下載應用程式

下載應用程式

只需電郵, 即可快速開始全球資產交易。

View details for OneKey SifuOneKey Sifu

OneKey Sifu

即刻諮詢,掃除疑慮。

繼續閱讀