M03 · 階梯 2 · L2
程式碼簽章與 entitlements
SuperBlob / CodeDirectory / CDHash(完整 32-byte)、special slot 負索引、CDHash 真算雪崩、entitlement taxonomy、ad-hoc vs CMS、hardened runtime 與 library validation。
從「映像」到「可信的映像」
M02 看到 LC_CODE_SIGNATURE 指向 __LINKEDIT 裡一段 EMBEDDED_SIGNATURE。這一章把它拆開:macOS 怎麼用一個雜湊鏈,把「這支二進位有沒有被動過」變成一個可驗證、改一個 byte 就垮的命題。後面 M04(Gatekeeper)、M05(SIP)、M06(TCC)判「可不可信/有沒有權限」,底層都回到這裡的 CDHash 與 entitlements。
SuperBlob:一個裝雜湊與中介資料的容器
EMBEDDED_SIGNATURE(magic 0xfade0cc0,big-endian)是一個 SuperBlob:開頭一張索引表,指向數個子 blob:
| slot | blob | magic | 內容 |
|---|---|---|---|
| 0 | CodeDirectory | 0xfade0c02 | 雜湊鏈本體(見下) |
| 2 | Requirements | 0xfade0c01 | 「誰可以是這支程式」的指定需求(DR) |
| 5 | Entitlements (XML) | 0xfade7171 | 能力清單(plist) |
| 7 | DER Entitlements | 0xfade7172 | 同上的 DER 編碼(macOS 12+ 強制並存) |
| 0x10000 | CMS Signature | 0xfade0b01 | PKCS#7 簽章;ad-hoc 時為空 |
下面的實驗室預設載入一個 hardened runtime + entitlements 的 arm64 樣本(本機 codesign -o runtime --entitlements … 重簽)。三件事一起看:CodeDirectory 欄位、CDHash 真算、改 entitlements 的雪崩。
程式碼簽章實驗室
模型邊界:CDHash 用瀏覽器 crypto.subtle 真算 SHA-256(雪崩無法造假,與 codesign 的 CandidateCDHashFull 一致)。 但 CMS 簽章鏈僅標 ad-hoc/有 CMS,不偽造 Apple 私鑰簽章驗證(DESIGN.md §5.6 / §10)。上傳檔案不離開瀏覽器。
CodeDirectory 與 CDHash:改一個 byte 就垮
CodeDirectory 是核心。它記錄:
- version(樣本是
0x20500,欄位佈局隨版本擴充:team id、exec segment、runtime version…) - flags(csflags):
ADHOC、RUNTIME(hardened runtime)、REQUIRE_LV(library validation)、GET_TASK_ALLOW… - hashType / hashSize:
SHA256 / 32 - code slots(正索引):把被簽的程式碼逐頁(pageSize,通常 4096)各算一個雜湊
- special slots(負索引):把其他 blob 各算一個雜湊塞進來——
-2Requirements、-3CodeResources、-5Entitlements(XML)、-7DER Entitlements。某 slot 沒有對應 blob 就補零。
CDHash 就是「對整個 CodeDirectory blob 做一次雜湊」。所以:
改一頁程式碼 → 那頁的 code slot 雜湊變 → CodeDirectory 變 → CDHash 變
改 entitlements → slot −5 雜湊變 → CodeDirectory 變 → CDHash 變
實驗室的雪崩面板直接示範:改 entitlements XML 任一字元 → 瀏覽器 Web Crypto 真算 slot −5 的 SHA-256 → 寫回 CodeDirectory → 重算 CDHash。你會看到 cdhash 整串翻掉。對有 CMS 簽章(Apple / Developer ID / notarized)的 binary,CMS 簽的就是這個 CDHash——cdhash 一變,簽章立刻失配,AMFI 拒絕載入。
⚠ CDHash 是完整 32-byte(DESIGN.md §11 P0):SHA-256 的 cdhash 是整整 32 bytes。很多工具(含
codesign的CandidateCDHash、CDHash=行)顯示的是截斷後 20 bytes——那是顯示縮寫。真值是CandidateCDHashFull那 64 個 hex。實驗室算出來的完整值,逐位元等於本機codesign -dvvv的CandidateCDHashFull。
模型邊界:雪崩面板的 SHA-256 是真算,無法造假;但本工具不驗 CMS 簽章鏈(不偽造 Apple 私鑰)。ad-hoc 樣本本就沒有 CMS——它只有 cdhash 當「身分」,沒有「誰背書」。這正是 ad-hoc 與 Developer ID / notarized 的差別。
Entitlements:能力,不是權限
entitlements 是一張能力清單(plist),蓋進簽章裡、由 AMFI 在載入時讀。重點心智模型:
- entitlement ≠ 自動獲得權限。它宣告「這支程式被授權可以要求某能力」,實際還要過 sandbox / TCC / SIP 等閘(後面幾章)。
- 有些 entitlement 是放寬保護的,研究時要會看風險面(實驗室以紅/黃/綠標):
get-task-allow(🔴):允許他者task_for_pid附加——可被除錯/讀寫記憶體。release 不該帶。cs.disable-library-validation(🔴):可載入非同 Team 簽章的 dylib(dylib hijack / 插件濫用面)。cs.allow-dyld-environment-variables(🔴):放行DYLD_*注入。cs.allow-jit/allow-unsigned-executable-memory(🟡):W^X 的受控例外。app-sandbox、network.client(🟢):啟用沙盒 / 沙盒例外能力。
com.apple.private.*是 Apple 保留的:第三方無法合法持有,常見於平台二進位。看到第三方 App 帶 private entitlement,是強烈的異常訊號(M14 偵測會用到)。
ad-hoc / hardened runtime / library validation
- ad-hoc(flag
ADHOC):只有 cdhash,沒有 CMS 背書。clang 在 Apple Silicon 預設這樣簽——能跑、但「誰簽的」是空的。 - hardened runtime(flag
RUNTIME):開了一組執行期限制(限制DYLD_*、未簽記憶體、get-task-allow等),notarization 要求它。 - library validation(flag
REQUIRE_LV):只載入同 Team ID 或平台簽章的程式庫——這是擋 dylib 注入的關鍵;disable-library-validation就是關掉它。
🟢 真機操作卡(對照實驗室)
codesign -dvvv /bin/ls # CodeDirectory、CDHash、flags、Team ID
codesign -d --entitlements - --xml /path/app # 取出 entitlements(XML)
codesign -d --extract-certificates /path/app # 取出 CMS 憑證鏈(Developer ID/Apple)
codesign --verify --deep --strict -vvvv app # 驗證簽章是否完整、未被竄改
codesign -d -r- /path/app # 反編譯該 binary 的 Designated Requirement 成文字
把
codesign -dvvv的CandidateCDHashFull和實驗室算出的 32-byte cdhash 並排——逐位元相同。要拆你自己的 binary,用實驗室的「上傳」(檔案不離開瀏覽器)。⚠ 不要對系統 binary 做任何重簽/修改;只做唯讀觀察(DESIGN.md §10)。
下一步:M04 Gatekeeper / quarantine / XProtect — 簽章只是「能不能信」的一半,另一半是「這支東西怎麼來的」(quarantine 旗標、notarization 票根、首次執行評估)。