featured.svg

Sony Alpha 7R V(ILCE-7RM5,機種代碼 0x11030080,內部代號 R5)憑藉 6,100 萬畫素的感光元件與獨立 AI 處理單元,在畫質細節與自動對焦表現上幾乎無可挑剔;但只要聊到機身直出(SOOC)的色彩選擇,許多 R5 用戶心裡多少都有個小遺憾:明明與隨後推出的 A7C II、ZV-E1、FX3 採用同世代的 BIONZ XR 影像處理器,Sony 卻在後續機型上新增了廣受好評的 FL2 與 FL3 兩種風格外觀(Creative Look),而身為高階機身的 R5 即使升級到官方 v4.00 韌體,依然停留在原本的 10 種預設外觀。

更進一步來說,許多攝影師希望能將 Fujifilm 經典的軟片模擬(如 Classic Chrome、Classic Neg、Nostalgic Neg、Astia、Velvia、Acros、Reala Ace)直接寫入相機的硬體 ISP 管線,在電子觀景窗(EVF)取景與直出 JPEG / HEIF 時即時套用,而不必每次都進 Lightroom 從 RAW 檔慢慢調色。

近期開源社群出現了針對 R5 v4.00 韌體的逆向工程專案(sony-a7r5-custom-firmware v4.34,韌體標頭版本號編碼為 BCD 0x34, 0x04)。出於對嵌入式系統架構與相機底層運作的好奇,我花了一段時間將整套專案與 Sony 官方 v4.00 的 BODYDATA.DAT(754,525,608 bytes)徹底拆解開來,在 Windows 環境下搭配 LLVM Clang 23.1.4 與 uv 從頭走完解包、雙目標交叉編譯、Unicorn 虛擬 CPU 模擬測試與 RSA-2048 重簽章封裝。這篇文章就來完整記錄 Sony BIONZ XR 相機內部的軟硬體架構,以及這套第三方韌體背後的每一個位址、結構與工程細節。

這套自製韌體做了什麼?

在進入記憶體位址與組語之前,先看看這套 R5 v4.34 第三方韌體在功能層面完成了哪些擴充:

  1. 開啟隱藏的 FL2 與 FL3 風格外觀: 原廠 R5 內建 10 組風格外觀(ST、PT、NT、VV、VV2、FL、IN、SH、BW、SE)。這套韌體將新世代 BIONZ XR 機型(A7C II v2.00、ZV-E1 v2.00、FX3 v6.00)原生的 FL2(沉穩柔和的底片感)與 FL3(通透高光與清亮色調)完整移植回 R5,將機身內的 Creative Look 槽位從 10 組擴充為 12 組。
  2. 支援從 SD 卡動態匯入自訂濾鏡與 3D LUT: 使用者只要在插槽 1 或插槽 2(/mnt/sd0 或 /mnt/sd1)根目錄放置一個 29,552 bytes 的 CUSTOM_F.BIN 或 SONY_LUT.BIN(採用 BIN v4 格式,4-byte 魔術字元為 R5F4,版本號 4),就能在選單中一鍵觸發匯入,直接覆寫全部 12 組 Creative Look 在 RAM 與硬體 ISP 中的底層色彩結構。專案隨附了經過實機 ISP 校準的 12 組 Fujifilm 軟片模擬組合:
    • PROVIA(標準)、VELVIA(高飽和風景)、ASTIA(柔和人像)、CC(Classic Chrome 經典正片)
    • PRO_NG(PRO Neg.Hi)、PRO_NS(PRO Neg.Std)、CL_NEG(Classic Neg. 經典負片)、NOSTAL(Nostalgic Neg. 懷舊負片)
    • ETERNA(電影感)、ET_BLC(Eterna Bleach Bypass 跳漂)、ACROS(高質感黑白)、REALA(Reala Ace)
  3. 完整的原生 UI、動態圖示與 Fn 選單整合: 不同於早期單純替換常數表的硬改做法,v4.34 會在拍攝資訊畫面(HUD)與轉盤上顯示自訂的 6 字元簡稱,例如 CL_NEG、NOSTAL,並在 Creative Look 選單列表中顯示 32 字元完整名稱,同時支援動態解析選單圖示(修正了早期 v4.33 誤將部分槽位索引至說明文字資源的問題,確保 ST 顯示原生 ST 圖示且其餘槽位動態對應正確圖示)。此外,它將「匯入自訂濾鏡」直接註冊為可指派的 Fn 快捷選單項目(項目 ID 0x26e / 0x1f8)。
  4. 出廠備份範本與自動白平衡微調: 一併修補了 original.tar 內全部 12 個地區的出廠設定備份範本(0110_backup/SYSIPSX-DSLR/GL/CX89200_*.bin 與 CX89201_*.bin):將 Creative Look 啟用位元遮罩(bitmask)從 10-bit(0x03ff)擴充為 12-bit(0x0fff),在偏移量 0x76c5d 填入 FL2 與 FL3 的 43-byte 預設參數紀錄,並將自動白平衡(AWB)調整為 White Priority = 97(0x61)與 Ambience Priority = -1(0xff),改善 Sony 原廠在室內暖光下容易偏冷或偏綠的傾向。

