M00 · 階梯 1 · L1
前置暖身:指標、ARM64、runtime、簽章密碼學
四塊認知鷹架——C/指標心智模型、ARM64 呼叫慣例與反組譯讀寫(含 PAC 直覺)、ObjC/Swift runtime 與 name mangling、公鑰密碼學/雜湊/數位簽章(SHA-256 雪崩、私鑰簽 公鑰驗),為後面所有模組鋪底。
為什麼先暖身
後面每一章都會悄悄假設你「已經會」四件事:能把一個位址當成「指向某處的數字」來想;能讀懂幾行 ARM64 組語在搬什麼、跳去哪;知道 Objective-C 的方法呼叫其實是一次 runtime 查表、Swift 的符號為什麼長那樣;以及——最關鍵——知道「一個雜湊改一個 bit 會全變」與「私鑰簽、公鑰驗」這兩句話到底在說什麼。
這一章不教任何 macOS 安全機制本身,只把這四塊心智模型先架好。架好之後,M01 的「每一級用上級公鑰驗下級」、M02 的 Mach-O 載入、M03 的 CDHash 雪崩,都會從「天書」變成「啊,就是那個」。
⚠ 本章是純認知鋪墊,不是攻擊教學。 我們建立的是「讀得懂、想得通」的能力,不是「做得出 exploit」的能力。所有反組譯、簽章內容都以唯讀觀察為界。
一、C 與指標:位址就是一個數字
把記憶體想成一條很長的格子帶,每格一個 byte,每格有一個編號(位址)。指標就是「裝著某格編號的變數」。它本身也是個數字,只是我們約定要拿這個數字去「看別格的內容」。
| 寫法 | 讀作 | 直覺 |
|---|---|---|
int x = 5; | x 這格裝 5 | 值 |
int *p = &x; | p 裝 x 的位址 | 指標=門牌號 |
*p | 去 p 指的那格,把內容拿出來 | 解參考(dereference) |
p + 1 | 往後一個 int 的距離 | 指標算術看型別大小 |
三個一直會用到的概念:
- 解參考一個壞指標 = 去敲一個不存在/不該敲的門。 NULL、已釋放、或亂算出來的位址,敲下去就是 crash 或更糟。記憶體破壞類漏洞(M13)的整個故事,都是「程式以為某格還是它的,其實不是了」。
- 棧(stack)vs 堆(heap):區域變數、函式呼叫的返回資訊放在棧上,往低位址長;
malloc來的放在堆上。後面 heap 模型(M13 教學抽象)只在乎「物件一個個排在堆上、釋放後格子可能被別人拿走」這件事。 - 結構就是「連續幾格的佈局約定」。Mach-O header、CodeDirectory(M02/M03)本質都是「在某位址開始,第幾 byte 是什麼欄位」的約定。會看 struct,就會看二進位格式。
⚠ 重點:指標不神祕,它只是「一個被約定要當門牌用的數字」。整個系統安全的很多假設,都建立在「這個門牌真的指向我以為的東西、而且那東西沒被換掉」——一旦這假設破了,就是漏洞。
二、ARM64:讀得懂幾行組語在幹嘛
Apple Silicon 是 ARM64(AArch64)。你不需要會寫組語,但要能讀——看反組譯時知道資料在哪、控制流去哪。
暫存器與最小指令集
| 暫存器 | 用途(呼叫慣例 AAPCS64) |
|---|---|
x0–x7 | 傳前 8 個整數/指標參數;x0 同時是回傳值 |
x8 | indirect result location register(大型結構回傳時,呼叫前由 caller 放回傳緩衝區位址) |
x9–x15 | caller-saved 暫存(呼叫者要自己保) |
x19–x28 | callee-saved(被呼叫者要還原) |
x29 (fp) | frame pointer,串接呼叫框 |
x30 (lr) | link register,存「呼叫完要回去哪」 |
sp | stack pointer |
pc | program counter(不可直接寫) |
w0–w30 是同一組暫存器的 32-bit 低半。最常見的幾條指令,讀懂就夠起步:
mov x0, #1 ; x0 = 1
add x0, x0, x1 ; x0 = x0 + x1
ldr x0, [x1] ; 從 x1 指的位址載入到 x0(解參考)
str x0, [x1, #8] ; 把 x0 存到 (x1+8) 的位址
bl _foo ; 呼叫 _foo:把「下一條的位址」寫進 lr,再跳過去
ret ; 跳回 lr(從函式返回)
cmp x0, #0 ; 比較,設旗標
b.eq loc_100 ; 條件分支:相等才跳
讀組語只看三件事:資料搬到哪(mov/ldr/str)、算了什麼(add/sub…)、控制流去哪(b/bl/ret/b.cond)。bl 進函式、ret 出函式、參數在 x0 起、回傳在 x0——掌握這條主軸,M02、M12 的反組譯就不再勸退。
ℹ 平台註(Apple Silicon arm64):進核心不是用
x8(那是 Linux AArch64 的慣例),而是把系統呼叫號放進x16,再執行svc #0x80。號碼正負分流:正號=BSD syscall(如getpid在x16放0x14),負號=Mach trap(如mach_vm_allocate放-0xa)。這和 x86 的分派模型不同——課程主打 Apple Silicon,看到mov x16, #…; svc #0x80就知道「這裡在進核心」,正負號告訴你走 BSD 還是 Mach 介面。本章只需認得這個形狀,細節留給 M08(Mach trap)與 M12(逆向)。
PAC:把「指標也簽名」的直覺
arm64e(Apple Silicon)有 Pointer Authentication(PAC)。直覺一句話:某些指標(如返回位址 lr、函式指標)在存進記憶體前,CPU 會用一把硬體金鑰算一個「簽名」塞進指標用不到的高位元;用之前再驗一次簽名,對不上就 fault。
未簽: 0x0000000100003f40
PAC簽: 0x00a1000100003f40 ← 高位元那段是 signature(示意,非真值)
- 目的:讓攻擊者無法隨意偽造一個指標去改控制流——你改了指標,簽名就對不上。這把記憶體破壞的「最後一步」變得很貴(M13 會談這場軍備競賽)。
- 你會在反組譯看到
paci*/auti*(簽 / 驗)、blraa、retab這類帶a/b後綴的指令——看到就知道「這裡在簽/驗指標」。 - 研究/偵測視角:PAC 不是「不可能繞過」,而是抬高成本、把攻擊者逼向 data-only 等其他面。我們在這裡只建立「指標被簽名保護」的直覺,不討論任何繞過手法。
⚠ 重點:
bl存返回位址、ret用返回位址——而 PAC 就是在這條「存了又用」的路上加一道簽章閘。理解這條路,後面 mitigation 的章節才有立足點。
三、ObjC / Swift runtime 與 name mangling
反組譯 macOS 程式,你看到的常常不是乾淨的 C 函式名,而是 runtime 分派與被 mangle 過的符號。先建立直覺。
Objective-C:方法呼叫是一次查表
ObjC 的 [obj doThing:arg] 不是直接 bl 到某固定位址,而是編譯成一次 runtime 呼叫:
objc_msgSend(obj, @selector(doThing:), arg);
// ^x0 ^x1 (selector) ^x2
objc_msgSend拿obj的 class、用 selector(方法名字串的代號)去查方法表(method cache → class 方法列表),找到實作(IMP)再跳過去。- 對讀組語的意義:看到大量
bl _objc_msgSend,真正呼叫了哪個方法藏在x1(selector)裡,不是看跳轉目標。逆向 ObjC(M12)的核心技巧就是「還原 selector 與 class」。 - 安全面直覺:分派是動態的——這帶來彈性,也帶來「呼叫目標在執行期才定」的分析難度與某些濫用面(後面模組視需要再談,本章不展開)。
Swift:被 mangle 的符號 _$s…
Swift 為了把型別、泛型、模組資訊塞進一個 C 連結符號,會做 name mangling。你會看到像這樣的符號:
_$s4Demo3FooC3baryyF
└┬┘ └┬┘└┬┘└┬┘└┬┘└┤
│ │ │ │ │ └ F = function
│ │ │ │ └ y y = 無參數、無回傳(Void)
│ │ │ └ bar = 方法名(3=長度)
│ │ └ C = class
│ └ Foo(3=長度)
└ Demo 模組(4=長度) ; _$s 是 Swift 5 的 mangling 前綴
你不用手解——重點是認得:看到 _$s 開頭就是 Swift 符號,且它把「模組. 型別. 方法 (簽名)」全編碼進去了。實機用 xcrun swift-demangle 一鍵還原(見操作卡;上面的符號會還原成 Demo.Foo.bar() -> ())。$S(大寫)是更早期前綴,看到也別意外(驗證於 Swift 版本)。
⚠ 重點:ObjC 看
objc_msgSend+ selector,Swift 看_$smangled 符號。這兩件事是 M12 逆向章能不能讀懂 macOS 二進位的分水嶺,這裡先讓你「認得出來」。
四、公鑰密碼學、雜湊、數位簽章
這一節是 M03(程式碼簽章)的前置,務必把直覺建對,否則 CDHash 雪崩會看不懂。
雜湊:單向、定長、雪崩
雜湊函式(如 SHA-256)把任意長度輸入,壓成固定長度輸出(SHA-256 = 32 bytes = 256 bits)。三個性質:
- 單向:給雜湊值,回推輸入在計算上不可行。
- 定長:輸入 1 byte 或 1 GB,輸出都是 32 bytes。
- 雪崩(avalanche):輸入改 1 個 bit,輸出大約一半的 bit 會翻,且看起來毫無規律。
雪崩是 M03 的靈魂。直覺實驗(操作卡裡可真跑):
echo -n "hello" → SHA-256: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
echo -n "hellp" → SHA-256: 完全不同的一串(只改了最後一個字母 o→p)
兩個輸入只差一個字母,輸出沒有任何「接近」可言——沒有「改一點點、雜湊也只變一點點」這種事。所以 macOS 才敢用「雜湊一致」當作「一個 byte 都沒被動過」的證明:改任一 byte → 雜湊全變 → 立刻被抓。
下面親手玩:改 A 或 B 任一字元,看兩個 SHA-256 並排逐 nibble 高亮差異、數有多少 bit 翻轉:
SHA-256 雪崩玩具
Web Crypto 真算(crypto.subtle.digest('SHA-256'))。這個「改一點 → 全變」是 macOS 敢用「雜湊一致=沒被動過」的基礎——直接接 M03 的 CDHash 雪崩。
公鑰密碼學與數位簽章:私鑰簽,公鑰驗
非對稱密碼學給每個主體一對金鑰:私鑰(保密) 與 公鑰(公開),數學上成對、但無法由公鑰反推私鑰。數位簽章的方向是這條:
簽(持私鑰者): message → SHA-256 → digest → 用「私鑰」加密/簽 → signature
驗(任何人,持公鑰): signature → 用「公鑰」驗 → 得回 digest' ; 再自己算 SHA-256(message) → digest
digest' == digest ? 相等 ⇒ (a)沒被改 (b)確實是私鑰持有者簽的
兩句口訣記死:
- 私鑰簽、公鑰驗。 只有持私鑰者能產生對得上的簽章;任何持公鑰的人都能驗,但驗的人造不出新簽章。
- 簽的是雜湊,不是整份資料。 所以「資料改一個 byte → 雜湊變 → 和簽章裡的 digest 對不上 → 驗證失敗」。這正是 M03 裡「改 entitlements 一個字元,CDHash 翻掉,CMS 簽章立刻失配」的底層原因。
| 詞 | 它保證什麼 | 它不保證什麼 |
|---|---|---|
| 雜湊(hash) | 完整性(沒被改) | 來源(誰做的) |
| 數位簽章(signature) | 完整性 + 來源(私鑰持有者背書) | 私鑰沒外洩、信任根可信 |
| 憑證鏈(cert chain) | 「這把公鑰屬於誰」由上級背書直到信任根 | 信任根本身的可信(那是另一個假設) |
⚠ 重點:「雜湊一致 ⇒ 內容沒被改」「簽章驗過 ⇒ 內容沒被改 且 是該私鑰持有者背書」。macOS 的整條信任鏈(M01 開機鏈、M03 簽章、M04 notarization)都是這兩句話的反覆套用。把這兩句想透,後面一路順。
🟢 真機操作卡(macOS 26 適用 · 全唯讀/安全)
以下指令都不修改系統,只觀察。在你自己的 Mac 上跑,把輸出對照本章四節的直覺。
# ── 雜湊雪崩:親手看一個字之差,輸出全變 ──
printf 'hello' | shasum -a 256 # 看:輸出是 64 hex(=32 bytes)
printf 'hellp' | shasum -a 256 # 看:和上一條毫無相似 → 雪崩
# ── ARM64 反組譯:讀 mov/ldr/bl/ret 與函式邊界 ──
otool -tvV /bin/ls | head -40 # 看:x0 起的參數、bl 呼叫、ret 返回;arm64e 上會看到 pacibsp/paciza
# 等 paci*/auti*/retab 系列(PAC 簽/驗指標)
nm -arch arm64e /bin/ls | head # 看:符號表(外部符號標 U)。系統 binary 的 arm64 slice 都是
# arm64e(PAC ABI)→ 用 -arch arm64e;純 arm64 是給非 PAC 的第三方程式
# ── ObjC 分派與 Swift mangling:認得出符號 ──
nm /System/Applications/Calculator.app/Contents/MacOS/Calculator 2>/dev/null | grep -m3 '_\$s'
# 看:_$s 開頭即 Swift mangled 符號(含 Swift 的系統 app 才看得到;
# /usr/bin/swift 是 driver shim,沒有這類符號,別拿它試)
echo '_$s4Demo3FooC3baryyF' | xcrun swift-demangle # 看:還原成 Demo.Foo.bar() -> ()(人類可讀)
otool -ov /System/Applications/Calculator.app/Contents/MacOS/Calculator 2>/dev/null | head -30
# 看:ObjC class/method 中介資料(__objc_* 區段)
# ── 公鑰/簽章直覺:用一把臨時金鑰,親手簽與驗(在你自己的工作目錄,不碰系統)──
openssl genpkey -algorithm RSA -out /tmp/demo_priv.pem # 產生私鑰
openssl pkey -in /tmp/demo_priv.pem -pubout -out /tmp/demo_pub.pem # 導出公鑰
printf 'attest-me' > /tmp/msg.txt
openssl dgst -sha256 -sign /tmp/demo_priv.pem -out /tmp/msg.sig /tmp/msg.txt # 私鑰簽
openssl dgst -sha256 -verify /tmp/demo_pub.pem -signature /tmp/msg.sig /tmp/msg.txt
# 看:Verified OK;改 msg.txt 任一字再驗 → Verification Failure(雪崩+簽章)
⚠ 危險/越界操作(本章不做,僅標示界線):任何對系統 binary 的重簽、修改、停用簽章驗證,或停用 SIP(
csrutil disable,需 recoveryOS)、修改開機策略(bputil,需授權)都不屬於暖身,會在對應模組以受控、唯讀或概念方式處理。本章只用上面這些唯讀/沙箱內指令。/tmp下的 demo 金鑰用完可自行刪除。
下一步:M01 macOS 整體架構與啟動鏈 — 把「私鑰簽、公鑰驗」和「雜湊雪崩」放大成一整條信任鏈:BootROM→iBoot→kernelcache→sepOS,每一級用上級公鑰驗下級的量測雜湊,單點竄改即全鏈中止。