通過在應用程序的安裝目錄中搜索一些關鍵字,我們實際上得到了兩個結果,它們含有混淆器名稱的信息:
NuDetectSDK 二進製文件也使用相同的混淆器,但它似乎沒有參與上圖所示的早期越獄檢測。另一方面,SingPass 是應用程序的主要二進製文件,我們可以觀察到與威脅檢測相關的字符串:
混淆器的名稱已被編輯,但不會影響代碼的內容。
不幸的是,二進製文件沒有洩漏其他字符串,這些字符串可以幫助識別應用程序檢測越獄設備的位置和方式,但幸運的是,應用程序沒有崩潰。
如果我們假設混淆器在運行時解密字符串,則可以嘗試在顯示錯誤消息時轉儲__data 部分的內容。在執行時,用於檢測越獄設備的字符串可能已被解碼並清楚地存在於內存中。
1.我們運行應用程序並等待越獄消息;
2.我們使用Frida 附加到SingPass,並註入一個庫:
2.1在內存中解析SingPass 二進製文件;
2.2轉儲__data 部分的內容;
2.3 將轉儲寫入iPhone 的/tmp 目錄;
一旦數據區被轉儲,__data部分會發生以下變化:
轉儲前後的__data 部分
此外,我們可以觀察到以下字符串,它們似乎與混淆器的RASP功能有關:
與RASP 功能相關的字符串
所有的EVT_*字符串都由一個且只有一個我命名為on_rasp_detection的函數引用。這個函數是應用程序開發者在觸發RASP事件時用來執行操作的威脅檢測回調函數。
為了更好地理解這些字符串背後的檢查邏輯,讓我們從用於檢測掛鉤函數的EVT_CODE_PROLOGUE 開始。
EVT_CODE_PROLOGUE:掛鉤檢測當通過彙編代碼接近on_rasp_detection 的交叉引用時,我們可以多次發現這種模式:
為了檢測給定函數是否被鉤住,混淆器加載函數的第一個字節,並將該字節與值0xFF進行比較。乍一看,0xFF似乎是任意的,但事實並非如此。實際上,常規函數以一個序言開始,該序言在堆棧上分配空間,以保存由調用約定定義的寄存器和函數所需的堆棧變量。在AArch64中,這個分配可以通過兩種方式執行:
這些指令是不相等的,如果偏移量存在,它們可能會導致相同的結果。在第二種情況下,指令sub SP、SP、#CST 用以下字節編碼:
正如我們所看到的,該指令的編碼從0xFF開始。如果不是這樣,那麼該函數要么以不同的堆棧分配序言開始,要么可能以一個掛鉤的蹦床開始。由於應用程序的代碼是通過混淆器的編譯器編譯的,因此編譯器能夠區分這兩種情況,並為正確的函數的序言插入正確的檢查。
如果函數指令的第一個字節沒有通過檢查,則跳轉到紅色基本塊。這個基本塊的目的是觸發一個用戶定義的回調,它將根據應用程序的設計和開發人員的選擇來處理檢測:
打印錯誤
應用程序崩潰
破壞內部數據
……
從上圖中,我們可以觀察到檢測回調是從位於#hook_detect_cbk_ptr 的靜態變量加載的。調用此檢測回調時,混淆器會向回調提供以下信息:
1.檢測碼:EVT_CODE_PROLOGUE 為0x400;
2.可能導致應用程序崩潰的受攻擊指針;
現在讓我們仔細看看檢測回調的整體設計。
檢測回調如上一節所述,當混淆器檢測到篡改時,它會通過調用存儲在地址的靜態變量中的檢測回調來做出反應:0x10109D760
通過靜態分析hook_detect_cbk,實現似乎破壞了回調參數中提供的指針。另一方面,在運行應用程序時,我們觀察到越獄檢測消息,而不是應用程序崩潰。
如果我們查看在該地址讀取或寫入的交叉引用,我們會得到以下指令列表:
所以實際上只有一條指令,init_and_check_rasp+01BC,用另一個函數覆蓋默認的檢測回調:
與默認回調相比:hook_detect_cbk(被覆蓋的函數)相比,hook_detect_cbk_user_def不會損壞一個會導致應用程序崩潰的指針。相反,它調用on_rasp_detection函數,該函數引用上圖中列出的所有字符串EVT_CODE_TRACING、EVT_CODE_SYSTEM_LIB等。
通過整體查看init_and_check_rasp函數,我們可以注意到X23寄存器也用於初始化其他靜態變量:
X23寫入指令
這些內存寫入意味著回調hook_detect_cbk_user_def 用於初始化其他靜態變量。特別是,這些其他靜態變量很可能用於其他RASP 檢查。通過查看這些靜態變量#EVT_CODE_TRACING_cbk_ptr、#EVT_ENV_JAILBREAK_cbk_ptr 等的交叉引用,我們可以找到執行其他RASP 檢查的位置以及觸發它們的條件。
EVT_CODE_SYSTEM_LIB
EVT_ENV_DEBUGGER
EVT_ENV_JAILBREAK
多虧了#EVT_*交叉引用,我們可以靜態地通過使用這些#EVT_*變量的所有基本塊,並突出顯示可能觸發RASP回調的底層檢查。在詳細檢查之前,需要注意以下幾點:
1.雖然應用程序使用了一個商業混淆器,除了RASP之外,還提供了本地代碼混淆,但代碼是輕度混淆的,這使得靜態彙編代碼分析非常容易。
2.應用程序為所有RASP 事件設置相同的回調。因此,它簡化了RASP 繞過和應用程序的動態分析。
反調試SingPass 使用的混淆器版本實現了兩種調試檢查。首先,它檢查父進程id (ppid) 是否與/sbin/launchd 相同,後者應該為1。
getppid 通過函數或系統調用調用。
如果不是這種情況,它會觸發EVT_ENV_DEBUGGER 事件。第二個檢查基於用於訪問extern_proc.p_flag 值的sysctl。如果此標誌包含P_TRACED 值,則RASP 例程會觸發EVT_ENV_DEBUGGER 事件。
在SingPass 二進制中,我們可以在以下地址範圍內找到這兩個檢查的實例:
越獄檢測對於大多數越獄檢測,混淆器會通過檢查設備上是否存在(或不存在)某些文件來嘗試檢測設備是否已越獄。
借助以下幫助程序,可以使用系統調用或常規函數檢查文件或目錄:
如上所述,我提到__data 部分的轉儲顯示與越獄檢測相關的字符串,但轉儲並未顯示混淆器使用的所有字符串。
通過仔細研究字符串編碼機制,可以發現有些字符串是在臨時變量中即時解碼的。我將在本文的第二部分解釋字符串編碼機制,這樣,我們可以通過在fopen、utimes等函數上設置鉤子,並在這些調用之後立即轉儲__data部分來揭示字符串。然後,我們可以遍歷不同的轉儲,查看是否出現了新的字符串。
最後,該方法無法對所有字符串進行解碼,但可以實現良好的覆蓋。用於檢測越獄的文件列表在附件中給出。
還有一個檢測unc0ver 越獄的特殊檢查,包括嘗試卸載/.installed_unc0ver:
0x100E4D814:_unmount('/.installed_unc0ver')
環境混淆器還會檢查觸發EVT_ENV_JAILBREAK 事件的環境變量。其中一些檢查似乎與代碼提升檢測有關,但仍會觸發EVT_ENV_JAILBREAK 事件。
startswith()從逆向工程的角度來看,startswith()實際上是作為一個“or-ed”的xor序列來實現的,以得到一個布爾值。這可能是編譯器優化的結果。你可以在位於地址0x100015684的基本塊中觀察這個模式。
高級檢測除了常規檢查之外,混淆器還執行高級檢查,比如驗證SIP(系統完整性保護)的當前狀態,更準確地說,是KEXTS代碼簽名狀態。
根據我在iOS越獄方面的經驗,我認為沒有越獄會禁用CSR_ALLOW_UNTRUSTED_KEXTS標誌。相反,我猜它是用來檢測應用程序是否在允許這種停用的Apple M1 上運行。
Assemblyrange:0x100004640–0x1000046B8
混淆器還使用Sandbox API 來驗證是否存在某些路徑:
通過這個API 檢查的路徑是OSX 相關的目錄,所以我猜它也被用來驗證當前代碼沒有在Apple Silicon 上被解除。例如,下面是使用Sandbox API 檢查的目錄列表:
Assemblyrange:0x100ED7684(function)
此外,它使用沙盒屬性file-read-metadata 作為stat() 函數的替代方案。
Assemblyrange:0x1000ECA5C–0x1000ECE54
該應用程序通過私有系統調用使用沙盒API 來確定是否存在一些越獄工件。這是非常明智的做法,但我想這並不符合蘋果的安全政策。
代碼符號表此檢查的目的是驗證已解析導入的地址是否指向正確的庫。換句話說,此檢查驗證導入表沒有被可用於掛鉤導入函數的指針篡改。
Initialization: part of sub_100E544E8
Assemblyrange:0x100016FC4–0x100017024
在RASP 檢查初始化(sub_100E544E8) 期間,混淆器會手動解析導入的函數。此手動解析是通過迭代SingPass 二進製文件中的符號、檢查導入符號的庫、訪問(在內存中)此庫的__LINKEDIT 段、解析導出trie 等來執行的。此手動解析填充一個包含已解析符號的絕對地址的表。
此外,初始化例程設置遵循以下佈局的元數據結構:
symbols_index 是一種轉換錶,它將混淆器已知的索引轉換為__got 或__la_symbol_ptr 部分中的索引。索引的來源(即__got 或__la_symbol_ptr)由包含類枚舉整數的origins 表確定:
symbols_index和origins這兩個表的長度都是由靜態變量nb_symbols定義的,它被設置為0x399。元數據結構後面跟著兩個指針:resolved_la_syms 和resolved_got_syms,它們指向混淆器手動填充的導入地址表。
每個部分都有一個專用表:__got 和__la_symbol_ptr。
然後,macho_la_syms 指向__la_symbol_ptr 部分的開頭,而macho_got_syms 指向__got 部分。
最後,stub_helper_start/stub_helper_end 保存了__stub_helper 部分的內存範圍。稍後我將介紹這些值的用途。
這個元數據結構的所有值都是在函數sub_100E544E8中進行初始化時設置的。
在SingPass 二進製文件的不同位置,混淆器使用此元數據信息來驗證已解析導入的完整性。它首先訪問symbols_index 和具有固定值的起源:
由於symbols_index表包含uint32_t值,#0xCA8匹配#0x32A(起源表的索引)當除以sizeof(uint32_t):0xCA8=0x32A * sizeof(uint32_t)。
換句話說,我們有以下操作:
然後,給定sym_idx 值並根據符號的來源,該函數訪問已解析的__got 表或已解析的__la_symbol_ptr 表。此訪問是通過位於sub_100ED6CC0 的輔助函數完成的。可以用下面的偽代碼來概括:
比較section_ptr 和manual_resolved 的索引sym_idx 處的條目,如果它們不匹配,則觸發事件#EVT_CODE_SYMBOL_TABLE。
實際上,比較涵蓋了不同的情況。首先,混淆器處理sym_idx 處的符號尚未解析的情況。在這種情況下,section_ptr[sym_idx] 指向位於__stub_helper 部分中的符號解析存根。這就是元數據結構包含本節的內存範圍的原因:
另外,如果兩個指針不匹配,函數會使用dladdr來驗證它們的位置:
例如,如果導入的函數與Frida掛鉤,則兩個指針可能不匹配。
在origin[sym_idx]被設置為SYM_ORIGINS:NONE的情況下,函數跳過檢查。因此,我們可以通過用0填充原始表來禁用這個RASP檢查。符號的數量接近元數據結構,元數據結構的地址是由___atomic_load和___atomic_store函數洩露的。
代碼跟踪檢查代碼跟踪檢查旨在驗證當前沒有被跟踪。通過查看#EVT_CODE_TRACING_cbk_ptr 的交叉引用,我們可以識別出兩種驗證。
GumExecCtxEVT_CODE_TRACING 似乎能夠檢測Frida 的跟踪檢查是否正在運行。這是我第一次觀