跳至主要内容

一支手機從開機到連上網,中間跨過了幾道信任邊界

本文整理自公開規格與業界通用知識(ARM TF-A、GlobalPlatform、Android Verified Boot、3GPP 等),不涉及任何特定廠商的內部設計或未公開資訊。文中描述的是通用架構,實際產品會有各自的差異。


為什麼要談「信任邊界」

我們習慣把手機當成一台電腦:一顆 CPU、一個作業系統、一堆 App。但實際上,一支現代智慧型手機裡跑著的獨立處理器,少則五六顆,多則十幾顆——應用處理器(AP)、數據機(modem)、數位訊號處理器(DSP)、GPU、感測器中樞、安全處理器、電源管理微控制器,各自跑著自己的韌體,很多時候還跑著自己的作業系統。

它們之間不是平等的。有些能存取全部記憶體,有些不行;有些的輸入來自使用者,有些的輸入來自空氣中任何人都能發射的無線訊號。每一條「這邊的東西不能無條件相信那邊」的界線,就是一道信任邊界。

系統整合工作的本質,很大一部分就是在把這些邊界正確地接起來——而失誤幾乎都發生在邊界上,不是發生在單一模組內部。

這篇文章想從三個角度走一遍:開機時信任是怎麼一級一級傳遞的、AP 之外那顆最特別的處理器(modem)為什麼要獨立成一個信任域、以及從開發到量產的過程中,哪些環節最容易讓前面兩件事白做。


第一部分:開機信任鏈

信任必須有個起點

「驗證下一階段的程式碼」聽起來很直覺,但這裡有個雞生蛋的問題:誰來驗證第一段程式碼?

答案是:沒有人。第一段程式碼必須從一開始就值得信任,而讓它值得信任的方法是——讓它無法被修改。

這就是 BootROM(也叫 mask ROM、Boot ROM)。它在晶片製造時就被寫死在矽晶片上,出廠後任何人、任何軟體都改不了。這個「不可變」的性質,就是整條信任鏈的信任根(Root of Trust, RoT)

在 ARM Trusted Firmware(TF-A)的階段命名裡,這個「從 ROM 執行的第一階段」對應到 BL1。但要注意這兩者不完全等價:TF-A 的 BL1 是一段軟體,只有在 Arm 參考平台上才被放進 ROM;實際商用 SoC 的不可變 BootROM 通常是晶片自己的 ROM 程式碼,TF-A 的階段命名只是一套廣泛使用的參考模型,不必然一一對應。

不可變也意味著:BootROM 裡的 bug 幾乎無法修補。這是為什麼 BootROM 的程式碼通常刻意寫得極簡——它只做最少的事情,因為每多一行程式碼就多一分永久性風險。公開揭露過的案例可以說明後果:Apple 的 checkm8(影響 A5 到 A11 世代)與 Nvidia Tegra X1 的 fusée gelée,都是 BootROM 層級的漏洞,導致已出貨的晶片無法透過任何軟體更新修復。

金鑰放在哪裡

BootROM 需要一把公鑰來驗證下一階段。但公鑰不能寫死在 ROM 裡——每個客戶、每個產品線的金鑰都不一樣,而 ROM 是共用的。

解法是 efuse(或稱 OTP,One-Time Programmable memory)。這是晶片上一塊只能寫一次的記憶體:每個 bit 出廠時通常是 0,可以被燒成 1,但燒完就再也回不去。

實務上 efuse 裡通常不放完整的公鑰(太佔空間),而是放公鑰的雜湊值。完整公鑰跟著韌體映像檔一起存在 flash 裡,BootROM 讀進來後先算雜湊、跟 efuse 裡的比對,確認公鑰沒被掉包,再用這把公鑰去驗證映像檔的簽章。

這裡有個關鍵性質值得記住:efuse 是單向的。一旦燒下去,這片晶片就永久進入那個狀態。這既是它的安全價值(攻擊者無法還原),也是它的風險(燒錯了整批報廢)。

