以攻擊者角度學習某風控裝置指紋產品
目錄:
一、產品特點
二、什麼是裝置指紋?
三、裝置指紋的應用場景
四、裝置指紋原理分析
五、裝置風險識別原理分析
六、破解虛擬機器保護
七、破解裝置指紋與風險識別
八、總結
一、產品特點
1.1 優點:
該產品在安全防破解方面個人認為算是業 內做到了比較高標準的安全保護機制,主要表現在以下幾點:
安全虛擬機器中實現加解密過程
將演算法程式碼轉換為安全虛擬機器指令,將換後指令解釋執行在虛擬機器之中,無法被反編譯還原回可讀的原始碼,指數級增長了程式碼邏輯複雜度,極大提升了黑客破解和篡改的時間成本與人力成本。
執行時系統環境安全識別
對本身做了完整性檢驗、偵錯程式監測、檢測當前執行裝置是否處於越獄狀態、檢測當前程序是否被程式碼注入、檢測當前程序API是否被hook。
1.2 缺點:
架構設計不具備高內聚低耦合
在我逆向過程中發現了很多分散重複相關聯的程式碼邏輯未進行抽象整合,不夠精煉,這也是使得包體過大的原之一。
太過於注重安全,並沒有權衡效能與安全的中間點,但是 從使用者體驗角度上來看,在某些要求高效能的APP上可能會損失掉部分使用者體驗。
1.3、產品功能
產品主要功能官方介紹如圖1所示:
圖1
二、什麼是裝置指紋?
2.1、裝置指紋
裝置裝置指紋,簡單來說就是一串符號(或者數字),對映現實中硬體裝置。如果這些符號和裝置是一一對應的,可稱之為“唯一裝置ID(Unique Device Identifier)"
通過在網站或者移動端嵌入裝置指紋SDK/JS,可以獲取操作裝置的多重屬性,為每一個操作裝置建立一個全球唯一的裝置ID。該裝置ID就相當於這個裝置的指紋,不論這個裝置使用何種瀏覽器、何種應用或是在何地,都能夠唯一標識該裝置,裝置系統升級變更,裝置指紋不會發生變更。
三、裝置指紋的應用場景
3.1、資料統計
在一般的資料統計分析領域,常常需要基於使用者裝置的維度來統計常規的pv,uv,點選數,使用者留存等,這就都需要一個穩定且唯一的裝置ID來保證。
3.2、風控安全
產品安全相關能力及應用場景官方介紹如圖2所示:
圖2
3.3、應用舉例
邀請返利
網際網路營銷中為了吸引新使用者註冊、會推出註冊領取禮品、禮券、紅包等活動的方式鼓勵推廣新會員,這樣便給羊毛黨機會,可通過使用改機工具偽造新裝置或者偽造某些系統底層引數(比如地理位置,imei號等等)的方式來繞過業務的限制獲取更多禮品,在這種場景下用指紋識別裝置的唯一性非常必要,所以一個好的裝置指紋產品應該具有確保裝置指紋生成的唯一性、防篡改、對系統環境異常識別的能力。
四、裝置指紋原理分析
4.1、裝置指紋具備特性
唯一性高-穩定性強:
對比於傳統的 IP、手機號等ID,裝置指紋具有唯一性高、穩定性強和資訊豐富這三個優勢。
簡單來說,唯一性高是指一人一裝置,因為使用者不同,每個智慧裝置上的使用痕跡和特徵也具有唯一性。穩定性強也很好理解,就是智慧裝置的硬體不常更新,它們對應穩定不變的ID。
第一,裝置重置之後,保持裝置指紋不變。
恢復出廠設定是所有智慧裝置的標配功能,裝置重置之後,系統自帶的裝置 ID 必然會發生變化,理論上來說就是“新裝置”了。 所以,如果只是使用系統自帶的裝置ID,黑產完全可以通過不斷恢復出廠設定模擬大量的裝置,來繞過風控系統的檢測。因此,如何在恢復出廠設定的情況下,仍然保持裝置指紋的穩定不變,是裝置指紋技術的主要挑戰之一。
第二,裝置更新之後,保持裝置指紋不變。
既然無法直接使用自帶的裝置ID,那我們就必須基於各類裝置資訊綜合計算出裝置指紋。但是,我們平時在使用智慧裝置的時候,不僅會有意或無意地變更裝置名稱、網路環境、位置等資訊,還會更新作業系統,系統版本、應用版本等特徵也會隨之改變。這都會影響到裝置指紋的計算。
知道了裝置更新能影響裝置指紋的計算,黑產在進行欺詐行為的時候會更加極端,它們會更換部分硬體去嘗試偽造新的裝置,比如,攝像頭、音響等相對容易拆卸安裝的部分。因此,如何在一定程度上相容裝置的變動和更新,也是裝置指紋需要考慮的問題之一。
總之,黑產總是會嘗試去修改虛擬裝置的各類配置,將其偽造成新的裝置,從而繞過風控系統的檢測。因此,一個穩定的裝置指紋可以幫助風控系統對抗黑產的虛擬裝置。
上面說的這兩個挑戰都屬於裝置指紋對穩定性的要求。最後,我們還要保證裝置指紋的唯一性,避免兩個不同的裝置產生相同的裝置指紋,比如,如何準確地區分同型號的裝置,也是裝置指紋需要滿足的要求之一。所以,唯一性是避免誤傷真實使用者的關鍵維度。
4.2、裝置指紋生成方式
裝置指紋整體生成流程如圖3所示 :
圖3
資訊採集:
想要獲得準確且穩定的裝置指紋,必須從多個維度採集不同的資訊。通過逆向從記憶體截取了部分採集裝置加密前資訊如下:
00000001010C6E00 0A 17 0A 02 4B 31 12 11 64 63 3A 32 62 3A 32 61 ....K1..dc:2b:2a 00000001010C6E10 3A 62 37 3A 31 37 3A 30 62 0A 08 0A 03 4B 31 30 :b7:17:0b....K10 00000001010C6E20 12 01 32 0A 12 0A 03 4B 31 31 12 0B 31 32 30 37 ..2....K11..1207 00000001010C6E30 35 39 35 34 31 37 36 0A 11 0A 03 4B 31 32 12 0A 5954176....K12.. 00000001010C6E40 39 33 33 34 38 30 30 33 38 34 0A 11 0A 03 4B 31 9334800384....K1 00000001010C6E50 33 12 0A 31 30 33 37 30 34 31 36 36 34 0A 0F 0A 3..1037041664... 00000001010C6E60 03 4B 31 34 12 08 37 35 30 58 31 33 33 34 0A 0B .K14..750X1334.. 00000001010C6E70 0A 03 4B 31 35 12 04 57 69 66 69 0A 14 0A 03 4B ..K15..Wifi....K 00000001010C6E80 31 37 12 0D 63 68 61 72 67 69 6E 67 2F 31 30 30 17..charging/100 00000001010C6E90 25 0A 0D 0A 03 4B 31 38 12 06 44 61 72 77 69 6E %....K18..Darwin 00000001010C6EA0 0A 10 0A 03 4B 31 39 12 09 69 50 68 6F 6E 65 2D ....K19..iPhone- 00000001010C6EB0 76 76 0A 0A 0A 02 4B 32 12 04 6E 6F 6E 65 0A 0D vv....K2..none.. 00000001010C6EC0 0A 03 4B 32 30 12 06 31 36 2E 31 2E 30 0A 6B 0A ..K20..16.1.0.k. 00000001010C6ED0 03 4B 32 31 12 64 44 61 72 77 69 6E 20 4B 65 72 .K21.dDarwin Ker 00000001010C6FC0 03 4B 33 31 12 0A 7A 68 2D 48 61 6E 73 2D 43 4E .K31..zh-Hans-CN 00000001010C6FD0 0A 0D 0A 03 4B 33 32 12 06 31 30 2E 31 2E 31 0A ....K32..10.1.1. 00000001010C6FE0 13 0A 03 4B 33 33 12 0C E4 B8 AD E5 9B BD E8 81 ...K33..中 國 .. 00000001010C6FF0 94 E9 80 9A 0A 09 0A 03 4B 33 37 12 02 43 4E 0A .通 ....K37..CN. 00000001010C7000 18 0A 03 4B 33 38 12 11 34 63 3A 65 39 3A 65 34 ...K38..4c:e9:e4 00000001010C7010 3A 38 34 3A 64 38 3A 38 30 0A 13 0A 03 4B 33 39 :84:d8:80
這些資訊可以大致分為:裝置ID、軟體特徵、硬體靜態特徵和硬體動態特徵幾個維度。
裝置ID:
裝置ID主要包括iOS裝置的 IDFA、IDFV,Android 裝置的 IMEI、MAC 等。這些ID本身就是蘋果和Google為了給APP廠商提供追蹤能力設計的標識,具備較好的唯一性和穩定性。
但是,作業系統為了保障使用者隱私,對APP的許可權做了較多的限制。比如,使用者可以自主選擇禁止APP獲取到這些ID,重置手機也會同時重置這些ID等。
而黑產也會利用這一特性,繞過APP廠商的識別策略。比如,黑產可以在蘋果系統中直接設定不允許獲取IDFA。這樣一來,APP 廠商的風控系統就沒有辦法通過裝置維度關聯黑產行為了,也就無法識別單一裝置批量操作的攻擊行為。
軟體特徵:
軟體靜態特徵主要是作業系統和APP本身的各類基本資訊,比如作業系統版本、手機名稱、APP版本等。這些資訊基本都可以通過更新或者手動配置的方式修改,因此在穩定性上表現較差。但是,這些資訊能夠反映出使用者的個人特徵,因此,它們能夠對裝置指紋的唯一性產生有一定的幫助。
比如,下圖是我手機的部分狀態資訊,其中的每一項都能夠直接或間接地代表我的部分資訊。比如,我使用了一張移動卡和聯通卡,我的手機型號是小米 9,我開著藍芽等。
硬體靜態特徵:
硬體靜態特徵主要是裝置的各類硬體資訊,比如,主機板、CPU、攝像頭等相關型號資訊。正常使用者基本不會去替換裝置上的各個硬體,因此硬體靜態特徵具備較高的穩定性。
但同一型號手機的硬體配置是一致的,所以,硬體靜態特徵在唯一性上相對欠缺。因此,通過硬體靜態特徵,我們無法很好地區分同型號的裝置。
硬體動態特徵:
硬體動態特徵基本原理是基於硬體的一些動態執行層產生的特徵(如:加速度感測器的偏差)來識別虛擬裝置。
舉個例子,因為加速度感測器校準結果的不精確性,其產生的最終結果會存在一定的偏差。通過多次快速地查詢加速度感測器,我們就可以模擬出同一時刻,加速度感測器返回的結果值。又因為存在機械偏差,所以這些結果值是不同的,那通過這些值,我們就可以計算出該感測器的線性偏差。
利用這樣的原理,我們可以採集任何一個感測器硬體的偏差特徵。因此,從穩定性上來說,硬體動態特徵的表現還是不錯的。不過由於特徵區間比較窄,唯一性稍差一些,更多被用來輔助區分同型號的不同裝置及裝置使用行為情況。
裝置指紋生成:
採集資訊之後,將基於這些資訊計算出一個正確的裝置指紋,是裝置指紋技術的核心。由於業內相同產品資料的維度和資料量的大小都各有不同,因此,各個公司生成裝置指紋的演算法略有不同。
裝置指紋生成需要解決的一個核心問題就是給出多組資訊,如何判定它們是不是來自同一個裝置或來自不同的裝置。
最基本的判定過程其實就是計算兩組資料的相似度,相似度越高、差異度越低,就越有可能是同一個裝置。
當伺服器收到新採集上來一組裝置資訊,我們要計算它和已有裝置資訊的相似度。比較簡單流行的演算法包括歐式距離、馬氏距離、聯合概率分佈等,相對複雜的包括馬爾科夫網路、置信度傳播演算法等。
通過演算法計算後,判斷是否到達設定的一個閾值,當新採集的資料與已有的相似度達到這個值之後,就可以判定這兩組裝置資料本質上都是同一臺裝置產生的。如果判定新採集資料屬於己有裝置,我們就分配相同的裝置指紋。如果屬於不同的裝置,我們就為新採集的資料生成新的裝置指紋,生成裝置指紋簡單流程如圖4所示:
圖4
五、裝置風險識別原理分析
5.1、裝置有什麼風險?
模擬器、修改裝置資訊、WiFi資訊、感測器、媒體和儲存、應用模擬、系統設定模擬、越獄、hook、除錯、注入等都是黑產作案過程中常見的手段。
該產品能識別的風險如圖5所示:
圖5
5.2、如何識別裝置風險?
檢測裝置是否越獄程式碼如下:
__text:0000000100040AFC ; bool __cdecl -[UIDevice stee_isJailbreak_5](UIDevice *self, SEL) __text:0000000100040AFC __UIDevice_stee_isJailbreak_5_ __text:0000000100040AFC __text:0000000100040AFC var_20 = -0x20 __text:0000000100040AFC var_10 = -0x10 __text:0000000100040AFC var_s0 = 0 __text:0000000100040AFC __text:0000000100040AFC F6 57 BD A9 STP X22, X21, [SP,#-0x10+var_20]! __text:0000000100040B00 F4 4F 01 A9 STP X20, X19, [SP,#0x20+var_10] __text:0000000100040B04 FD 7B 02 A9 STP X29, X30, [SP,#0x20+var_s0] __text:0000000100040B08 FD 83 00 91 ADD X29, SP, #0x20 __text:0000000100040B0C F3 03 01 AA MOV X19, X1 __text:0000000100040B10 F4 03 00 AA MOV X20, X0 __text:0000000100040B14 D6 83 00 94 BL _stee_vm_get __text:0000000100040B18 F5 03 00 AA MOV X21, X0 __text:0000000100040B1C E1 03 14 AA MOV X1, X20 __text:0000000100040B20 CE 83 00 94 BL _stee.add.i64 __text:0000000100040B24 E0 03 15 AA MOV X0, X21 __text:0000000100040B28 E1 03 13 AA MOV X1, X19 __text:0000000100040B2C CB 83 00 94 BL _stee.add.i64 __text:0000000100040B30 E1 45 80 52 MOV W1, #0x22F ; a2 __text:0000000100040B34 E0 03 15 AA MOV X0, X21 ; vcstack __text:0000000100040B38 AF 83 00 94 BL _stee.interp4.DXRiskStatic __text:0000000100040B3C E0 03 15 AA MOV X0, X21 __text:0000000100040B40 B8 83 00 94 BL _stee.ret.i32 __text:0000000100040B44 00 00 00 12 AND W0, W0, #1 __text:0000000100040B48 FD 7B 42 A9 LDP X29, X30, [SP,#0x20+var_s0] __text:0000000100040B4C F4 4F 41 A9 LDP X20, X19, [SP,#0x20+var_10] __text:0000000100040B50 F6 57 C3 A8 LDP X22, X21, [SP+0x20+var_20],#0x30 __text:0000000100040B54 C0 03 5F D6 RET
上面程式碼配合特徵來檢測裝置是否越獄,如果是越獄返回true,但是程式碼被虛擬機器保護了,越獄特徵如下:
"/Applications/Cydia.app" "/Applications/FakeCarrier.app" "/Applications/Icy.app" "/Applications/IntelliScreen.app" "/Applications/MxTube.app" "/Applications/RockApp.app" "/Applications/SBSettings.app" "/Applications/WinterBoard.app" "/Applications/blackra1n.app" "/Library/MobileSubstrate/DynamicLibrari"... "/Library/MobileSubstrate/DynamicLibrari"... "/Library/MobileSubstrate/MobileSubstrat"... "/System/Library/LaunchDaemons/com.ikey."... "/System/Library/LaunchDaemons/com.sauri"...
檢測裝置是否有代理流程如下:
__text:00000001000DAE6C get_network_info __text:00000001000DAE6C __text:00000001000DAE6C FF 43 04 D1 SUB SP, SP, #0x110 __text:00000001000DAE70 FC 6F 0B A9 STP X28, X27, [SP,#0x100+var_50] __text:00000001000DAE74 FA 67 0C A9 STP X26, X25, [SP,#0x100+var_40] __text:00000001000DAE78 F8 5F 0D A9 STP X24, X23, [SP,#0x100+var_30] __text:00000001000DAE7C F6 57 0E A9 STP X22, X21, [SP,#0x100+var_20] __text:00000001000DAE80 F4 4F 0F A9 STP X20, X19, [SP,#0x100+var_10] __text:00000001000DAE84 FD 7B 10 A9 STP X29, X30, [SP,#0x100+var_s0] __text:00000001000DAE88 FD 03 04 91 ADD X29, SP, #0x100 __text:00000001000DAE8C F4 03 00 AA MOV X20, X0 __text:00000001000DAE90 1F 20 03 D5 NOP __text:00000001000DAE94 E8 B3 4A 58 LDR X8, =___stack_chk_guard __text:00000001000DAE98 08 01 40 F9 LDR X8, [X8] __text:00000001000DAE9C A8 83 1A F8 STUR X8, [X29,#var_58] __text:00000001000DAEA0 E0 E3 01 91 ADD X0, SP, #0x100+var_88 ; this __text:00000001000DAEA4 2E 5C 00 94 BL __ZN14dx_risk_json114JsonC1Ev ; dx_risk_json11::Json::Json(void) __text:00000001000DAEA8 4E 05 01 94 BL _CFNetworkCopySystemProxySettings __text:00000001000DAEAC F3 03 00 AA MOV X19, X0 __text:00000001000DAEB0 53 02 00 B4 CBZ X19, loc_1000DAEF8 __text:00000001000DAEB4 1F 20 03 D5 NOP __text:00000001000DAEB8 40 77 56 58 LDR X0, =_OBJC_CLASS_$_NSNull ; void * __text:00000001000DAEBC 1F 20 03 D5 NOP __text:00000001000DAEC0 41 FD 55 58 LDR X1, =unk_189D7DF07 ; char * __text:00000001000DAEC4 52 06 01 94 BL _objc_msgSend __text:00000001000DAEC8 FD 03 1D AA MOV X29, X29 __text:00000001000DAECC 65 06 01 94 BL _objc_retainAutoreleasedReturnValue __text:00000001000DAED0 F5 03 00 AA MOV X21, X0 __text:00000001000DAED4 1F 20 03 D5 NOP __text:00000001000DAED8 C1 FC 55 58 LDR X1, =unk_189D7EBF6 ; char * __text:00000001000DAEDC E0 03 13 AA MOV X0, X19 ; void * __text:00000001000DAEE0 E2 03 15 AA MOV X2, X21 __text:00000001000DAEE4 4A 06 01 94 BL _objc_msgSend ; isEqual:返回1跳過代理檢測
isEqual:判斷是否有代理,返回1就可以跳過代理檢測
六、破解虛擬機器保護
6.1、什麼是虛擬機器保護?
虛擬機器保護是一種基於虛擬機器的程式碼保護技術。它將基於彙編系統中的可執行程式碼轉換為位元組碼指令系統的程式碼,來達到不被輕易逆向和篡改的目的。簡單點說就是將程式的程式碼轉換自定義的操作碼(opcode),然後在程式執行時再通過解釋這些操作碼,選擇對應的函式執行,一條指令對應N多函式,從而實現程式原有的功能。
6.2、虛擬機器流程分析
虛擬機器主入口:
每個函式基本都被虛擬保護,入口程式碼如下:
__text:00000001000DAE20 __text:00000001000DAE20 F6 57 BD A9 STP X22, X21, [SP,#-0x10+var_20]! __text:00000001000DAE24 F4 4F 01 A9 STP X20, X19, [SP,#0x20+var_10] __text:00000001000DAE28 FD 7B 02 A9 STP X29, X30, [SP,#0x20+var_s0] __text:00000001000DAE2C FD 83 00 91 ADD X29, SP, #0x20 __text:00000001000DAE30 F3 03 01 AA MOV X19, X1 __text:00000001000DAE34 F4 03 00 AA MOV X20, X0 __text:00000001000DAE38 0D FB 00 94 BL _stee_vm_get __text:00000001000DAE3C F5 03 00 AA MOV X21, X0 __text:00000001000DAE40 E1 03 14 AA MOV X1, X20 __text:00000001000DAE44 05 FB 00 94 BL _stee.add.i64 __text:00000001000DAE48 E0 03 15 AA MOV X0, X21 __text:00000001000DAE4C E1 03 13 AA MOV X1, X19 __text:00000001000DAE50 02 FB 00 94 BL _stee.add.i64 __text:00000001000DAE54 61 70 80 52 MOV W1, #0x383 __text:00000001000DAE58 E0 03 15 AA MOV X0, X21 __text:00000001000DAE5C FD 7B 42 A9 LDP X29, X30, [SP,#0x20+var_s0] __text:00000001000DAE60 F4 4F 41 A9 LDP X20, X19, [SP,#0x20+var_10] __text:00000001000DAE64 F6 57 C3 A8 LDP X22, X21, [SP+0x20+var_20],#0x30 __text:00000001000DAE68 E3 FA 00 14 B _stee.interp4.DXRiskStatic
每一個被保護的函式虛擬機器入口基本相同,獲取虛擬堆疊,傳入方法代號找到對應的opcode碼
獲取opcode碼:
__int64 __fastcall stee_interp4_DXRiskStatic(__int64 vcstack, int Funcindex, double a3) { return vm_dispatcher_hander( vcstack, (__int64)&unk_10011F160, (__int64)off_10011E690, &byte_1000D1F70[dword_100114150[Funcindex]], a3);//取opcode碼 }
解密opcode並解釋執行,程式碼流程如下:
__int64 __fastcall vm_dispatcher_hander(signed __int64 vcstack, __int64 a2, __int64 hander, unsigned __int8 *bycode, double ret) { unsigned __int8 *m_bycode; // x23 __int64 m_hander; // [xsp+8h] [xbp-58h] m_bycode = bycode; m_hander = hander; v6 = a2; m_vmstack = vcstack; m_vmstack_1 = (double **)(m_vmstack + 8); m_vmstack_2 = *(_QWORD *)(m_vmstack + 8); opcode1 = (signed __int64)(m_bycode + 4); Operands = (_QWORD *)(m_vmstack_2 - 4 * (*m_bycode | ((unsigned __int64)m_bycode[1] << 8))); *(_QWORD *)(m_vmstack + 8) = m_vmstack_2 + 4 * (m_bycode[2] | ((unsigned __int64)m_bycode[3] << 8)); ++opcode1; switch ( *opcode2 ) { case 7u: v19 = *(_DWORD *)(opcode2 + 1); opcode1 = (signed __int64)(opcode2 + 5); if ( v19 >= 2 ) goto LABEL_17; goto LABEL_192; case 0xAu: v166 = opcode2[1] | (opcode2[2] << 8); opcode1 = (signed __int64)(opcode2 + 3); if ( v166 < 2 ) goto LABEL_382; goto LABEL_216; case 0xBu: v102 = *m_vmstack_1; v103 = *m_vmstack_1 - 1; v104 = *(_DWORD *)v103; *m_vmstack_1 -= 2; v105 = v104 & 0x3F; if ( !v105 ) { *m_vmstack_1 = v103; continue; } v106 = *((_QWORD *)v102 - 2); LOBYTE(v105) = 64 - v105; LABEL_288: *(_QWORD *)&v208 = __ROR8__(v106, v105); goto LABEL_302; case 0xCu: v107 = *((_DWORD *)*m_vmstack_1 - 1); v108 = (double *)((char *)*m_vmstack_1 - 12); *m_vmstack_1 = v108; v109 = *v108; goto LABEL_136; case 0xDu: v110 = opcode2[1]; opcode1 = (signed __int64)(opcode2 + 2); if ( v110 >= 2 ) { v111 = *(__int64 (**)(void))(v6 + 8 * v110); goto LABEL_322; } v237 = (_DWORD)v110 == 1; goto LABEL_396; case 0xEu: v31 = *m_vmstack_1; ret = *(*m_vmstack_1 - 2); v32 = (signed __int64)*m_vmstack_1 - 12; v33 = ret >= *(*m_vmstack_1 - 1); goto LABEL_394; case 0xFu: v57 = *m_vmstack_1; ret = -3.59538627e308/*NaN*/; case 0x11u: v21 = *m_vmstack_1; v22 = (signed __int64)*m_vmstack_1 - 4; cmdata = **((signed __int16 **)*m_vmstack_1 - 1); goto LABEL_282; v27 = &opcode2[2 * v25 + 3]; goto LABEL_275; case 0x13u: v15 = (double *)((char *)*m_vmstack_1 - 4); v16 = *((_DWORD *)*m_vmstack_1 - 2) >= *(_DWORD *)v15; goto LABEL_7; case 0x14u: v28 = *m_vmstack_1; v29 = -1; goto LABEL_424; case 0x15u: LODWORD(v186) = (char)opcode2[1]; opcode1 = (signed __int64)(opcode2 + 2); v187 = *m_vmstack_1; LODWORD(v188) = (signed int)v186 >> 7; goto LABEL_421; case 0x16u: v68 = opcode2[1] | (opcode2[2] << 8); opcode1 = (signed __int64)(opcode2 + 3); if ( v68 >= 2 ) goto LABEL_67; goto LABEL_199; case 0x17u: v15 = (double *)((char *)*m_vmstack_1 - 4); LODWORD(ret) = *(_DWORD *)v15; v16 = *((float *)*m_vmstack_1 - 2) > *(float *)v15; goto LABEL_7; case 0x18u: v30 = *m_vmstack_1; *(_DWORD *)v30 = 0; *m_vmstack_1 = (double *)((char *)v30 + 4); continue; case 0x1Au: v79 = *m_vmstack_1; v80 = (signed __int64)*m_vmstack_1 - 12; v81 = *((_QWORD *)*m_vmstack_1 - 2) > *((_QWORD *)*m_vmstack_1 - 1); goto LABEL_392; case 0x1Cu: v36 = *m_vmstack_1 - 1; v48 = *((_QWORD *)*m_vmstack_1 - 2) << (*(_DWORD *)v36 & 0x3F); goto LABEL_326; case 0x1Du: v36 = *m_vmstack_1 - 1; ret = *(*m_vmstack_1 - 2) - *v36; goto LABEL_325; case 0x1Eu: v226 = *m_vmstack_1; --*m_vmstack_1; *((_DWORD *)v226 - 2) = **((unsigned __int8 **)v226 - 1) | (*(unsigned __int8 *)(*((_QWORD *)v226 - 1) + 1LL) << 8); *m_vmstack_1 = (double *)((char *)v226 - 4); continue; case 0x1Fu: v21 = *m_vmstack_1; --*m_vmstack_1; v22 = (signed __int64)v21 - 4; cmdata = **((char **)v21 - 1); goto LABEL_282; case 0x20u: v15 = (double *)((char *)*m_vmstack_1 - 4); v16 = *((_DWORD *)*m_vmstack_1 - 2) <= *(_DWORD *)v15; goto LABEL_7; case 0x21u: v44 = (float *)((char *)*m_vmstack_1 - 4); v160 = *(unsigned int *)v44; v42 = v44 - 1; --*m_vmstack_1; v161 = *((unsigned int *)v44 - 1); if ( v161 <= 0xFF800000 ) v227 = (v161 > 0x7F800000 && (v161 & 0x80000000) == 0) == 0; else v227 = 0; if ( !v227 ) goto LABEL_430; if ( v160 > 0xFF800000 || v160 > 0x7F800000 && (v160 & 0x80000000) == 0 ) goto LABEL_432; v163 = v161 ^ v160; LODWORD(ret) = v160; if ( *(float *)&v161 > *(float *)&v160 ) *(float *)&ret = *(v44 - 1); v164 = *(float *)&ret; if ( v161 <= v160 ) v160 = *((unsigned int *)v44 - 1); goto LABEL_339; case 0x22u: v36 = *m_vmstack_1; ret = *(*m_vmstack_1 - 1); v48 = (unsigned __int64)ret; goto LABEL_326; case 0x23u: v40 = (float *)((char *)*m_vmstack_1 - 4); *(float *)&ret = *v40 + *((float *)*m_vmstack_1 - 2); goto LABEL_316; case 0x24u: v100 = opcode2[1]; opcode1 = (signed __int64)(opcode2 + 2); goto LABEL_344; case 0x26u: v79 = *m_vmstack_1; v80 = (signed __int64)*m_vmstack_1 - 12; v81 = *((_QWORD *)*m_vmstack_1 - 2) <= *((_QWORD *)*m_vmstack_1 - 1); goto LABEL_392; case 0x27u: v40 = (float *)((char *)*m_vmstack_1 - 4); *(float *)&ret = *((float *)*m_vmstack_1 - 2) - *v40; goto LABEL_316; v29 = v51 - v51 / v50 * v50; LABEL_424: *(_DWORD *)v28 = v29; *m_vmstack_1 = (double *)((char *)v28 + 4); continue; case 0x65u: Operands_1 = ((unsigned __int16)(opcode2[1] | (unsigned __int16)(opcode2[2] << 8)) | ((unsigned __int64)opcode2[3] << 16)) & 0xFFFFFFFF00FFFFFFLL | ((unsigned __int64)opcode2[4] << 24); opcode1 = (signed __int64)(opcode2 + 5); goto LABEL_252; case 0x67u: v15 = (double *)((char *)*m_vmstack_1 - 4); v16 = *(_DWORD *)(*m_vmstack_1 - 1) & *(_DWORD *)v15;// 加密資料 goto LABEL_7; case 0x69u: v229 = opcode2[1] | ((unsigned __int64)opcode2[2] << 8); opcode1 = (signed __int64)(opcode2 + 3); case 0x93u: v57 = *m_vmstack_1; *(_QWORD *)&ret = 1LL; LABEL_369: *v57 = ret; *m_vmstack_1 = v57 + 1; continue; case 0x94u: Operands_1 = opcode2[1]; opcode1 = (signed __int64)(opcode2 + 2); LABEL_252: (*(void (__fastcall **)(signed __int64))(m_hander + 8 * Operands_1))(m_vmstack + 8);// 執行出口hander,呼叫外面方法 continue; case 0x95u: v204 = *m_vmstack_1; *m_vmstack_1 = (double *)((char *)*m_vmstack_1 - 4); v205 = *((_DWORD *)v204 - 1); if ( v205 >= (opcode2[1] | ((unsigned int)opcode2[2] << 8)) ) { v206 = objc_msgSend(&OBJC_CLASS___NSString, (const char *)&unk_189D7DBA4, "VM Error: impossible", ret); NSLog(v206); } opcode1 = (signed __int64)&opcode2[*(signed int *)&opcode2[4 * v205 + 3]]; continue; case 0x96u: opcode1 = (signed __int64)(opcode2 + 3); v113 = &opcode2[opcode2[1] | (unsigned __int16)(opcode2[2] << 8)]; LABEL_148: vm_dispatcher_hander(m_vmstack, v6, m_hander, v113, ret); continue; case 0x97u: v79 = *m_vmstack_1; v80 = (signed __int64)*m_vmstack_1 - 12; v81 = *((_QWORD *)*m_vmstack_1 - 2) >= *((_QWORD *)*m_vmstack_1 - 1); continue; case 0xFFu: v34 = *m_vmstack_1; v82 = *((_QWORD *)*m_vmstack_1 - 1); LABEL_250: *((_DWORD *)v34 - 2) = __clz(v82); LABEL_268: *((_DWORD *)v34 - 1) = 0; *m_vmstack_1 = v34; continue; default: continue; } } }
上面就是虛擬機器最核心的直譯器程式碼片斷,邏輯也註釋清楚,主要功能就是解密opcode、模擬執行opcode、常見的邏輯運算都被模擬了,所以加解 密演算法也是在虛擬中完成的,要還原演算法還是難度比較高的,但是如果opcode是呼叫外部系統方法就跳出虛擬機器。程式碼如下:
LABEL_252: (*(void (__fastcall **)(signed __int64))(m_hander + 8 * Operands_1))(m_vmstack + 8);// 執行出口hander,呼叫外面方法 continue;
所以opcode為0xFC表示call xxx呼叫外部方法,其實只要守在這個地方,基本邏輯功能也能逆向出來。整體流程如圖6所示:
圖6
七、破解裝置指紋與風險識別
7.1、抓包分析
通過抓包可以看來請求引數資料與返回資料,如圖7所示:
圖7
各引數解釋,如圖8所示:
圖8
上面標紅的引數就是我們要攻擊目標。
ConstID攻擊:如果是同一臺裝置永遠不會變化,攻擊方式是用同一臺裝置返回不同的ConstID就算攻擊成功。
風險識別攻擊:在裝置越獄且使用代理的情況下返回"否"就算攻擊成功。
7.2、破解裝置指紋與風險識別
通過前面對虛擬機器保護的分析,知道它最終於會走外部方法時跳出虛擬機器,我們只要在該出口守住即可。然後在詳細分析各外部方法功能,程式碼邏輯如下:
__text:00000001000C23D4 F4 4F BE A9 STP X20, X19, [SP,#-0x10+var_10]! __text:00000001000C23D8 FD 7B 01 A9 STP X29, X30, [SP,#0x10+var_s0] __text:00000001000C23DC F3 03 00 AA MOV X19, X0 __text:00000001000C23E0 74 02 40 F9 LDR X20, [X19] __text:00000001000C23E4 FD 43 00 91 ADD X29, SP, #0x10 __text:00000001000C23E8 88 8E 5E F8 LDR X8, [X20,#-0x18]! __text:00000001000C23EC 80 86 40 A9 LDP X0, X1, [X20,#8] __text:00000001000C23F0 00 01 3F D6 BLR X8 __text:00000001000C23F4 74 02 00 F9 STR X20, [X19] __text:00000001000C23F8 FD 7B 41 A9 LDP X29, X30, [SP,#0x10+var_s0] __text:00000001000C23FC F4 4F C2 A8 LDP X20, X19, [SP+0x10+var_10],#0x20 __text:00000001000C2400 C0 03 5F D6 RET
get_network_info:檢測裝置是否正在使用代理。
_UIDevice_stee_isJailbreak_1:檢測裝置是否越獄。
最後通過key:value的方式組合壓縮加密上報組報給伺服器,伺服器做出相應的迴應,對外部方法的分析,整理出如下欄位對應關係,如果要做協議刷介面的方式就全部還原出來。
K23:是否越獄 (true:越獄, false:非越獄) K3:是否代理(true:代理, false:非代理) k1:mac地址 k5:idfv k6:idfa
基於以上分析邏輯關係,破解的方法有如下幾種:
第一種、篡改函式返回值:直接篡改檢測裝置狀態後的返回值,改成false,非越獄、非代理、非除錯等
第二種、篡改組合後的key:value值:將記憶體中組好的值進行修改為我們想要的,但是前提是須要對組合邏輯與對應的key關係分析清楚。
第三種、協議直接刷介面:分析完整個產品的邏輯、加解密演算法、欄位對應關係即可脫離產品本身程式碼,自實現有伺服器互動的邏輯模擬請求,由於加解密演算法都被虛擬機器保護,要全部還原花費時間成本太高。
我選擇的攻擊方法是第二種在記憶體中修其組合好的引數,最終在同一臺越獄裝置上成功攻擊,如圖9所示:
圖9
與上面對比,constId裝置id是一個新的,裝置風險全為否,攻擊成功。
八、總結
通過攻擊視角對裝置指紋技術進行基本原理性的學習瞭解。裝置指紋是風控系統中對裝置實現長期追蹤和異常識別的一種關鍵技術。與黑產的對抗中會起到一定的作用,但它不是銀彈,黑產也會用各種方式破解攻擊裝置指紋,生成或收集生成好的裝置指紋,因此裝置指紋也須要時效性。
從整體來看該產品與業內同產品相比較安全方面做得還是比較高水準的,通過虛擬機器保護真正的做到了加解密邏輯的隱藏,防止被還原直接刷介面,但是百密一疏終有一漏,因此不斷升級更新演算法補漏與設定一定的策略來識別被攻破的行為還是很有必要的。
歡迎關注公眾號: