Fairplay DRM與混淆實現的研究

研究Fairplay DRM(Digital Rights Management,即數字版權保護)最關鍵的兩點是授權和加密。但長久以來,關於App DRM的研究卻很少,而就是在這樣的前提下,Fairplay DRM又爲iOS App的安全研究疊加了一層“阻礙”。我們通過分析混淆系統的設計和實現過程中的問題,克服調試跟蹤的障礙,設計了多種靜態和動態的對抗方案;同時通過大量的逆向工程,填補了安全研究人員對macOS系統機制中,關於Fairplay這一部分的認知空白。

什麼是DRM?

DRM全稱Digital Rights Management,即數字版權保護。蘋果爲了保護App Store分發的音樂/視頻/書籍/App免於盜版,開發了Fairplay DRM技術,並申請了很多相關的專利,比較有代表性的如:

長久以來,關於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。

圖1

而負責解密Mach-O的組件主要包括:內核態的FairplayIOKit和用戶態的fairplayd

Fairplay的Open

MacOS的XNU Kernel中有text_crypter_create_hook這個導出符號,IOTextEncryptionFamily驅動則註冊了這個Hook,並作爲橋樑,將調用轉發給了FairplayIOKit內核驅動。

圖2

最終負責處理的函數是:

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。

圖3

最後,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目錄下。

圖4

其中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是一個優良的編譯器框架,其中,我們可以將其大略的分爲前端、中端、後端:

圖5

配圖節選自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)]

具體的實例:

圖6

這樣,逆向人員就無法直接獲取跳轉的目標函數、基本塊了。同理,通過將判斷語句的條件映射到跳轉表,也可以實現對條件跳轉的混淆。

所以,在逆向被混淆的Fairplay代碼時,IDA Pro大多數時刻,只能識別出來函數的第一個基本塊,無法分析出函數的邊界

跨函數混淆 + 調用約定混淆

正常情況下,編程語言如C語言的參數傳遞遵循特定的調用約定,但是部分混淆工具會對一些內部函數的調用約定進行修改,以Fairplay DRM爲例:

圖7

我們可以看到常規的以寄存器和棧傳遞參數的方式被替換成了以堆傳遞參數的方式了,在構造好了結構體的情況下,這個參數傳遞的特徵可以被清晰的看出來。同時,這裏面對一些傳遞的參數進行了異或混淆,在子函數裏面再恢復出來,使得我們難以直接得到原始數據,而靜態分析的工具比如IDA Pro也不支持跨函數的數據流分析。

更嚴重的是,一些影響子函數運行的重要依賴數據,被提升到了父函數內,導致在沒有恢復調用關係前,我們根本無法推測子函數的運行流程。

那麼,Fairplay DRM的破解之道就是要找到它的弱點。

Fairplay混淆的弱點

通過前邊的工作,我們已經能Fairplay正常的完成打開和解密工作了,通過一系列的靜態分析和追蹤調試,我們發現了這一套混淆系統的一些對抗方案。

這些問題的本質原因是:混淆系統在IR層面設計,對機器相關的部分操作沒有混淆,因此在生成的機器碼裏面,我們可以推斷得到混淆前的一些特徵信息

函數邊界識別

前面提到,由於Fairplay用到了非直接跳轉的混淆技術,IDA Pro無法直接分析函數的邊界。通過跟蹤,我們發現在arm64e設備下,該內核驅動中,同一個函數的所有基本塊在運行到跳轉指令時,均使用了同一個PAC Context,或者稱之爲PAC Modifier。

圖8

藉由這個特性,我們可以將函數的邊界和基本塊分組,儘管目前爲止這些基本塊之間並不是連通的。

非直接跳轉

對於無條件跳轉,我們通過設置斷點跟蹤執行流,就可以解決了。

圖9

再通過KeyPatch這樣的工具,我們可以將一些簡單的函數恢復到比較易於理解的地步。

圖10

但是這裏的難點在於恢復混淆裏面的非直接跳轉指令,如下圖所示:

圖11

對於這個跳轉指令,我們可以生成如下的表達式:

//cmp x0, #0
w10 = qword[x12 + (EQ * 0xB + w19) << 3]
//0xB代表兩個基本塊的在LUT中的下標差

通過CSET指令的形式,我們已經可以推斷跳轉指令應該是J.NE或者J.EQ了,通過我們的調試器插件,我們可以得到其中一個分支的跳轉地址和原本的跳轉指令,再通過表達式的信息,我們可以很快推斷出另外一個分支的地址。

圖12

再通過Keypatch,我們可以得到混淆前的分支語句結構:

圖13

至此,我們已經可以完整地恢復Fairplay的大多數控制流了。

數據流混淆

這一部分在前面已經提及到了一些,目前我們已經找到了MBA表達式的模式,但還沒能找到Fairplay中生成不透明常量的完整規則。其中MBA表達式的重寫規則目前看起來僅有一個,即:

x - c = (x ^ ~c) + ((2 * x) & ~(2 * c + 1)) + 1;

一些基於模式匹配的工具,比如D810已經可以比較好的處理這樣的情況了。

結束語

目前,我們已經可以獲取到解密每一段Mach-O的AES密鑰了,通過大量的調試和反混淆,我們已經得出了這些密鑰生成的初步結論。我們希望最終的目的是不依賴Apple設備的前提下,完成Fairplay DRM加解密的研究。

圖14

最後,附上源碼,歡迎大家進行參考和研究。

參考文獻

作者簡介

吳聊、落落、朱米,均來自美團信息安全部。

閱讀美團技術團隊更多技術文章合集

前端 | 算法 | 後端 | 數據 | 安全 | 運維 | iOS | Android | 測試

| 在公衆號菜單欄對話框回覆【2020年貨】、【2019年貨】、【2018年貨】、【2017年貨】等關鍵詞,可查看美團技術團隊歷年技術文章合集。

| 本文系美團技術團隊出品,著作權歸屬美團。歡迎出於分享和交流等非商業目的轉載或使用本文內容,敬請註明“內容轉載自美團技術團隊”。本文未經許可,不得進行商業性轉載或者使用。任何商用行爲,請發送郵件至[email protected]申請授權。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章