一級驗一級

有了信任根之後,剩下的就是遞迴:每一級在把控制權交出去之前,先驗證下一級

以 TF-A 的分階段模型為例,典型的流程長這樣:

BootROM (BL1,不可變)
↓ 驗證
BL2 (Trusted Boot Firmware / 常見的 preloader)
↓ 驗證
├─→ BL31 (EL3 Runtime Firmware,常駐的 secure monitor)
├─→ BL32 (TEE OS,如 OP-TEE / Trusty)
└─→ BL33 (Non-secure bootloader,如 U-Boot / LK)
↓ 驗證
Linux Kernel + DTB
↓ 驗證
System / Vendor 分區(dm-verity)

圖中 BL2 下方的三個分支表示的是「BL2 負責載入與驗證這三個映像」,不是三者平行執行。實際的執行順序是 BL1 → BL2 → BL31(EL3 常駐)→ 由 BL31 初始化 BL32 → 再把控制權交給 BL33。

每一個箭頭都是一次驗證(最後一段的 dm-verity 稍有不同,是「簽章根雜湊 + 執行時逐塊比對」,稍後會談)。整條鏈的強度由最弱的那個環節決定——只要有一級忘了驗、或驗錯了,後面所有的驗證都失去意義,因為攻擊者可以從那個斷點直接接管。

這是信任鏈最反直覺的地方:它不是「多一層多一分安全」的疊加關係,而是串聯電路。九級做對、一級做錯,等於零級。

簽章 ≠ 加密

這兩件事經常被混為一談,但目的完全不同:

簽章驗證(Authentication)加密(Confidentiality)
防的是別人改了我的韌體別人讀懂我的韌體
保護的是完整性、來源機密性
對 secure boot必要不必要
常見誤解「有加密就安全了」

Secure boot 需要的是簽章驗證,不是加密。一份沒加密但簽章正確的韌體,安全性完全足夠——任何人都可以讀它、分析它,但沒有私鑰就無法製造出一份能通過驗證的修改版。

反過來,一份加密但沒有簽章驗證的韌體,安全性可能是零。加密只是提高了逆向工程的門檻,而逆向工程從來就不是防得住的事情——它只是需要時間。把機密性當成安全機制,是一種會過期的保護。

防止版本回退

假設你發現了一個 bootloader 漏洞,發布了修補版本。攻擊者能不能把舊的、有漏洞的版本刷回去?舊版本的簽章是合法的,驗證會通過。

這叫回退攻擊(rollback attack),防禦方式是 anti-rollback:在映像檔裡放一個單調遞增的版本號,同時在 efuse 裡保留一組 bit 記錄「目前見過的最低可接受版本」。每次成功開機到新版本,就燒掉一個 bit 把門檻推高。舊版本的版本號低於門檻,驗證就會失敗。

實務上這個機制很好用但也很危險:efuse bit 數量有限(用完就不能再升級門檻了),而且推進之後就無法降版——如果新版本有嚴重問題想 rollback 回去,你會發現自己把自己鎖在外面。所以 anti-rollback 的推進時機通常要非常保守,一般是等新版本在市場上穩定一段時間之後才觸發。

到了 OS 之後

Bootloader 驗完 kernel,故事還沒結束。System 分區有好幾 GB,開機時全部算一次雜湊不切實際。

Android Verified Boot(AVB)搭配 dm-verity 的做法是:把分區切成 4KB 的區塊,對所有區塊建一棵雜湊樹(Merkle tree),只有樹根被簽章。開機時只驗證樹根,之後每當有區塊被實際讀取,才即時驗證它到樹根的那條路徑。

這樣就把「開機時驗證幾 GB」轉換成「執行時每次驗證幾 KB」,開銷分攤到平常,而且沒被讀到的資料根本不需要驗證。

