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 第三方韌體在功能層面完成了哪些擴充:
- 開啟隱藏的
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 組。 - 支援從 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)
- 完整的原生 UI、動態圖示與 Fn 選單整合:
不同於早期單純替換常數表的硬改做法,v4.34 會在拍攝資訊畫面(HUD)與轉盤上顯示自訂的 6 字元簡稱,例如
CL_NEG、NOSTAL,並在 Creative Look 選單列表中顯示 32 字元完整名稱,同時支援動態解析選單圖示(修正了早期 v4.33 誤將部分槽位索引至說明文字資源的問題,確保ST顯示原生ST圖示且其餘槽位動態對應正確圖示)。此外,它將「匯入自訂濾鏡」直接註冊為可指派的Fn快捷選單項目(項目 ID0x26e/0x1f8)。 - 出廠備份範本與自動白平衡微調:
一併修補了
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 即時影像管線」嚴格切分在兩顆不同架構的處理器與兩套作業系統上:
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 2025AP 運行的是簡稱 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:
DATV(4 bytes):容器格式版本號(0x00010000)。PROV(4 bytes):provisioning 旗標。UDID(20 bytes):目標機種與更新識別碼。FDAT(754,525,456 bytes):加密後的韌體本體,由 736,841 個 1,024-byte 加密區塊(共754,525,184 bytes)加上尾端 272 bytes trailer (16-byteAES-CBC IV +256-byteRSA-2048 數位簽章) 組成。DEND(4 bytes):big-endianCRC32校驗碼,涵蓋從檔案偏移0一直到DENDchunk 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)包含三個緊密相連的區段:
- Bytes
0 .. 512(FdatHeader):記錄機種代號model = 0x11030080、region = 0、版本號versionMajor = 4、versionMinor = 0x00(自製韌體將偏移0x20..0x21改為 BCD0x34, 0x04代表v4.34)、fs.offset = 512、fs.size = 1,253,376、firmwareOffset = 1,253,888、firmwareSize = 750,323,200,以及位於偏移8..12、涵蓋 bytes12..512的標頭CRC32。 - 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。 - 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,096RX 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色溫補償表(每組0xDCbytes,10 × 0xDC = 2,200 bytes)。- tone / gamma / 色彩矩陣結構(每組
0x16B4=5,812 bytes)。 5×9×93D 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) 策略:
- 在
cpapp-b.bin後段找到一塊乾淨的未使用記憶體區段0x688df740(其中可執行區段位於RX = 0x68995080,包含7,308 bytes的 C 執行期程式碼cp_transfer.c/cp_stage413_probe.c,以及522,152 bytes的擴充表格與狀態空間)。 - 將擴充為 12 組的完整 look 參數表搬移至
0x688df740。 - 自動掃描整個
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:
- 啟用硬體浮點與向量協同處理器 (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 - 遍歷模擬 3 種狀態 × 12 組 look 索引:
模擬器直接設定
PC呼叫cpapp-b.bin內真實的 getter 函式(如0x684b5148、0x684b5220、0x684b527c等),針對 look 索引1..12依序測試三種執行期狀態:- 狀態 A:cold boot 未初始化預設狀態
- 狀態 B:載入內建
FL2/FL3預設值狀態 - 狀態 C:由 SD 卡
.BIN濾鏡覆寫全部 12 組槽位狀態
- 位元級與暫存器完整性斷言:
- 針對索引
1..10(原廠 10 種外觀),比對修補後韌體回傳的結構體每一個 byte,必須與未修改的原廠cpapp-b.bin100% 逐位元一致。 - 針對索引
11..12(FL2、FL3),驗證回傳的指標與內容精準對應0x688df740重定位表中的資料。 - 檢查函式返回時的 callee-saved 暫存器(
r4..r11與sp),確保堆疊與暫存器零污染。
- 針對索引
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)執行本地編譯時,解決了三個關鍵的跨平台與編譯器差異:
- 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。 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'))後,便完美保持了 UNIXLF的454 bytes結構。- 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在執行更新時會進行 雙層驗證:
- 第一層公鑰指紋驗證 (
_ZN7Updater21SignatureVerification9verifyKeyEPKc):即Updater::SignatureVerification::verifyKey(char const*),先計算/etc/VerificationKey.pem的指紋,檢查它是否等於寫死在.so裡的 Sony 官方公鑰雜湊!這意味著就算你只換掉.pem公鑰檔,第一層檢查也會直接報錯拒絕。 - 第二層密碼學簽章驗證 (
_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) 公鑰替換。
這套硬體引導流程的底層機制最早由安全研究員 David Buchanan 在 DavidBuchanan314/ILCE-7M4-RE 中發掘,完整運作過程如下:
-
透過 Sony Multi-port UART 進入暫時性 SRAM Fastboot: 使用 Sony Multi-port 轉接板連接至一塊刷有
uart_boot韌體的 RP2040(接線為GPIO28 ➔ RX、GPIO29 ➔ TX、GPIO27 ➔ Reset,初始握手鮑率為非標準的48000baud),同時將相機 USB-C 接上電腦。在重置瞬間,主機透過 UART 將 Sony 官方簽章過的 BootROM SRAM loader(fastboot.bin)串流寫入相機內部 SRAM 位址0x92028000。隨後 UART 切換至115200baud 並印出:[mira] fastboot ready -
Sony 的
miraloader 採用標準 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 磁區區塊,使用0xCAC1RAWchunk 寫入有變動的磁區、以0xCAC3DONT_CAREchunk 跳過未修改磁區)! 此外,Sony 工程師還在mira裡實作了三個專屬的oem擴充指令:oem partition:列出內部SDM2eMMC 上全部 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 會在不改變分區與壓縮區塊幾何結構的前提下進行原地修補:
- 替換公鑰:將
initrd.img內的/etc/VerificationKey.pem(451 bytes)替換為使用者自己生成的 RSA-2048public.pem(同樣為451 bytes)。 - 修補 4 個 updater
.so的verifyKey()函式入口: 將_ZN7Updater21SignatureVerification9verifyKeyEPKc開頭的 8 bytes(兩條 ARM64 指令)替換掉:這個 8-byte 修補讓; 修補前 (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 (代表自訂公鑰指紋合法) retverifyKey()認可我們放入的自訂public.pem,同時 完整保留了VerifySignature()的所有密碼學檢查,因此相機在透過 SD 卡刷機時,依然會嚴格驗證BODYDATA.DAT的 RSA-2048 數位簽章。 - 等長重壓縮與 233 磁區 Sparse Image 刷寫:
腳本只重新壓縮
initrd.img中受影響的 zlib SquashFS fragments,並精確控制壓縮參數使其與原始 fragment 的 byte 長度完全相同;接著更新/p1chksum裡的initrd.imgMD5,最後將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):
- 編譯並整合雙處理器映像:產出修補後的
appFw.so、cpapp-b.bin、nflasha5(WBI2➔wbi2)、nflasha8、nflasha15、part_image.sum(精確454 bytes)以及 12 個地區的CX8920*.bin備份檔與backup.sum,並全數通過Unicorn Engine虛擬 CPU 模擬閘門。 - 封裝明文
FDAT串流:組裝出751,577,088 bytes的明文FDAT映像(SHA-256 為a46c6bb0811064cec9dce08b47deee40116ea5d51f255faf26d8814684361635)。 - 雙態 AES 區塊加密與 RSA-2048 簽章:將明文切分為
736,841個 1,024-byte 區塊(754,525,184 bytes),以AES-ECB+AES-CBC(CXD90057_k8, iv)加密,使用private.pem對加密內容與 IV 產生 256-byteRSASSA-PKCS1-v1_5 / SHA-256簽章,最後計算最外層DEND的CRC32,產出最終的BODYDATA.DAT(754,525,608 bytes,SHA-256 為b267a02ba29a7c8f84cbd38fa11413c903772f73d64ffd34e2a61129533cd0f6)。 - 雙向密碼學與解密完整性驗證:
- 同時透過
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 軟片模擬,而是得以深入現代高階數位相機內部的系統工程設計:
- 一顆
CXD90057SoC 同時跑著四核 64 位元 SCEL Linux(負責 97.3 MB 的/lib/appFw.so單體應用)與 32 位元 Cortex-R5 TOPPERS/FMP RTOS(在0x67800000負責硬體即時 ISP 管線),並透過nflasha5的WBI2LZO 記憶體休眠快照達成 0.5 秒瞬間開機。 - 面對高風險的雙架構二進位修改,透過原地 ext2 區塊替換、重定位 12 組 ISP 參數表至
0x688df740,並結合啟用 VFP/NEON 的Unicorn Engine虛擬 CPU 模擬閘門,將傳統嵌入式修改「刷上去才知道會不會磚」的不確定性,轉化為刷機前就能 100% 自動化驗證的工程管線。 - 而在底層 BootROM 的
miraSRAM loader(0x92028000)中遇見熟悉的 Google Android Fastboot v0.5(0x18d1:0x4ee0)與 Sparse Image 協定,並只用 8 bytes(mov w0, #0; ret)優雅地將自己的 RSA-2048 金鑰對接入nflasha1信任鏈,更是跨領域逆向工程中那種「原來底層都是相通的」最大樂趣。