BIONZ XR (CXD90057) 的雙處理器與雙作業系統架構

要理解為什麼在相機裡「多加兩個濾鏡槽位」需要動到兩套截然不同的編譯器與修補管線,我們必須先看懂 Sony BIONZ XR(晶片代號 CXD90057)內部的異質雙處理器架構。

檢視從韌體解出的 Device Tree Blob(0800_appli/setting/cxd90057.dtb)、Linux 核心映像(0700_part_image/dev/nflasha3:/vmlinux.bin)以及開源授權清單(0650_prfile/PMBP/LICENSE/CP_TOPRS.txt),可以清楚看見 Sony 的「雙腦分工(split-brain)」設計——將「使用者介面與檔案管理」以及「硬體 ISP 即時影像管線」嚴格切分在兩顆不同架構的處理器與兩套作業系統上:

bionz-xr-architecture.svg

AP 應用處理器:64 位元 SCEL Linux 與 97.3 MB 的 appFw.so

第一顆處理器是應用處理器 AP (Application Processor),在 Device Tree 中宣告為 sony,cxd90057,內部搭載四核心 64 位元 ARM Cortex-A35 (arm,cortex-a35,ARMv8-A 架構) 叢集,並配備 arm,gic-400 中斷控制器、arm,pl011 UART、arm,pl022 SPI 與 arm,pl330 DMA 控制器。

從 nflasha3 分區內的 /vmlinux.bin 提取出的核心版本字串如下:

Linux version 4.9.337-scel (jp10239@dic-build-scm-u20-006)
(gcc version 6.4.0 (20201211 (Sony CE Linux 14.0.1.0)) )
#1 SMP PREEMPT Fri Oct 31 13:51:16 JST 2025

AP 運行的是簡稱 SCEL 的 Sony CE Linux 14.0.1.0。最令人印象深刻的是它的應用程式架構:在 nflasha15(一個大小為 410,652,672 bytes、約 391.6 MB 的 ext2 檔案系統分區)中,幾乎整台相機的 GUI 選單樹、轉盤與 Fn 事件派發、組態屬性儲存(對應 nflasha2 / nflasha6)、記憶卡 I/O 以及與底層硬體的 IPC 通訊,全部被編譯進單一一個高達 97,368,304 bytes(97.3 MB)的 64 位元 ARM64 ELF 共享函式庫——/lib/appFw.so。

0.5 秒瞬間開機的秘密:nflasha5 與 WBI2 休眠快照陷阱

拆解分區時,我們注意到一個非常關鍵的機制:如果讓一顆 Cortex-A35 從頭冷啟動 Linux 4.9 kernel、掛載 nflasha15 並動態連結載入 97.3 MB 的 /lib/appFw.so,開機時間至少需要好幾秒鐘,這對抓拍優先的專業相機來說是不可接受的。

為了達成撥開電源開關後「0.5 秒內進入可拍攝狀態」,Sony 在 nflasha5 分區(243,789,184 bytes,約 232.5 MB)中存放了一份預先壓縮好的記憶體休眠快照,開頭 8 bytes 為魔術字元 WBI2\x00\x00\x00\x00(即 Warm Boot Image 2,十六進位 57 42 49 32)。平常開機時,bootloader 完全跳過正常的 Linux 初始化流程,直接將 nflasha5 的 LZO 壓縮快照解壓縮回 DRAM,瞬間恢復已經載入好 appFw.so 的執行緒狀態!

這在韌體逆向工程中構成了一個極易踩中的陷阱:如果你只修改了 nflasha15 裡的 /lib/appFw.so 與 nflasha8 裡的 cpapp-b.bin,刷機重開後,相機依然會從 nflasha5 的 舊版 WBI2 休眠快照 醒來,導致 DRAM 裡跑的還是未修補的舊版 appFw.so,甚至因為與新版 CP 韌體狀態不一致而引發異常。

解決方案卻優雅得令人會心一笑——只需要在建置管線中修改 nflasha5 開頭 0x0 偏移量的 1 個 byte:

Offset 0x00000000:  57 42 49 32 ("WBI2")  ➔  77 62 69 32 ("wbi2")

把大寫 W(0x57)改為小寫 w(0x77)後,刷機完成後的 第一次開機 時,bootloader 檢查 nflasha5 發現 WBI2 魔術字元失效,便會自動放棄快速喚醒,改從 nflasha3:/vmlinux.bin 與 nflasha15:/lib/appFw.so 執行完整的冷開機(cold boot),並在開機完成後自動將包含新版修補程式的 DRAM 狀態重新寫入 nflasha5,產生全新的 WBI2 快照。從第二次開機起,相機就再次恢復 0.5 秒瞬間喚醒的速度。