AVB 同時定義了 verified boot state,用四種顏色表示裝置狀態:

  • GREEN:bootloader 上鎖,使用廠商金鑰驗證通過
  • YELLOW:bootloader 上鎖,但使用的是使用者自訂金鑰(開機時顯示金鑰指紋)
  • ORANGE:bootloader 解鎖,不做驗證
  • RED:驗證失敗,不應繼續開機

這個設計有個值得注意的用意:它並不禁止使用者刷自己的系統,而是要求裝置誠實地把狀態揭露出來。使用者有權決定要不要解鎖,但解鎖這件事必須是可見的、無法偽裝成 GREEN 的,這樣依賴裝置完整性的服務(例如支付)才能做出自己的判斷。


第二部分:AP 之外的世界

SoC 裡不只一顆 CPU

前面談的整條信任鏈,主要跑在應用處理器(AP)上。但 AP 只是 SoC 裡的一部分。

一顆典型的行動 SoC 裡還有:GPU、NPU、ISP(影像訊號處理器)、視訊編解碼器、感測器中樞、音訊 DSP、安全處理器、電源管理微控制器——以及本文接下來的主角:數據機(modem)

其中大部分是「加速器」性質的:AP 餵資料給它們,它們算完回傳。但 modem 不是。modem 是一個真正獨立的子系統,跑自己的作業系統、有自己的記憶體、自己的生命週期,而且它的輸入不來自 AP。

Modem 的內部結構

現代蜂巢式數據機在架構上大致分成三塊:

協定處理器。負責 3GPP 協定堆疊的上層——RRC(無線資源控制)、NAS(非接取層)、PDCP/RLC/MAC 等等。這部分邏輯複雜、狀態機龐大,通常跑在一顆即時性導向的 CPU 核心上(傳統上常見 ARM Cortex-R 系列這類為即時系統設計的核心),底下跑一個 RTOS。

基頻 DSP。負責實體層(PHY)——調變解調、通道編解碼(LTE 資料通道用 Turbo 碼、控制通道用卷積碼;5G NR 資料通道改用 LDPC、控制通道用 Polar 碼)、FFT、通道估測、等化。這些是密集的、高度平行的數學運算,用通用 CPU 做效率太差、功耗太高,所以會用專用的 DSP 架構,通常帶有向量/SIMD 指令集和專門的硬體加速單元。

RF 收發器與前端。類比/混合訊號的部分,把基頻訊號轉成實際的射頻,以及相反方向。這塊常常是獨立的晶片。

為什麼要分成兩顆處理器

這不是歷史包袱,而是被規格逼出來的。

LTE 的一個 subframe 是 1 ms。接收端收到一個下行 subframe 之後,必須在固定的時間點回覆 HARQ 的 ACK/NACK——FDD LTE 的下行 HARQ 回饋固定落在 subframe n+4,扣掉傳輸與上行準備時間,實際留給終端完成解調、解碼、CRC 檢查的處理預算大約是 3 ms。5G NR 在較高的 subcarrier spacing 下 slot 更短(slot 長度是 1 ms / 2^μ),時間預算又更緊。

關鍵在於:下一個 subframe 在 1 ms 後就會到。這 3 ms 的處理必須以每 1 ms 一次的節奏持續 pipeline 下去,不能累積延遲。

這是**硬性即時(hard real-time)**要求:晚了不是效能差一點,是這個傳輸區塊直接失敗、要重傳。而且這件事每毫秒都在發生。

把這種工作放在跑 Linux 的 AP 上,排程抖動會直接殺死你。所以 PHY 層必須跑在專用硬體上,配專用的 DSP 和嚴格的即時排程。同時,協定堆疊的上層邏輯又太複雜、太狀態導向,不適合塞進 DSP。分工是必然的結果。

AP 與 modem 之間怎麼溝通

早期的設計裡,AP 和 modem 是兩顆分開的晶片,中間走實體匯流排——USB(HSIC)、UART、SDIO、共享記憶體介面都出現過。經典的介面協定是 AT command,後來演進出各種以封包為單位的協定(如 QMI、MBIM 這類控制介面,以及專門的資料路徑)。

