研究Fairplay DRM(Digital Rights Management,即數字版權保護)最關鍵的兩點是授權和加密。但長久以來,關於App DRM的研究卻很少,而就是在這樣的前提下,Fairplay DRM又為iOS App的安全研究疊加了一層“阻礙”。我們通過分析混淆系統的設計和實現過程中的問題,克服除錯跟蹤的障礙,設計了多種靜態和動態的對抗方案;同時通過大量的逆向工程,填補了安全研究人員對macOS系統機制中,關於Fairplay這一部分的認知空白。
什麼是DRM?
DRM全稱Digital Rights Management,即數字版權保護。蘋果為了保護App Store分發的音樂/視訊/書籍/App免於盜版,開發了Fairplay DRM技術,並申請了很多相關的專利,比較有代表性的如:
- US8934624B2: Decoupling rights in a digital content unit from download
- US8165286B2: Combination white box/black box cryptographic processes and apparatus
- ES2373131T3: Safe distribution of content using descifrado keys
長久以來,關於App DRM的研究很少,而DRM的關鍵是授權和加密。破解Fairplay DRM加密的方式俗稱“砸殼”,這是進行iOS App安全研究的必要前提。自從2013年蘋果引入App DRM機制以後,誕生了如Cluth、Bagbak、Flexdecrypt這樣的經典“砸殼工具”,而此類“砸殼工具”通常需要越獄裝置的支援,因此具有一定的侷限性。
2020年釋出的M1 Mac將Fairplay DRM機制引入了MacOS,由於Mac裝置的許可權沒有iOS嚴格,因此我們得以在MacOS上探索更多Fairplay DRM的原理,最終目標是使解密流程不受Apple平臺的限制。下面,我們先來聊聊Apple中是如何實現的?
Apple上DRM的實現:Fairplay DRM
LC_ENCRYPTION_INFO中的標記
加密的MachO含有LC_ENCRYPTION_INFO欄位,其中cryptoff標識了加密部分在檔案中的起始偏移,cryptsize標識了加密部分的尺寸,cryptid則表明了加密的方法。Fairplay DRM保護下的App,其加密尺寸為4096的倍數,加密方式標識為1。
而負責解密Mach-O的元件主要包括:核心態的FairplayIOKit和使用者態的fairplayd。
Fairplay的Open
MacOS的XNU Kernel中有text_crypter_create_hook這個匯出符號,IOTextEncryptionFamily驅動則註冊了這個Hook,並作為橋樑,將呼叫轉發給了FairplayIOKit核心驅動。
最終負責處理的函式是:
com_apple_driver_FairPlayIOKit::xhU6d1(
char const* executable_path,
long long cpu_type,
long long cpu_subtype,
rp6S0jzg** out_handle
)
此後,核心中的FairplayIOKit開始初始化,通過host_get_special_port中的unfreed port傳送MIG呼叫到使用者態的fairplayd,fairplayd開始處理SC_Info目錄下的sinf和supp檔案,並將處理的資料返回給核心中的FairplayIOKit。
注:使用者態的fairplayd具體工作流程不在本文討論範圍內。
其中MIG呼叫的結構如下:
struct FPRequest{
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_ool_descriptor_t ool;
NDR_record_t ndr;
uint32_t size;
uint64_t cpu_type;
uint64_t cpu_subtype;
};
struct FPResponse{
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_ool_descriptor_t ool1; //supf檔案對映
mach_msg_ool_descriptor_t ool2; //unk,正比與加密內容的尺寸
uint64_t unk1;
uint8_t unk2[136];
uint8_t unk3[84];
uint32_t size1;
uint32_t size2;
uint64_t unk5;
};
完成所有呼叫後,返回的結構rp6S0jzg*實際是一個uint32_t型別的handle,接下來則可以用這個handle來完成解密操作。
Fairplay的Decrypt Page
前面提到的Fairplay Open操作最終返回了一個pager_crypt_info的結構體,其中page_decrypt的Hook由IOTextEncryptionFamily驅動接管,並最終轉發給FairplayIOKit。
最後,FairplayIOKit中負責解密的函式定義如下:
com_apple_driver_FairPlayIOKit::bvqhJ(
rp6S0jzg *hanlde,
unsigned long long offset,
unsigned char const* src,
unsigned char * dst
)
至此,Fairplay的解密邏輯完成呼叫。值得注意的是,在Fairplay DRM中,page的概念為4096bytes。
那麼,使用者態fairplayd處理的sinf和supp檔案又是什麼樣子的呢?
SINF和SUPF檔案
結構
使用者態的fairplayd會讀取隨IPA攜帶的兩個重要檔案:SINF和SUPF,儲存在App的SC_Info目錄下。
其中SUPF檔案和IPA一起分發,每個使用者的IPA和SUPF檔案都是一致的,其中SUPF檔案中儲存了加密Mach-O的金鑰,但是金鑰本身被另外的機制加密。而SINF檔案則作為每個使用者的DRM許可,記錄了購買使用者的識別符號和姓名,以及解密SUPF需要的資訊,因此在Sandbox策略下,App無法讀取自身的SINF檔案,以防止其被作為唯一ID追蹤使用者。
SINF
SINF檔案是一個LTV+KV結構的檔案,它的欄位如下所示:
sinf.frma: game
sinf.schm: itun
sinf.schi.user: 0xdeadbeef
sinf.schi.key : 0x00000005
sinf.schi.iviv: 0x12345678901234567890123456789012
sinf.schi.righ.veID: 0x000007d3
sinf.schi.righ.plat: 0x00000000
sinf.schi.righ.aver: 0x01010100
sinf.schi.righ.tran: 0xdc64f80c
sinf.schi.righ.sing: 0x00000000
sinf.schi.righ.song: 0x59a73c58
sinf.schi.righ.tool: P550
sinf.schi.righ.medi: 0x00000080
sinf.schi.righ.mode: 0x00002000
sinf.schi.righ.hi32: 0x00000004
sinf.schi.name: User Name
sinf.schi.priv: (432 Bytes Private Key)
sinf.sign: (128 Bytes Private)
SUPF
SUPF檔案主要分為三個部分,我們將其命名為Key Segments、Fairplay Certificate、RSA Signature,其中Key Segments可以含有多個子Segment,用來儲存多個架構的解密資訊。
KeyPair Segments:
Segment 0x0: arm64, Keys: 0x36c/4k, sha1sum = e369546960d805dd1188d42e3350430c7e3a0025
Fairplay Certificate:
Data:
Version: 3 (0x2)
Serial Number:
33:33:af:08:07:08:af:00:01:af:00:00:10
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple FairPlay Certification Authority
Validity
Not Before: Jul 8 00:48:29 2008 GMT
Not After : Jul 7 00:48:29 2013 GMT
Subject: C=US, O=Apple Inc., OU=Apple FairPlay, CN=AP.3333AF080708AF0001AF000010
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (1024 bit)
Modulus:
00:b0:01:16:4b:62:b2:37:8d:60:12:4f:02:15:15:
a0:32:1b:e8:ed:44:ed:e9:17:5b:ec:9e:5d:11:24:
5a:66:2f:dc:a3:25:aa:52:70:e1:09:22:09:4b:65:
0f:67:f5:82:dc:af:78:9b:4c:45:f3:b4:f4:77:aa:
fc:a3:b2:84:c3:8b:09:c6:2e:55:f5:14:85:07:ac:
ae:0d:ff:ff:ca:41:3b:44:cb:52:b6:28:60:55:23:
35:8d:26:71:c6:12:a5:e0:72:58:09:3c:4a:9e:b6:
63:df:2a:91:94:27:eb:65:0a:b2:36:45:11:c1:91:
43:58:12:d9:e5:18:a1:ad:db
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment, Data Encipherment, Key Agreement
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
7B:07:34:81:A5:75:D0:F6:11:BB:D2:36:3F:79:93:4B:A1:70:EB:CF
X509v3 Authority Key Identifier:
keyid:FA:0D:D4:11:91:1B:E6:B2:4E:1E:06:49:94:11:DD:63:62:07:59:64
Signature Algorithm: sha1WithRSAEncryption
06:11:4e:87:ed:b1:08:70:c2:0d:e4:d2:94:bb:7f:ee:50:18:
c0:2a:21:34:0e:99:1f:bf:60:a2:58:d0:0c:28:3d:03:5b:ab:
4e:72:69:ba:41:52:45:b2:29:27:4a:c8:ba:7f:b5:9b:63:78:
b1:68:41:40:59:3f:05:8a:57:74:c5:63:30:cc:f3:20:41:c0:
3c:65:d4:0d:22:47:f3:97:76:e6:d6:3c:eb:e7:20:78:10:59:
fd:96:09:82:c3:41:f0:5f:d0:3e:91:44:6d:77:3f:a5:d9:da:
f0:f7:53:ad:94:61:28:1c:4c:40:3b:17:2b:dd:e3:00:df:77:
71:22
RSA Signature: 6aeb00124d62f75f5761f7c26ec866a061f0776be7e84bfad4b6a1941dbddfdb3bd1afdcc5ef305877fa5bee41caa37b1a9d4ce763cf7d2cb89efa60660a49dd5ddff0f46eee7cd916d382f727d912e82b6e0a62e8110c195e298481aa8c8162faac066ef017c6c2c508700d7adb57e0c988af437621e698946da1b09adf89e9
下面,我們來聊聊Fairplay DRM的混淆原理和實現。
混淆原理和一些實現
LLVM Pass
LLVM是一個優良的編譯器框架,其中,我們可以將其大略的分為前端、中端、後端:
配圖節選自CMU的CS 15-745課程:https://www.cs.cmu.edu/~15745。
前端負責將高階語言轉化為LLVM IR;中端處理LLVM IR,完成一系列的分析、優化任務,我們稱之為Pass,再次輸出LLVM IR;後端則負責將LLVM IR轉化為機器碼。其中,中端的玩法特別豐富,基本的優化任務:如死程式碼消除、常量摺疊都在這一部分完成;Address Sanitizer、PC Sanitizer等編譯器插樁也是在這裡進行的;其他的混淆框架如討論的較多的ollvm以及Hikari,甚至包括蘋果的混淆機制,也都是基於此完成。
這一混淆方式可以基本的分為控制流混淆和資料流混淆,除此之外的一些混淆方式,比如VMP等,不在本文討論範圍內。
makeOpaque
在編譯器中,為了防止一些具體的表示式被優化,我們會將表示式進行等價變化,我們暫時將這樣的操作定義為makeOpaque(如Safari的JavascriptCore,其JIT元件B3就提供這樣的機制),C++虛擬碼如下:
Expression* makeOpaque(Expression *in);
不透明謂詞(Opaque Predicate)
謂詞(Predicate)在計算機中,指的是執行後為True或False的表示式。數論裡面的一些結論可以作為我們生成不透明謂詞的基礎,這些不透明謂詞的結果恆為True或恆為False。比如下列表示式中,y執行的結果就恆為True:
uint32_t x = 0;
bool y = ((x * x % 4) == 0 || (x * x % 4) == 1);
不透明謂詞應用到混淆中的一個例子就是bogus CFG。
如源語句如下:
foo1();
foo2();
經過變換,我們新增了一個虛假的分支(即bogus CFG)
:
foo1();
if ( false )
junk_code();
else
foo2();
但是如果沒有經過特別處理,編譯器、反編譯器的死程式碼消除就會將虛假分支去除掉,因此我們需要makeOpaque的引入,假設我們引入了前面示例中的表示式:
foo1();
uint32_t x = rand();
bool y = ((x * x % 4) == 0 || (x * x % 4) == 1);
if ( !y )
junk_code();
else
foo2();
那麼如果編譯器、反編譯器沒有相應的識別機制的話,這一部分的死程式碼就保留了下來,通過在死程式碼裡面插入大量干擾指令,可以為逆向的人員帶來極大的困擾。經測試在-O2優化下,Clang 11已經可以識別這個規則,但是GCC 5.4無法識別。
可逆變換
這裡我們介紹一下目前混淆技術中常用的等價變換方式。
異或
異或規則是最常見的變換,這裡不再贅述。
x ^ c ^ c = x;
仿射變換(Affine transformation)
我們先來看一下仿射函式。
下面我們來看一下實際應用。
由於計算機中的運算屬於隱式的模運算,因此會具有一些有意思的性質。如對於一個uint32上的運算,模運算逆元定義如下:
//對於
uint32_t a, r_a;
//如果滿足
(a * r_a) % UINT32_MAX == 1;
//那麼 a 和 r_a 互為模反元素
對於互為模反元素的a和r_a(可通過擴充套件歐幾里得演算法求得),有這樣的特性:
uint32_t x = rand();
uint32_t y1 = a * x + c;
//那麼滿足
x == ra * y1 + (- ra * c)
最後舉個例子來說明:
//對於互為模反元素的4872655123 * 3980501275,取
uint32_t x = 0xdeadbeef;
uint32_t c = 0xbeefbeef;
//則 -ra * c = 0x57f38dcb,且
((x * 4872655123) + 0xbeefbeef) * 3980501275 + 0x57f38dcb == x
/*
可在lldb中驗證如下
(lldb) p/x uint32_t x=0xdeadbeef; (uint32_t)(((x * 4872655123) + 0xbeefbeef) * 3980501275 + 0x57f38dcb)
(uint32_t) $8 = 0xdeadbeef
*/
MBA表示式(Mixed Boolean-Arithmetic Expression)
MBA表示式是把算術運算(+,-,*,/)和位運算(&,|,~)混合在一起用以隱藏原本表示式的混淆方法。它基於不同的數學原理存在多種形式,這裡主要介紹多項式MBA,這是目前混淆技術中最常遇到的形式。
類似的,在Fairplay混淆中用到的MBA表示式為:
//OperationSet(+, -, *, &, |, ~)
x - c = (x ^ ~c) + ((2 * x) & ~(2 * c + 1)) + 1;
而使用MBA進行混淆操作主要依靠以下兩個步驟:
不透明常量(Opaque Constant)
不透明常量是基於MBA混淆的方法,用於隱藏資料流中的常量。它使用了置換多項式,是一種在有限域上的可逆多項式。
控制流平坦化
這一部分是逆向工程中討論的最熱門的話題,即將正常的控制流轉換等價替換為一個狀態機,從而干擾靜態的控制流分析,業界也有較多的解決方案。同時因為Fairplay DRM中沒有明顯用到這種型別的混淆,不再多討論。
非直接跳轉(Indirect Branch)
將一些基本塊的起始地址儲存在全域性變數中,通過不透明常量的生成,使得反彙編工具和肉眼無法直接獲取到基本塊跳轉的目標,模型如下:
//記錄基本塊地址到全域性查詢表LUT
LUT[i] = PC;
//執行跳轉
jmp/call LUT[makeOpaque(i)]
具體的例項:
這樣,逆向人員就無法直接獲取跳轉的目標函式、基本塊了。同理,通過將判斷語句的條件對映到跳轉表,也可以實現對條件跳轉的混淆。
所以,在逆向被混淆的Fairplay程式碼時,IDA Pro大多數時刻,只能識別出來函式的第一個基本塊,無法分析出函式的邊界。
跨函式混淆 + 呼叫約定混淆
正常情況下,程式語言如C語言的引數傳遞遵循特定的呼叫約定,但是部分混淆工具會對一些內部函式的呼叫約定進行修改,以Fairplay DRM為例:
我們可以看到常規的以暫存器和棧傳遞引數的方式被替換成了以堆傳遞引數的方式了,在構造好了結構體的情況下,這個引數傳遞的特徵可以被清晰的看出來。同時,這裡面對一些傳遞的引數進行了異或混淆,在子函式裡面再恢復出來,使得我們難以直接得到原始資料,而靜態分析的工具比如IDA Pro也不支援跨函式的資料流分析。
更嚴重的是,一些影響子函式執行的重要依賴資料,被提升到了父函式內,導致在沒有恢復呼叫關係前,我們根本無法推測子函式的執行流程。
那麼,Fairplay DRM的破解之道就是要找到它的弱點。
Fairplay混淆的弱點
通過前邊的工作,我們已經能Fairplay正常的完成開啟和解密工作了,通過一系列的靜態分析和追蹤除錯,我們發現了這一套混淆系統的一些對抗方案。
這些問題的本質原因是:混淆系統在IR層面設計,對機器相關的部分操作沒有混淆,因此在生成的機器碼裡面,我們可以推斷得到混淆前的一些特徵資訊。
函式邊界識別
前面提到,由於Fairplay用到了非直接跳轉的混淆技術,IDA Pro無法直接分析函式的邊界。通過跟蹤,我們發現在arm64e裝置下,該核心驅動中,同一個函式的所有基本塊在執行到跳轉指令時,均使用了同一個PAC Context,或者稱之為PAC Modifier。
藉由這個特性,我們可以將函式的邊界和基本塊分組,儘管目前為止這些基本塊之間並不是連通的。
非直接跳轉
對於無條件跳轉,我們通過設定斷點跟蹤執行流,就可以解決了。
再通過KeyPatch這樣的工具,我們可以將一些簡單的函式恢復到比較易於理解的地步。
但是這裡的難點在於恢復混淆裡面的非直接跳轉指令,如下圖所示:
對於這個跳轉指令,我們可以生成如下的表示式:
//cmp x0, #0
w10 = qword[x12 + (EQ * 0xB + w19) << 3]
//0xB代表兩個基本塊的在LUT中的下標差
通過CSET指令的形式,我們已經可以推斷跳轉指令應該是J.NE或者J.EQ了,通過我們的偵錯程式外掛,我們可以得到其中一個分支的跳轉地址和原本的跳轉指令,再通過表示式的資訊,我們可以很快推斷出另外一個分支的地址。
再通過Keypatch,我們可以得到混淆前的分支語句結構:
至此,我們已經可以完整地恢復Fairplay的大多數控制流了。
資料流混淆
這一部分在前面已經提及到了一些,目前我們已經找到了MBA表示式的模式,但還沒能找到Fairplay中生成不透明常量的完整規則。其中MBA表示式的重寫規則目前看起來僅有一個,即:
x - c = (x ^ ~c) + ((2 * x) & ~(2 * c + 1)) + 1;
一些基於模式匹配的工具,比如D810已經可以比較好的處理這樣的情況了。
結束語
目前,我們已經可以獲取到解密每一段Mach-O的AES金鑰了,通過大量的除錯和反混淆,我們已經得出了這些金鑰生成的初步結論。我們希望最終的目的是不依賴Apple裝置的前提下,完成Fairplay DRM加解密的研究。
最後,附上原始碼,歡迎大家進行參考和研究。
參考文獻
- Eyrolles, N. (2017). Obfuscation with Mixed Boolean-Arithmetic Expressions: reconstruction, analysis and simplification tools (Doctoral dissertation, Université Paris-Saclay)
- https://github.com/obfuscator-llvm/obfuscator
- https://github.com/HikariObfuscator/Hikari
- https://github.com/keystone-engine/keypatch
- https://eshard.com/posts/d810_blog_post_1
作者簡介
吳聊、落落、朱米,均來自美團資訊保安部。
閱讀美團技術團隊更多技術文章合集
前端 | 演算法 | 後端 | 資料 | 安全 | 運維 | iOS | Android | 測試
| 在公眾號選單欄對話方塊回覆【2020年貨】、【2019年貨】、【2018年貨】、【2017年貨】等關鍵詞,可檢視美團技術團隊歷年技術文章合集。
| 本文系美團技術團隊出品,著作權歸屬美團。歡迎出於分享和交流等非商業目的轉載或使用本文內容,敬請註明“內容轉載自美團技術團隊”。本文未經許可,不得進行商業性轉載或者使用。任何商用行為,請傳送郵件至tech@meituan.com申請授權。