CP 相機處理器:32 位元 Cortex-R5 與 TOPPERS/FMP RTOS

第二顆處理器是相機處理器 CP (Camera Processor),採用 32 位元 ARM Cortex-R5 (ARMv7-R 架構),上面運行的是由日本名古屋大學高田實驗室主導開發、符合 μITRON 規範的對稱多處理即時作業系統——TOPPERS/FMP。

CP 的韌體存放於 nflasha8(一個 24,531,968 bytes 的 FAT12 分區)內的 /CPAPP-B.BIN,檔案大小為 17,617,008 bytes(約 16.8 MB)。它是一個無檔頭的純二進位映像(flat binary),在開機時被直接載入至 CP 記憶體空間的基底位址 0x67800000。

CP 負責所有嚴格要求硬體即時性(hard real-time)的底層工作:控制 CMOS 感光元件讀出、驅動 BIONZ XR 硬體 ISP 管線、套用 gamma / tone 曲線與 5×9×9 3D HSV 色彩調整格,以及即時自動對焦(AF)、自動曝光(AE)與快門控制。

拆解 R5 v4.00 原廠 BODYDATA.DAT 與雙態 AES 加密層

要從零建置自製韌體,第一步是從 Sony 官方釋出的 R5 v4.00 更新檔 BODYDATA.DAT(754,525,608 bytes,SHA-256 為 51e1176a255a9a4b4782dfbcfa6c5f332b71173bdfe9ed2bcf4fca6bcbc6c2e2)解出乾淨的原廠分區與二進位基底。

外層 UFU 容器格式

BODYDATA.DAT 以 8-byte 魔術字元 \x89UFU\r\n\x1a\n 開頭(刻意仿照 PNG 檔頭的設計,可用於偵測 FTP / HTTP 文字模式傳輸造成的換行損毀),隨後是由 big-endian 的 (uint32 size, char kind[4]) 組成的五個 chunk:

  1. DATV (4 bytes):容器格式版本號(0x00010000)。
  2. PROV (4 bytes):provisioning 旗標。
  3. UDID (20 bytes):目標機種與更新識別碼。
  4. FDAT (754,525,456 bytes):加密後的韌體本體,由 736,841 個 1,024-byte 加密區塊(共 754,525,184 bytes)加上尾端 272 bytes trailer (16-byte AES-CBC IV + 256-byte RSA-2048 數位簽章) 組成。
  5. DEND (4 bytes):big-endian CRC32 校驗碼,涵蓋從檔案偏移 0 一直到 DEND chunk header 之前的所有位元組。

FDAT 的 1,024-Byte 區塊結構與雙態 AES 加密 (CXD90057_k8)

FDAT 內部的明文串流總長度為 751,577,088 bytes。在打包加密時,Sony 將明文每 1,020 bytes 切為一塊,並在前面加上 4 bytes 的標頭,組成固定 1,024 bytes 的區塊:

struct FdatBlock {
    uint16_t sum16;       // Little-endian: 後續 511 個 uint16_le words 的無號總和 (& 0xFFFF)
    uint16_t sizeAndEnd;  // 低 15 bits 為有效 payload 長度 (1..1020);
                          // Bit 15 (0x8000) 在最後一個區塊 (#736,841) 設為 1,其餘為 0
    uint8_t  payload[1020]; // 不足 1,020 bytes 處以 0xFF 填充
};

接著對這 736,841 個區塊(754,525,184 bytes)進行兩階段加密:

  • 前 512 bytes (Block 0 的前半部):使用 Sony 外層通用金鑰(constants.key_aes)以 AES-128-ECB 模式加密。
  • 剩餘所有位元組 (Offset 512 .. 754,525,184):使用 BIONZ XR 世代專屬金鑰(constants.key_cxd90057_k8)與存放在 signature_offset - 16 處的 16-byte IV,以 AES-CBC 模式加密。

當我們解開全部 736,841 個區塊、驗證每個區塊的 sum16 並剝除 4-byte 標頭後,重組出的 751,577,088 bytes 明文串流(original.fdat)包含三個緊密相連的區段:

  1. Bytes 0 .. 512 (FdatHeader):記錄機種代號 model = 0x11030080、region = 0、版本號 versionMajor = 4、versionMinor = 0x00(自製韌體將偏移 0x20..0x21 改為 BCD 0x34, 0x04 代表 v4.34)、fs.offset = 512、fs.size = 1,253,376、firmwareOffset = 1,253,888、firmwareSize = 750,323,200,以及位於偏移 8..12、涵蓋 bytes 12..512 的標頭 CRC32。
  2. Bytes 512 .. 1,253,888 (updater.img):大小 1,253,376 bytes 的 SquashFS 4.0 映像檔,內含更新流程腳本 /bin/startupdate.sh、/bin/endupdate.sh、/bodylib/libupdaterbody.so 以及 CP 更新韌體 /cp/udtrcp.bin。
  3. Bytes 1,253,888 .. 751,577,088 (original.tar):大小 750,323,200 bytes 的 POSIX tar 封裝檔,內含各個 NAND 分區映像與校驗檔。