現在主流是把 modem 整合進 SoC,AP 和 modem 共用同一顆晶片上的記憶體控制器。這時的介面通常是:

  • 共享記憶體 + 環形緩衝區:資料路徑的主力,AP 和 modem 各自讀寫約定好的記憶體區域
  • 郵箱(mailbox)/中斷:通知對方「我放東西進去了」
  • RPC 層:控制平面的請求/回應
  • 獨立的電源域與重置線:modem 可以獨立於 AP 崩潰、重啟、進入低功耗

整合到同一顆晶片省了功耗、面積和延遲,但也帶來一個結構性問題:它們現在共用實體記憶體了。

為什麼 modem 是一道特別重要的信任邊界

三個性質疊在一起,讓 modem 在安全模型裡的地位很特殊:

第一,它的輸入本質上不可信。 AP 上的軟體處理的輸入來自使用者、來自網路封包——這些至少有 TLS、有作業系統的沙箱、有權限模型。而 modem 處理的是空氣中的無線訊號。在協定的早期階段(例如網路搜尋、初始接取),裝置根本還沒完成雙向認證,此時收到的訊息無法確認來源。任何人架起一台基地台模擬器,就能對範圍內的裝置送出任意的協定訊息。而軟體無線電(SDR)的成本這些年已經降到幾百美金等級。

第二,它的攻擊面極大。 3GPP 的協定規格是數千頁的文件,涵蓋 2G 到 5G 好幾個世代,充滿了為了向後相容而保留的老舊機制、可選欄位,以及好幾套不同的變長編碼(RRC 用 ASN.1 unaligned PER,NAS 用自己的 IEI/TLV 格式)。每一個解析器都是潛在的記憶體安全問題,而這些程式碼絕大多數是用 C 寫的、跑在沒有現代記憶體保護的 RTOS 上。

第三,它在傳統上有很高的權限。 如果 modem 能夠 DMA 存取整個系統記憶體,那麼一個 modem 上的遠端程式碼執行漏洞,就等於整台裝置淪陷——連 AP 上的所有安全機制都繞過了,因為攻擊者從側面進來。

把這三點合起來:一個不需要使用者互動、只要目標在無線電範圍內就能觸發的遠端攻擊路徑。 這是為什麼 baseband 安全是行動裝置安全裡最受重視的領域之一。

兩個關鍵的緩解機制

IOMMU / SMMU。 這是最重要的一個。在 modem 和記憶體控制器之間放一個位址轉譯與權限檢查單元,由 AP 側的安全軟體設定,明確規定 modem 只能存取哪幾段實體記憶體。這樣即使 modem 韌體被完全攻破,攻擊者能碰到的也只有那幾段共享緩衝區,碰不到 AP 的 kernel、碰不到 TEE、碰不到使用者資料。

這把「modem 淪陷 = 全機淪陷」降級成「modem 淪陷 = modem 淪陷」。從安全架構的角度,這是投資報酬率最高的單一措施——它不需要 modem 韌體本身是完美的,就能把爆炸半徑限制住。

Modem 自己的 secure boot。 Modem 韌體同樣需要簽章驗證,而且驗證者最好不是 AP 上的一般軟體。常見做法是由安全處理器或 TEE 負責載入與驗證 modem 映像,再放它出閘。這樣 modem 的信任鏈跟 AP 的信任鏈才是接在同一個信任根上,而不是各走各的。

還有一個常被忽略的細節:modem 崩潰重啟之後怎麼辦? Modem 子系統重啟(subsystem restart)是常見的錯誤復原機制,但重啟路徑必須跟冷開機路徑一樣做完整驗證。「快速重啟」如果為了省時間跳過驗證,就等於在信任鏈上開了一個後門。


第三部分:從開發到量產

前面兩部分講的是設計。但一個設計得再好的安全架構,如果整合階段沒有正確落實,實際效果可能是零。而這正是系統整合最容易被低估的地方。

