線程檢查
之前的 Frida stalker 檢查的替代方法是通過以下調用訪問當前線程狀態:
然後,由於以下比較,它檢查 state->ts_64.__pc 是否在 libsystem_kernel.dylib 中:
換句話說,如果 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/