我們從 BODYDATA.DAT 依序解出全部 9 個原廠輸入檔案,並與專案的 firmware/r5/inputs.lock.json 進行 SHA-256 比對,100% 吻合:

輸入鍵值 (Input Key) 檔案名稱 大小 (Bytes) SHA-256 雜湊值
original_dat original.DAT 754,525,608 51e1176a255a9a4b...bc6c2e2
original_fdat original.fdat 751,577,088 b3af8557d6279d8b...cfcb0ff
original_tar original.tar 750,323,200 8b03700edb87d293...6565813
updater updater.img 1,253,376 c4b18a82eea52a50...dcce4a9
stock_nflasha5 nflasha5 243,789,184 15020d1afe7f1cf4...1363e8
stock_nflasha8 nflasha8 24,531,968 a625468fbbd6b0e0...1f8f46
stock_nflasha15 nflasha15 410,652,672 197fc934db37ed9b...638fa5
stock_ap appFw.so 97,368,304 41bd8f4cda1f44c7...9ce7c7
stock_cp cpapp-b.bin 17,617,008 3269bf6242633979...7ee7c8

深入 AP 修補工程:appFw.so 的 40.6 KB Code Cave 與 17 處 Branch Hooks

AP 端的建置管線(apply_recipe + build_ap_image.py + build_ap_module.py)對 nflasha15 內的 /lib/appFw.so 執行了兩階段改造:

第一階段:基底 FL2 / FL3 配方修補 (ap_baseline)

首先套用 baseline_recipes.json 中的 42 處二進位修補,將 appFw.so 的檔案大小從原廠的 97,368,304 bytes 擴充至 97,452,032 bytes(在 ELF 可執行區段增加了 +83,728 bytes),打通原廠 Creative Look 表格中對 FL2 與 FL3 的基礎支援。

第二階段:C / ARM64 執行期模組注入 (build_ap_module.py)

接著將 3 個 C 語言原始碼檔案(r5_stage413_transfer_core.c、r5_filter_Menu.c、r5_filter_renderer423.c)與 2 個 ARM64 組語跳板檔(r5_stage413_trampoline.S、r5_filter_menu_trampoline.S)編譯為位置無關(PIC)的執行期 payload,並精準釘選(pin)在 appFw.so 的兩個 ELF 虛擬位址區段:

  • RX code cave (可執行程式碼區段):位於虛擬位址 vaddr = 0x03c97ef8(對應檔案偏移 file_offset = 0x03c97ef8),模組本身佔用 40,600 bytes(連同第一階段基底 cave 合計為 63,096 RX bytes)。
  • BSS state region (執行期狀態緩衝區):位於虛擬位址 vaddr = 0x05cf18c0,大小為 136,384 bytes(0x214c0 bytes),用於在 RAM 中存放從 SD 卡解析出來的 .BIN 濾鏡包、當前啟用的 6/32 字元外觀名稱,以及準備傳送給 CP 的 IPC 傳輸緩衝區。

為了讓原本 appFw.so 的生命週期與選單事件流進這塊 code cave,建置腳本在 appFw.so 內植入了 17 處 ARM64 B / BL call-site hooks:

Hook 名稱 檔案偏移量 (File Offset) 原廠指令 (Hex) 修補後跳轉指令 (Hex) 攔截時機與用途
startup 25,170,660 (0x18012a4) 60a26e39 0544da15 系統啟動時跳入初始化跳板
initial 55,422,568 (0x34daae8) 60c21091 69dd6615 Creative Look 子系統初始狀態建立
completed 55,464,256 (0x34e4dc0) 60c21091 fab46615 屬性載入完成後同步自訂濾鏡狀態
media 54,279,380 (0x33c38d4) a003168b 5b3a6b15 監控 SD 卡掛載狀態(/mnt/sd0、/mnt/sd1)
inbound 55,921,996 (0x35549cc) 20008052 86f66415 攔截並注入跨核心 IPC 濾鏡資料
power 56,306,296 (0x35b26f8) fd7bbba9 a57f6315 電源關閉或進入休眠前保存狀態
menu / UI hooks (×11) 多處 UI 分派點 原生選單指令 跳轉至 r5_filter_Menu 註冊 Fn 項目、繪製 6/32 字元名稱、查詢 v4.34 圖示 (icon_query434) 等 11 處選單攔截點

原地 Ext2 區塊整合 (integrate())

由於 nflasha15 是一整個 410,652,672 bytes 的 ext2 檔案系統映像,把變大後的 appFw.so(97,452,032 bytes)寫回 nflasha15 時不能隨便重新打包整個檔案系統(否則 inode 順序或時間戳記改變可能引發非預期行為)。

