以 SingPass 應用為例分析 iOS RASP 應用自保護的實現以及繞過方法(下)。


線程檢查

之前的 Frida stalker 檢查的替代方法是通過以下調用訪問當前線程狀態:

然後,由於以下比較,它檢查 state->ts_64.__pc 是否在 libsystem_kernel.dylib 中:

以 SingPass 應用為例分析 iOS RASP 應用自保護的實現以及繞過方法(下)。

換句話說,如果 state->ts_64.__pc 與 &mach_msg 的距離小於 0x4000,則認為它在 libsystem_kernel.dylib 中。

乍一看,對這個 RASP 檢查可能不是很熟悉,但由於之前與 EVT_CODE_TRACING 相關的檢查旨在檢測 Frida Stalker,因此該檢查也可能旨在檢測 Frida Stalker。

為了證實這個假設,我開發了一個小測試用例,在一個獨立的二進制文件中重現了這個檢查,我們可以根據它是否通過 Frida stalker 來觀察差異:

Stalker 測試用例的輸出

沒有 Stalker 的測試用例的輸出

通過使用函數 gum_stalker_exclude 從跟蹤者中排除庫 libsystem_kernel.dylib ,從而輕松繞過此檢查:

可以看到,state->ts_64.__pc 位於 libsystem_kernel.dylib 中:

排除內存范圍的測試用例的輸出


應用加載的庫

RASP 事件 EVT_APP_LOADED_LIBRARIES 旨在檢查 Mach-O 依賴項的完整性。

換句話說,它檢查 Mach-O 導入的庫是否被修改。

Assembly ranges: 0x100E4CDF8 – 0x100e4d39c

由於 dladdr 函數,與此檢查相關的代碼首先訪問 Mach-O 標頭:

dl_info 包含庫的基地址,其中包含第一個參數中提供的地址,因此,一個Mach-O二進制文件會連同它的標頭文件Dl_info一起加載。

Dli_fbase實際上指向mach_header_64。

然後該函數遍歷類似 LC_ID_DYLIB 的命令以訪問依賴項的名稱:

此名稱包含依賴項的路徑。

例如,我們可以按如下方式訪問此列表:

依賴項的名稱用於填充哈希表,其中哈希值以 32 位編碼:

在後面的代碼中,這個計算表將與另一個哈希表(代碼中硬編碼的)進行比較,如下所示:


哈希示例

如果某些庫已被修改為註入,例如 FridaGadget.dylib,那麼動態計算的哈希將與代碼中硬編碼的哈希不匹配。

雖然這種檢查的執行是相當『標準』的,但有幾點值得一提:

首先,哈希函數似乎是一個派生的MurmurHash。

其次,哈希是32位編碼的,但是圖4中的代碼引用了64位的X11/X12寄存器。

這實際上是一個限制內存訪問次數的編譯器優化。

最後,在每個檢查實例中,硬編碼的哈希值在二進制文件中重復。

在 SingPass 中,此 RASP 檢查出現兩次,因此我們在以下位置找到這些值:0x100E4CF38、0x100E55678。

這種重復可能用於防止易於修復的單點位置《 single spot location》。


代碼系統庫

此檢查與事件 EVT_CODE_SYSTEM_LIB 相關聯,該事件包括驗證內存系統庫及其在 dyld 共享緩存《磁盤上》中的內容的完整性。

Assembly ranges: 0x100ED5BF8 – 0x100ED5D6C and 0x100ED5E0C – 0x100ED62D4

此檢查通常以以下模式開始:

如果帶有給定 check_region_cbk 回調的 iterate_system_region 的結果不為 0,它會觸發 EVT_CODE_SYSTEM_LIB 事件:

要理解這個檢查背後的邏輯,我們需要了解 iterate_system_region 函數的用途以及它與回調 check_region_cbk 的關系。


iterate_system_region

該函數旨在調用系統函數 vm_region_recurse_64,然後根據可能觸發第一個參數check_region_cbk中給出的回調的條件過濾它的輸出。

