Suzu macOS Playground

M00 · 階梯 1 · L1

前置暖身:指標、ARM64、runtime、簽章密碼學

四塊認知鷹架——C/指標心智模型、ARM64 呼叫慣例與反組譯讀寫(含 PAC 直覺)、ObjC/Swift runtime 與 name mangling、公鑰密碼學/雜湊/數位簽章(SHA-256 雪崩、私鑰簽 公鑰驗),為後面所有模組鋪底。

● lldb · otool · nm · swift-demangle · shasum · openssl

為什麼先暖身

後面每一章都會悄悄假設你「已經會」四件事:能把一個位址當成「指向某處的數字」來想;能讀懂幾行 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 同時是回傳值
x8indirect result location register(大型結構回傳時,呼叫前由 caller 放回傳緩衝區位址)
x9–x15caller-saved 暫存(呼叫者要自己保)
x19–x28callee-saved(被呼叫者要還原)
x29 (fp)frame pointer,串接呼叫框
x30 (lr)link register,存「呼叫完要回去哪」
spstack pointer
pcprogram 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 看 _$s mangled 符號。這兩件事是 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 雪崩玩具

sha256(A):

sha256(B):

0 / 256 bit 不同(0%)改一個字元 → 約一半 bit 翻轉、毫無「接近」可言(雪崩)。

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,每一級用上級公鑰驗下級的量測雜湊,單點竄改即全鏈中止。