建置腳本透過 nflasha15_layout 先為 /lib/appFw.so 的 inode 擴充對應的資料區塊鏈(block chain),接著 integrate() 直接將新版 appFw.so 的內容覆寫進對應的 ext2 data blocks,並在最後執行嚴格的位元級斷言:確保 nflasha15 分區中除了 /lib/appFw.so 佔用的區塊與對應的 ext2 metadata 之外,其餘每一個 byte 都與原廠映像 100% 完全相同。

深入 CP 修補工程:cpapp-b.bin 表格重定位與 Unicorn 虛擬 CPU 模擬

看完了 AP 端,接著來看更刺激的 CP 端(nflasha8:/CPAPP-B.BIN,基底位址 0x67800000)。

為什麼必須重定位 12 組 Look 參數表?

在原廠 R5 v4.00 的 cpapp-b.bin 中,CP 將 10 組 Creative Look(ST..SE)的 ISP 參數存放在固定長度為 10 的連續陣列裡,主要包含三大結構:

  • s_kelvin 與 s_adobekelvin 色溫補償表(每組 0xDC bytes,10 × 0xDC = 2,200 bytes)。
  • tone / gamma / 色彩矩陣結構(每組 0x16B4 = 5,812 bytes)。
  • 5×9×9 3D HSV 色彩調整格:每組 184 bytes,由 5 個明度階層 luma levels × 9 個飽和度環 saturation rings × 9 個色相扇區 hue sectors 組成。

問題在於:在 cpapp-b.bin 的記憶體佈局中,這幾個 10-entry 陣列的屁股後面緊接著其他關鍵的 RTOS 全域變數!如果我們直接在原位址把陣列從 10 組硬撐到 12 組(+FL2, +FL3),多出來的 2 組資料就會直接蓋掉後面的 RTOS 全域變數,導致相機一開機就崩潰。

因此,建置管線(build_cp.py)採取了 完整重定位 (table relocation) 策略:

  1. 在 cpapp-b.bin 後段找到一塊乾淨的未使用記憶體區段 0x688df740(其中可執行區段位於 RX = 0x68995080,包含 7,308 bytes 的 C 執行期程式碼 cp_transfer.c / cp_stage413_probe.c,以及 522,152 bytes 的擴充表格與狀態空間)。
  2. 將擴充為 12 組的完整 look 參數表搬移至 0x688df740。
  3. 自動掃描整個 cpapp-b.bin,一舉修補了 167 處 ARM32 指令——包含將所有查表函式的 literal-pool 基底位址改指到 0x688df740 新表,以及將所有的陣列邊界檢查從 cmp rX, #10 修改為 cmp rX, #12。

刷機前的虛擬 CPU 守門員:Unicorn Engine (getter_gates.py)

一口氣修改了裸機 RTOS 裡的 167 處 ARM32 指令,要怎麼確定沒有任何一條指令改錯、或者不小心破壞了呼叫慣例(calling convention)?

這套專案設計了一道極為嚴謹的防線:src/gamma/getter_gates.py。在每次打包韌體前,它會使用 Unicorn Engine 建立一顆 32 位元 ARM 虛擬 CPU(UC_ARCH_ARM, UC_MODE_ARM),將 17.6 MB 的 cpapp-b.bin 實際映射載入至虛擬位址 0x67800000:

  1. 啟用硬體浮點與向量協同處理器 (VFP / NEON): 因為 CP 的色彩查表與插值函式內部使用了 ARM VFP 指令,模擬器在啟動時會先透過協同處理器暫存器啟用 CP10 與 CP11:
    mrc p15, 0, r0, c1, c0, 2
    orr r0, r0, #0x00f00000   ; 啟用 CP10 / CP11 (VFP/NEON 完全存取權限)
    mcr p15, 0, r0, c1, c0, 2
    mov r0, #0x40000000       ; EN bit in FPEXC
    vmsr fpexc, r0
  2. 遍歷模擬 3 種狀態 × 12 組 look 索引: 模擬器直接設定 PC 呼叫 cpapp-b.bin 內真實的 getter 函式(如 0x684b5148、0x684b5220、0x684b527c 等),針對 look 索引 1..12 依序測試三種執行期狀態:
    • 狀態 A:cold boot 未初始化預設狀態
    • 狀態 B:載入內建 FL2 / FL3 預設值狀態
    • 狀態 C:由 SD 卡 .BIN 濾鏡覆寫全部 12 組槽位狀態
  3. 位元級與暫存器完整性斷言:
    • 針對索引 1..10(原廠 10 種外觀),比對修補後韌體回傳的結構體每一個 byte,必須與未修改的原廠 cpapp-b.bin 100% 逐位元一致。
    • 針對索引 11..12(FL2、FL3),驗證回傳的指標與內容精準對應 0x688df740 重定位表中的資料。
    • 檢查函式返回時的 callee-saved 暫存器(r4..r11 與 sp),確保堆疊與暫存器零污染。
