Suzu macOS Playground

M03 · 階梯 2 · L2

程式碼簽章與 entitlements

SuperBlob / CodeDirectory / CDHash(完整 32-byte)、special slot 負索引、CDHash 真算雪崩、entitlement taxonomy、ad-hoc vs CMS、hardened runtime 與 library validation。

● 前置:M02 codesign · otool · dyld_info · csreq

從「映像」到「可信的映像」

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:

slotblobmagic內容
0CodeDirectory0xfade0c02雜湊鏈本體(見下)
2Requirements0xfade0c01「誰可以是這支程式」的指定需求(DR)
5Entitlements (XML)0xfade7171能力清單(plist)
7DER Entitlements0xfade7172同上的 DER 編碼(macOS 12+ 強制並存)
0x10000CMS Signature0xfade0b01PKCS#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 各算一個雜湊塞進來——-2 Requirements、-3 CodeResources、-5 Entitlements(XML)、-7 DER 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 票根、首次執行評估)。