iterate_system_region首先通過SYS_shared_region_check_np系統調用訪問dyld共享緩存的基址。

這個地址用於讀取和記憶dyld_cache_header結構中的一些屬性:

1.共享緩存標頭;

2.共享緩存結束地址;

3.與共享緩存相關的其他限制;

計算過程如下:

從逆向工程的角度來看,用於記憶這些信息的堆棧變量與稍後調用的 vm_region_recurse_64 的參數信息別名。

我不知道這種混疊是否是故意的,但它使結構的逆向工程變得更加復雜。

在vm_region_recurse_64上有一個循環,它查詢vm_region_submap_info_64信息,查找dyld共享緩存范圍內的這些地址。

由於mach_msg_type_number_t *infoCnt參數被設為19,我們可以確定查詢的類型(vm_region_submap_info_64):

此循環在某些條件下中斷,並且在其他條件下觸發回調。

正如稍後解釋的那樣,回調驗證 dyld 共享緩存中存在的庫的內存完整性。

這個循環在某些條件下中斷,而在其他條件下觸發回調。

回調會驗證dyld共享緩存中存在的庫在內存中的完整性。

基本上,如果發生以下情況,就會觸發對共享緩存進行深度檢測的回調:


check_region_cbk

當條件滿足時,iterate_system_region調用check_region_cbk,第一個參數中帶有可疑地址:

在分析 SingPass 時,隻有一個回調函數與iterate_system_region一起使用,它的代碼並沒有特別混淆(字符串除外)。

一旦我們知道這些檢查與dyld共享緩存有關,我們就可以很容易地弄清楚這個函數中涉及的結構。

這個回調位於0x100ed5e0c地址,並重命名為check_region_cbk。

它首先訪問有關地址的信息:

此信息用於讀取與地址參數關聯的__TEXT 段的內容。

__TEXT 字符串以及共享緩存的不同路徑《如 /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e 和標頭的魔法值:0x01010b9126:dyld_v1 arm64e 或 0x01010b9116:dyld_v1 arm64》都被編碼。

另一方面,該函數打開 dyld_shared_cache 並查找包含與地址參數關聯的庫的共享緩存部分:

第二次調用mmap()的目的是加載包含庫代碼的共享緩存部分。

然後,該函數檢查__TEXT段的內容是否與內存中的內容相匹配。

執行此比較的循環位於0x100ED6C58 – 0x100ED6C70。

我們可以從這個RASP檢查的描述中觀察到,開發者花了很多精力來避免性能問題和內存消耗。

另一方面,在我的測試中從來沒有調用過回調check_region_cbk《即使我掛鉤了系統函數》。

我不知道是不是因為我誤解了條件,但最後,我必須手動強制條件。


RASP 的設計弱點

由於保存函數指針的不同 #EVT_* 靜態變量,混淆器能夠為支持的 RASP 事件提供專用回調。

盡管如此,應用程序開發人員定義的函數 init_and_check_rasp 將所有這些指針設置為同一個回調:hook_detect_cbk_user_def。

在這樣的設計中,所有 RASP 事件最終都在一個函數中,這削弱了不同 RASP 檢查的強度。

這意味著我們隻需要針對這個函數來禁用或繞過 RASP 檢查。

由於這個缺點,我可以防止應用程序一啟動就顯示錯誤消息。

它存在另外兩個 RASP 檢查:EVT_APP_MACHO 和 EVT_APP_SIGNATURE,由於開發人員未啟用它們,因此在 SingPass 中不存在。


總結

一方面商業解決方案實現了強大而先進的 RASP 功能,例如,內聯系統調用分佈在應用程序的不同位置。

另一方面,應用程序的開發人員通過為所有事件設置相同的回調來削弱 RASP 功能。

此外,該應用程序似乎沒有使用商業解決方案提供的本機代碼混淆,這使得 RASP 檢查不受靜態代碼分析的保護。

無論用戶提供什麼配置,對這些檢查強制執行代碼混淆都是值得的。

參考及來源:https://www.romainthomas.fr/post/22-08-singpass-rasp-analysis/