unicorn-emulation-gate.svg

Windows + LLVM Clang 23.1.4 本地建置調適與跨工具鏈比對

上游專案原本是在 macOS 環境下使用 Apple Clang 21(clang 21.1.x)開發,並在腳本中寫死了 /usr/bin/clang 與 /usr/bin/openssl 等路徑。當我們將建置管線移植到 Windows 11 並使用 LLVM Clang 23.1.4 與 uv(cpython-3.12.11)執行本地編譯時,解決了三個關鍵的跨平台與編譯器差異:

  1. Clang 23 新增的全域變數靜態分析警告: LLVM Clang 23 引入了更嚴格的 -Wunused-but-set-global 檢查,在開啟 -Werror 時會攔截 r5_filter_renderer423.c:23 中的診斷計數器 static uint32_t allocation_failures。我們在 build_ap_module.py 與 build_cp.py 的交叉編譯旗標中補上了 -Wno-unused-but-set-variable 與 -Wno-unused-but-set-global。
  2. 0701_part_image_sum/part_image.sum 的 454-Byte CRLF 陷阱: 在打包 original.tar 前,build.py 會重新計算 dev/nflasha5、dev/nflasha8 與 dev/nflasha15 的 CRC32 校驗碼並更新 part_image.sum。因為該建置管線採用原地覆寫 tar 成員的策略,part_image.sum 的檔案大小必須嚴格維持在 454 bytes。 在 Windows 上,Python 的 Path.write_text() 預設會將換行符號 \n 轉換為 \r\n(CRLF),導致這份 6 行校驗檔瞬間膨脹為 460 bytes 並觸發 tar 區塊大小 AssertionError!將寫入方式改為 sums.write_bytes(text.encode('ascii')) 後,便完美保持了 UNIX LF 的 454 bytes 結構。
  3. Apple Clang 21 與 LLVM Clang 23 的產出比對: 我們將 Windows + LLVM Clang 23.1.4 編譯出的成品與原作者 macOS + Apple Clang 21 的參考輸出進行了逐檔案比對:
    • 所有非編譯型資料產出(包含 nflasha5、12 個地區的 CX8920*.bin 出廠備份範本與 backup.sum)的 SHA-256 雜湊值 100% 相同,例如 nflasha5 皆為 eac727efd7af2142ddd9b8ae22387614eaaaba9dc9f04ce0434bf8e7c766d693。
    • 經過 C 編譯器產出的 appFw.so(97,452,032 bytes)與 cpapp-b.bin(17,617,008 bytes)不僅檔案大小完全一致,內部的 RX / BSS 區段尺寸(AP 端為 40,600 B / 136,384 B;CP 端為 7,308 B / 522,152 B)與 17 個 hook 跳板位址也分毫不差,僅在 C 函式內部有 Clang 21 與 Clang 23 之間的微幅指令排程與暫存器分配差異,且全數通過 Unicorn 模擬測試。

密碼學簽章驗證、Partition 1 (nflasha1) 信任鏈與 Android Fastboot

當我們修改完 nflasha5、nflasha8 與 nflasha15,並重新打包、以 CXD90057_k8 加密出 736,841 個 FDAT 區塊後,就來到了整個韌體工程最後、也最關鍵的一道安全防線:數位簽章驗證。

為什麼不能直接把改好的 BODYDATA.DAT 放進原廠相機更新?

在 BODYDATA.DAT 的 FDAT 區段最尾端(檔案偏移 754,525,332,緊接在 736,841 個加密區塊與 16-byte IV 之後),存放著一段 256 bytes 的 RSA-2048 數位簽章,採用 RSASSA-PKCS1-v1_5 填充與 SHA-256 雜湊,簽章範圍涵蓋了前面整整 754,525,200 bytes 的加密串流與 IV。

當你在原廠未改機的 R5 上透過 SD 卡執行韌體更新時,相機其實是重新開機進入 Partition 1 (nflasha1) 的更新環境:

  • nflasha1 是一個 25,100,288 bytes(約 23.9 MB)的 FAT12 復原分區,裡面放著 /boot/initrd.img(一個以 zlib 壓縮的 SquashFS 4.0 根檔案系統)以及記錄檔案 MD5 的 /p1chksum。
  • 在 /boot/initrd.img 內部,/etc/VerificationKey.pem(451 bytes,SHA-256 為 8281413b5607446939613e07d020b2c7d8ee2371349099de56bc8288f7be0a43)存放著 Sony 官方的 RSA-2048 公鑰。
  • 更嚴密的是,initrd.img 裡負責韌體更新的四個核心共享函式庫:
    • /usr/lib/libupdaterapi.so
    • /usr/lib/libsdfileinputupdaterapi.so
    • /usr/lib/libseninputupdaterapi.so
    • /usr/lib/libuudfileinputupdaterapi.so 在執行更新時會進行 雙層驗證:
    1. 第一層公鑰指紋驗證 (_ZN7Updater21SignatureVerification9verifyKeyEPKc):即 Updater::SignatureVerification::verifyKey(char const*),先計算 /etc/VerificationKey.pem 的指紋,檢查它是否等於寫死在 .so 裡的 Sony 官方公鑰雜湊!這意味著就算你只換掉 .pem 公鑰檔,第一層檢查也會直接報錯拒絕。
    2. 第二層密碼學簽章驗證 (_ZN7Updater21SignatureVerification15VerifySignatureEPKhjS2_j):使用 /etc/VerificationKey.pem 對 BODYDATA.DAT 的 FDAT 區段執行標準的 RSA-2048 PKCS#1 v1.5 SHA-256 數位簽章驗證。