以下的分類與術語主要對應兩份公開規格:Arm 的 PSA Security Model(定義了裝置生命週期狀態)與 PSA ADAC(Authenticated Debug Access Control,定義了受控的除錯授權機制)。

開發需要的,正是量產不該有的

工程師在 bring-up 階段需要一堆東西才能工作:

  • 除錯序列埠(UART console):看 log、進 shell
  • JTAG / SWD:接除錯器、單步執行、讀寫記憶體
  • 下載模式(download mode):透過 USB 把映像檔刷進裝置
  • 工廠測試模式:校正 RF、測試感測器、寫入序號與 IMEI
  • 未鎖定的 bootloader:隨時刷任意映像
  • 開發用簽章金鑰:或者根本不驗證

這些每一項都是必要的——沒有它們沒辦法開發。而它們每一項在量產裝置上都是攻擊面。

這就構成了整合工作的核心張力:同一批人、同一套程式碼,要在開發時保持全開,在出貨時確保全關。 而「確保全關」的驗證,是最容易在專案後期被時程壓縮掉的工作。

生命週期狀態

比較成熟的處理方式,是把「裝置處於哪個階段」變成晶片上一個明確的、可查詢的狀態,而不是散落在各處的設定。

概念上的階段大致是:

階段特徵
Blank / Virgin剛出廠,什麼都沒燒,全部開放
Development開發金鑰、除錯全開
Production量產金鑰、除錯關閉、secure boot 生效
RMA / Return受控的重新開啟除錯,供故障分析

狀態記錄在 efuse 裡,硬體會依據狀態自動決定 JTAG 是否可用、要用哪把金鑰驗證。這樣「關掉除錯」就不再是一個要靠人記得去改的軟體設定,而是硬體強制的結果。

RMA 那一格是最難設計的:你需要能分析退回來的故障品,但重新開啟除錯不能變成攻擊者的萬用鑰匙。常見的做法是要求「裝置特定的授權」——用晶片唯一 ID 產生的挑戰值,回原廠簽章後才能解鎖那一顆晶片,且不影響其他任何裝置。

「有支援」和「有生效」是兩件事

這可能是整合階段最常見、也最昂貴的一類錯誤。

平台方案(BSP)通常會支援完整的安全機制。但支援的意思是「程式碼寫好了、參考設計裡有」,不等於這台實際出貨的裝置上它正在運作。中間隔著一連串必須被實際執行的動作:

  • efuse 有沒有真的燒(不是「排程中」,是燒了)
  • 燒的是量產金鑰還是開發金鑰
  • 生命週期狀態有沒有推進到 production
  • 量產 build 有沒有真的關掉除錯選項
  • anti-rollback 有沒有啟用

每一項都需要在最終出貨的裝置上實測驗證,而不是查閱設定檔或相信 build 系統。設定檔說要燒,跟 efuse 真的被燒了,是兩個獨立的事實。

一個實用的檢查習慣是:不要驗證「機制存在」,要驗證「攻擊失敗」。

弱驗證(不夠)強驗證(該做的)
確認 secure boot 設定為 enabled刷一個沒簽章的映像進去,確認裝置拒絕開機
確認 build config 關掉了 console實際接上 UART,確認沒有輸出、沒有 shell
確認 JTAG 設定為 disabled實際接除錯器,確認連不上
確認 anti-rollback 有設定刷舊版本進去,確認被拒絕

右邊那一欄的每一項都能在幾十分鐘內做完,而且結果是二元的、無法自我欺騙的。左邊那一欄可以做得很漂亮,然後在出貨後才發現某個環節根本沒生效。

工程樣品與量產機的差異管理

還有一類問題出在流程本身:拿去做安全驗證的那台裝置,跟實際出貨的那批,往往不是同一個東西。

工程樣品為了方便測試,通常刻意保持除錯開啟、efuse 未燒。如果驗證是在工程樣品上做的,那你驗證的是一台不存在於市場上的裝置。