一次性硬體引導:RP2040 UART 與隱藏在相機裡的 Android Fastboot v0.5

因為全世界只有 Sony 擁有對應的 RSA-2048 私鑰,任何想要透過 SD 卡刷入自簽章 BODYDATA.DAT 的相機,都必須先透過硬體除錯介面完成一次性的 Partition 1 (nflasha1) 公鑰替換。

fastboot-trust-chain.svg

這套硬體引導流程的底層機制最早由安全研究員 David Buchanan 在 DavidBuchanan314/ILCE-7M4-RE 中發掘,完整運作過程如下:

  1. 透過 Sony Multi-port UART 進入暫時性 SRAM Fastboot: 使用 Sony Multi-port 轉接板連接至一塊刷有 uart_boot 韌體的 RP2040(接線為 GPIO28 ➔ RX、GPIO29 ➔ TX、GPIO27 ➔ Reset,初始握手鮑率為非標準的 48000 baud),同時將相機 USB-C 接上電腦。在重置瞬間,主機透過 UART 將 Sony 官方簽章過的 BootROM SRAM loader(fastboot.bin)串流寫入相機內部 SRAM 位址 0x92028000。隨後 UART 切換至 115200 baud 並印出:

    [mira] fastboot ready
  2. Sony 的 mira loader 採用標準 Google Android Fastboot v0.5: 當 mira 啟動 USB 控制器後,電腦端看到的 USB 裝置識別碼竟然是 Google 官方的 Android Fastboot ID:

    • VID = 0x18d1 (Google Inc.)
    • PID = 0x4ee0 (Android Fastboot)
    • bInterfaceClass = 0xFF、bInterfaceSubClass = 0x42、bInterfaceProtocol = 0x03

    透過 USB bulk endpoint 發送 getvar:version 會直接回傳 OKAY0.5。它不僅完整支援標準 Android Fastboot 指令(getvar:partition-size:nflasha1、download:<hex_len>、upload、flash:nflasha1、reboot)與標準的 OKAY / FAIL / INFO / DATA%08x 封包協定,甚至原生支援 Android Sparse Image 格式(魔術字元 0xed26ff3a,v1.0,512-byte 磁區區塊,使用 0xCAC1 RAW chunk 寫入有變動的磁區、以 0xCAC3 DONT_CARE chunk 跳過未修改磁區)! 此外,Sony 工程師還在 mira 裡實作了三個專屬的 oem 擴充指令:

    • oem partition:列出內部 SDM2 eMMC 上全部 17 個分區(nflasha1 .. nflasha17)。
    • oem partition dump nflasha1(搭配 upload):將 Partition 1 讀取至 RAM 並透過 USB 完整傾印回電腦。
    • oem dumpbrom:傾印 CXD90057 的晶片內部 BootROM。

原地修補 nflasha1 的 8-Byte verifyKey() 與 Sparse Image 回寫