反過來的錯誤也存在:量產機上還留著開發期的殘留。公開的韌體分析研究反覆發現同一類東西:測試用的憑證、僅供內部工具使用的特權介面、為了產線效率保留的快速刷機路徑——它們的共通點是「當初有正當理由存在」,然後沒有人負責在最後把它拿掉,因為拿掉它不會讓任何測試變綠。

沒有人會因為多留了一個 debug hook 而測試失敗。 這是為什麼這類問題需要靠流程(明確的關卡與責任人)而不是靠測試來攔截。

幾類典型的實作缺陷

即使機制都啟用了,實作本身也可能有問題。以下是業界公開討論過的、跨廠商反覆出現的模式:

驗證失敗但繼續執行。 程式碼確實呼叫了驗證函式,也確實拿到了失敗的回傳值——然後只寫了一行 log 就繼續往下跑。這在開發階段是刻意的(不然沒辦法測試),然後忘了在量產路徑上改回去。

只驗證了 header。 簽章涵蓋的範圍不包含全部的映像內容,攻擊者可以保留合法的 header,替換後面的實際程式碼。

先載入再驗證。 把整個映像複製到最終執行位址,然後在原地做驗證——但驗證期間如果有其他 DMA 主控可以修改那塊記憶體,就存在 TOCTOU(檢查與使用之間的時間差)問題。正確做法是在受保護的記憶體中驗證,或驗證後再搬移。

信任鏈的斷點。 A 驗 B,B 驗 C,但 B 忘了驗 D,而 D 也有很高的權限。這類問題在有多個並行子系統的 SoC 上特別容易發生,因為信任鏈不是一條線而是一棵樹,樹的某根枝條被漏掉不容易被注意到。

回退路徑不驗證。 主要開機路徑做得很嚴謹,但錯誤復原路徑、recovery 模式、子系統重啟路徑則沒有。攻擊者只要能觸發錯誤,就能走到那條沒人看守的路上。

這些缺陷的共同點值得注意:它們都不是密碼學的問題。簽章演算法本身沒被破解,破的是「什麼時候驗、驗多少、驗完之後怎麼辦」這些工程層面的決定。安全機制的強度,通常不取決於它用的密碼學有多強,而取決於它被整合得有多完整。


收尾:一張圖的距離

把整篇文章壓縮成一句話:

從按下電源到連上網路,你的資料經過了十幾道信任邊界。每一道都可能被正確地實作、也可能被正確地繞過,而差別往往不在設計,在整合。

對系統整合工作者來說,我覺得最實用的三個心態轉換是:

  1. 把信任鏈當成串聯電路,不是防護層。 不要問「我們有幾層防護」,要問「最弱的那一級在哪裡」。

  2. 驗證「攻擊失敗」,而不是「機制存在」。 設定檔會說謊,實測不會。

  3. 開發期的便利,就是量產期的攻擊面。 每一個為了開發而開的口,都需要一個明確的負責人和一個明確的關閉時機——而且要有人去確認它真的關了。


延伸閱讀(皆為公開資料)

  • ARM Trusted Firmware-A 文件與 Trusted Board Boot Requirements(TBBR):分階段開機與驗證模型的參考實作
  • Android Verified Boot / AVB 官方文件:dm-verity、rollback protection、verified boot state 的設計說明
  • GlobalPlatform TEE 系列規格:可信執行環境的標準化介面
  • ARM System MMU (SMMU) Architecture Specification:DMA 隔離的架構基礎
  • Arm PSA Security ModelPSA ADAC(Authenticated Debug Access Control):裝置生命週期狀態與受控除錯授權的公開規格
  • 3GPP TS 24.301(EPS NAS)/ TS 24.501(5GS NAS)/ TS 36.331(E-UTRA RRC)/ TS 38.331(NR RRC):LTE 與 NR 的 NAS 與 RRC 協定規格,理解 baseband 攻擊面規模的第一手材料
  • OP-TEE 專案文件:開源 TEE 實作,適合實際動手理解 secure world 的運作