取得相機傾印出的 nflasha1 映像後,build_r5_partition1.py 與 build_r5_verifykey_skip.py 會在不改變分區與壓縮區塊幾何結構的前提下進行原地修補:

  1. 替換公鑰:將 initrd.img 內的 /etc/VerificationKey.pem(451 bytes)替換為使用者自己生成的 RSA-2048 public.pem(同樣為 451 bytes)。
  2. 修補 4 個 updater .so 的 verifyKey() 函式入口: 將 _ZN7Updater21SignatureVerification9verifyKeyEPKc 開頭的 8 bytes(兩條 ARM64 指令)替換掉:
    ; 修補前 (Before): fd 7b b2 a9  e0 03 01 aa
    stp x29, x30, [sp, #-0x50]!
    mov x0, x1
    
    ; 修補後 (After):  00 00 80 52  c0 03 5f d6
    mov w0, #0    ; 永遠回傳 0 (代表自訂公鑰指紋合法)
    ret
    這個 8-byte 修補讓 verifyKey() 認可我們放入的自訂 public.pem,同時 完整保留了 VerifySignature() 的所有密碼學檢查,因此相機在透過 SD 卡刷機時,依然會嚴格驗證 BODYDATA.DAT 的 RSA-2048 數位簽章。
  3. 等長重壓縮與 233 磁區 Sparse Image 刷寫: 腳本只重新壓縮 initrd.img 中受影響的 zlib SquashFS fragments,並精確控制壓縮參數使其與原始 fragment 的 byte 長度完全相同;接著更新 /p1chksum 裡的 initrd.img MD5,最後將 25.1 MB 的 nflasha1 中實際發生變動的 233 個 512-byte 磁區封裝成一個不到 125 KB 的 Android Sparse Image,透過 flash:nflasha1 瞬間寫回相機。

本地生成自簽章 BODYDATA.DAT 與 100% 解密驗證

在完成所有分析與工具鏈調適後,我們在本地生成了一組全新的 2048-bit RSA 金鑰對(e = 65537):

  • 私鑰 private.pem(1,679 bytes),用於簽署自製 BODYDATA.DAT。
  • 公鑰 public.pem / VerificationKey.pem(451 bytes,SHA-256 為 9b2ab38babd06c8aea18a4064ddc7bbd4dd9014ed14c16db369df8363fc229c8),用於注入 nflasha1。

接著我們執行完整的 --stage all 自動化建置與簽章管線:

uv run python firmware/r5/build.py --inputs firmware/r5/inputs.local.json --output D:/code/r5-build-all --stage all

整條管線依序完成了以下工作並產出完整驗證報告(D:\code\r5-build-all\verification.json):

  1. 編譯並整合雙處理器映像:產出修補後的 appFw.so、cpapp-b.bin、nflasha5(WBI2 ➔ wbi2)、nflasha8、nflasha15、part_image.sum(精確 454 bytes)以及 12 個地區的 CX8920*.bin 備份檔與 backup.sum,並全數通過 Unicorn Engine 虛擬 CPU 模擬閘門。
  2. 封裝明文 FDAT 串流:組裝出 751,577,088 bytes 的明文 FDAT 映像(SHA-256 為 a46c6bb0811064cec9dce08b47deee40116ea5d51f255faf26d8814684361635)。
  3. 雙態 AES 區塊加密與 RSA-2048 簽章:將明文切分為 736,841 個 1,024-byte 區塊(754,525,184 bytes),以 AES-ECB + AES-CBC(CXD90057_k8, iv) 加密,使用 private.pem 對加密內容與 IV 產生 256-byte RSASSA-PKCS1-v1_5 / SHA-256 簽章,最後計算最外層 DEND 的 CRC32,產出最終的 BODYDATA.DAT(754,525,608 bytes,SHA-256 為 b267a02ba29a7c8f84cbd38fa11413c903772f73d64ffd34e2a61129533cd0f6)。
  4. 雙向密碼學與解密完整性驗證:
    • 同時透過 PyCryptodome 與獨立的 openssl pkeyutl -verify 驗證 BODYDATA.DAT 尾端的 256-byte RSA-2048 簽章,確認回傳 Signature Verified Successfully,並測試竄改簽章後能被正確拒絕(tampered_signature_rejected: true、outer_crc_valid: true)。
    • 將產出的 BODYDATA.DAT 重新丟回解密器,獨立解開全部 736,841 個區塊,確認解出的每一顆 byte 都與打包前的明文分區 100% 吻合。

結語

回頭看這次對 Sony A7R V 第三方韌體 v4.34 的拆解與本地建置,最讓我著迷的不只是最終在相機上多出了 FL2、FL3 與 12 組 Fujifilm 軟片模擬,而是得以深入現代高階數位相機內部的系統工程設計:

  • 一顆 CXD90057 SoC 同時跑著四核 64 位元 SCEL Linux(負責 97.3 MB 的 /lib/appFw.so 單體應用)與 32 位元 Cortex-R5 TOPPERS/FMP RTOS(在 0x67800000 負責硬體即時 ISP 管線),並透過 nflasha5 的 WBI2 LZO 記憶體休眠快照達成 0.5 秒瞬間開機。
  • 面對高風險的雙架構二進位修改,透過原地 ext2 區塊替換、重定位 12 組 ISP 參數表至 0x688df740,並結合啟用 VFP/NEON 的 Unicorn Engine 虛擬 CPU 模擬閘門,將傳統嵌入式修改「刷上去才知道會不會磚」的不確定性,轉化為刷機前就能 100% 自動化驗證的工程管線。
  • 而在底層 BootROM 的 mira SRAM loader(0x92028000)中遇見熟悉的 Google Android Fastboot v0.5(0x18d1:0x4ee0)與 Sparse Image 協定,並只用 8 bytes(mov w0, #0; ret)優雅地將自己的 RSA-2048 金鑰對接入 nflasha1 信任鏈,更是跨領域逆向工程中那種「原來底層都是相通的」最大樂趣。