在本文中,我們將介紹如何使用Frida繞過一些應用程序實現的反調試技術的實際示例。
FridaFrida是一個動態代碼檢測工具包。換句話說,它是一組允許代碼插裝的工具,提供給我們一些API,使我們能夠在執行過程中攔截、分析和修改Windows、macOS、GNU/Linux、iOS、Android和QNX應用程序的部分代碼。本質上,Frida允許在運行時對即將執行的操作進行操作。比如,當我們從一個簡單的c++程序開始,該程序使用一個函數將兩個值相加並返回結果。我們想要操作的函數聲明為Add(int,int)。首先,我們將更改其中一個int參數,然後,更改返回的結果。編譯完代碼後,我們需要通過分析可執行代碼來定位Add函數的偏移量。在本例中,我們使用IDA Pro來分析代碼,但也可以使用允許我們分析可執行代碼的任何其他方法或應用程序。我們在偏移量0x00401000處確定函數,並且我們知道可執行文件的基址是0x00400000,因此,函數的偏移量是0x00001000。
然後,我們可以攔截函數調用並使用Frida修改其行為。我們將開發一個小Javascript腳本來完成這項工作。首先,我們需要確定程序在執行時被加載的位置、它的基址,然後,我們可以通過將這個基址和之前獲得的偏移量(base +0x00001000)相加來定位Add函數的偏移量。現在,我們可以使用帶有函數地址的Interceptor來在函數執行之前或之後添加一些代碼。我們使用Frida Javascript API 來完成這一切。
因此,當我們以' 1 '和' 2 '作為參數執行應用程序時,我們期望結果是' 1 + 2=3 '。但是,如果我們取消註釋第一行(args[0]=ptr(' 100 ');)在函數執行之前,我們將變量op1的值替換為100,得到的結果為' 1 + 2=102 '。另一方面,如果取消註釋第二行(retval.replace(' 3210 ')) ,我們會在函數Add 執行之後但在返回結果之前替換返回值,得到'1 + 2=3210'。
基於系統調用的技術在這個檢測結果中,我們考慮使用Windows API的函數來獲取與調試器存在相關的信息的技術。有很多函數可以用於這個目標:從像IsDebuggerPresent這樣的函數,它返回一個布爾值,這個值取決於應用程序是否被調試(True或False),到像FindWindow這樣的函數,它告訴我們是否存在一個帶有知名調試器(IDA、Ollydbg、Inmunity 調試器等)名稱的窗口。
基於內存檢查的技術應用程序對內存中的某些標誌進行顯式驗證的方法,這些標誌顯示進程是否正在調試。可以用於此目的的一些標誌是IsDebugged 標誌、Heap標誌或NTGlobalFlag。這些標誌是Windows 為每個進程維護的結構的成員,其中包含有關它們的信息。
基於時間的技術這個檢測結果包括使用與時間相關的計算來確定是否正在調試進程的方法,當一個進程正在被調試時,執行同一組指令所花費的時間要比未被調試時多。這種時差通常是很明顯的。出於這個原因,應用程序可以檢查一組指令執行開始和結束的時間,如果它花費的時間超過一個既定的閾值,它可以很有可能確定該過程是正在調試。
基於異常的技術最後,我們根據觸發異常對一組方法進行分組,以確定進程是否正在被調試。在調試進程和未調試進程時,系統處理異常的方式是不同的。程序可以利用這一事實來確定是否附加了調試器。
設置測試環境為了實現我們的設置,我們將使用Windows 10虛擬機,我們最初將在其中安裝Python 3.8.6rc1。
然後我們安裝Frida,它可以直接從GitHub下載,或者使用Python pip工具安裝。我們使用pip是因為它比其他方法更簡單。
我們還安裝了Visual Studio Community 2019來開發示例程序,在示例程序中我們實現了一些反調試技術來展示它是如何工作的。這些程序將被用來測試繞過這些反調試技術的不同方法。 Frida 的使用方式有很多種:我們可以直接使用包中包含的可執行文件(每個可執行文件都有特定的功能),也可以使用包中也包含的Python 模塊來開發我們自己的接口。
我們選擇了第二種方法,開發了一個小接口,允許我們生成新的進程或附加到現有進程中,注入一個或多個提供某些功能的腳本。我們選擇這種方式是因為我們希望能夠根據特定的需求定制接口,這些需求將在以後的文章中詳細介紹。
接下來,我們將討論基於以下系統調用的技術:IsDebuggerPresent、NtQueryInformationProcess 和CreateToolhelp32Snapshot。所有這些都使我們能夠了解如何以不同的方式使用Frida,以繞過這些控制。
IsDebuggerPresent我們將嘗試繞過的第一個檢查是基於系統調用IsDebuggerPresent 的方法。正如Microsoft 文檔中所指出的,此函數不接收任何參數,並根據進程是否正在調試返回一個布爾值:返回值“True”表示正在調試進程,“False”表示相反。為了說明這個方法,我們開發了一個使用這個系統調用執行調試檢測的簡單程序:
在第一個控制台中,我們看到當我們從Visual Studio執行應用程序時,應用程序如何指示它正在被調試。但是,如果應用程序是直接從終端執行的,則表明它沒有被調試。檢查這個事實的另一種方法是從像x64dbg 這樣的調試器中執行它。唯一用來做決定的是函數IsDebuggerPresent 返回的值,因此,我們應該用Frida 開發一個腳本,攔截這個調用並修改返回值,總是返回False (0x0)。下面的腳本是為了完成所有這些而開發的:
首先,它使用Module.findExportByName(第3 行)定位函數“IsDebuggerPresent”的地址。
一旦獲得這個地址,它就會使用Interceptor.attach(.) 攔截這個調用(第8 行)。
最後,它在函數結束之前(第13 行)將返回值替換為0x0 (False)。
NtQueryInformationProcess我們將展示的第二個系統調用是NtQueryInformationProcess。這個函數讓我們獲得與進程相關的不同信息。它比上一個更複雜,因為它允許我們使用ProcessInformationClass參數選擇要查詢的信息,並且它將在ProcessInformation參數上為我們提供這些信息。
在本例中,我們將展示如何繞過4 項檢查,每項檢查使用不同的ProcessInformationClass 值。這些值是:ProcessDebugPort、ProcessDebugFlags、ProcessDebugObjectHandle 和ProcessBasicInformation。
ProcessDebugPort (0 x7)如果附加了任何調試器,此類用於獲取調試器的端口號。如果附加了調試器,此值將不同於0。
ProcessDebugFlags (0x1F)使用此類,我們可以檢索一個標誌,該標誌將為我們提供有關活動調試器存在的信息。在本例中,如果processinformation參數中返回的值為0,則表明正在調試應用程序。
ProcessDebugObjectHandle (0x1E)將此值表示為ProcessInformationClass,僅當正在調試進程時,此系統調用才會返回有效的句柄。
ProcessBasicInformation (0 x0)使用這個類,我們在ProcessInformation 參數中獲得了一個名為PROCESS_BASIC_INFORMATION 的結構,其中包括其他數據:指向PEB 結構的指針(偏移量0x4)、進程的PID(偏移量0x16)和父進程的PID(偏移量0x20)。軟件可以使用此信息應用的一種反調試技術是使用父進程的PID 獲取父進程的名稱,並將其與眾所周知的調試器名稱列表進行檢查。
如上所述,我們開發了一個小型C++ 應用程序,展示了這種技術的一個示例。為了逃避這些檢查,我們必須使用Frida 開發一個必須執行以下操作的腳本:
首先,它必須使用Module.findExportByName 定位函數“NtQueryInformationProcess”的地址。
然後,它必須使用Interceptor.attach(.) 攔截函數調用。
每次調用函數“NtQueryInformationProcess”(OnEnter)時,腳本必須執行以下操作:
保存參數ProcessInformationClass,允許我們選擇必須修改哪些返回信息(第40 行)。
保存指向返回參數ProcessInformation 的指針(第44、48、52 和57 行)。
還要保存每種情況下所需的參數。
最後,就在函數結束之前(OnLeave),腳本可以使用之前保存的信息來確定需要使用參數ProcessInformation 返回什麼值。根據this.ProcessInformationClass 我們應該返回以下值:
0x7 (ProcessDebugPort),ProcessInformation 將被替換為值0x0(第63 行)。
0x1F (ProcessDebugFlags),ProcessInformation 將被替換為值0x1(第68 行)。
0x1E(ProcessDebugObjectHandle),在本例中,我們將檢查返回值,如果成功,則將參數ProcessInformation替換為0x0。參數ReturnLength 也將被替換(第72 - 81 行)。
0x0 (ProcessBasicInformation),最後,如果選擇了這個選項,我們應該知道PROCESS_BASIC_INFORMATION 結構體將父進程的PID (InheritedFromUniqueProcessId, offset0x20) 替換為一個非可疑的PID,例如進程‘explorer.exe’ 的PID。
要使用Frida 獲取進程“explorer.exe”的PID,我們可以使用Windows API 調用,例如函數GetShellWindow 和GetWintowThreadProcessId。我們可以使用NativeFunction del API de Frida 通過Javascript 聲明這些函數,一旦它們被聲明,我們就可以使用它們來獲取進程PID,如下所示:
使用Visual Studio調試器執行這個示例程序,我們得到如下結果:
如果我們使用相同的調試器執行相同的應用程序,但注入之前描述的Frida 腳本,我們會得到另一個結果:
CreateToolhelp32Snapshot接下來我們將討論CreateToolhelp32Snapshot函數。使用這個函數的最常見的反調試技術是驗證父進程的名稱和PID,確定父進程是否是一個知名的調試器,但是這個函數也可以用於其他目的。首先,這個函數創建一個包含一些關於進程、線程和模塊的系統信息的快照。可以使用第一個參數選擇該快照的信息。允許使用以下值:TH32CS_INHERIT、TH32CS_SNAPALL、TH32CS_SNAPHEAPLIST、TH32CS_SNAPMODULE、TH32CS_SNAPMODULE32、TH32CS_SNAPPROCESS、TH32CS_SNAPTHREAD。
使用該函數最基本的反調試技術是只使用TH32CS_SNAPPROCESS來獲取正在運行的進程列表。然後,查找我們的進程,識別我們的進程父進程的PID,然後在列表中查找父進程的信息。最後,驗證父進程的信息。但是,通過使用不同標誌或使用TH32CS_SNAPALL的函數給出的信息,我們可以執行其他驗證。例如:
處理相關驗證(使用TH32CS_SNAPPROCESS):
在整個列表中搜索被禁止的進程(如VsDebugConsole.exe、devenv.exe、x32dbg.exe.),無論該進程是否相關;
模塊相關驗證(使用TH32CS_SNAPMODULE和TH32CS_SNAPMODULE32):
在與我的進程相關的模塊列表中搜索被禁止的模塊(如frida-agent.dll .)。
線程相關驗證(使用TH32CS_SNAPTHREAD):
驗證列出的所有線程都有一個相關聯的進程,試圖檢測隱藏的進程;
驗證應用程序線程數;
驗證我的應用程序中沒有引用禁止模塊的任何線程。
可見,使用此函數的應用程序可以驗證必須在它們之間保持一致的不同內容。因此,要繞過這些檢查,我們的任務必須是找到一種變通方法,允許我們繞過所有這些檢查,並保持列表的一致性。帶著這個目標,我們研究了這個函數究竟返回了哪些信息,以及我們如何操作它。
函數CreateToolhelp32Snapshot 返回一個SECTION 類型的HANDLE。如果我們分析HANDLE 指向的內存部分,我們可以找到一個未記錄的結構,其中包含以下部分:
我們可以使用NtQuerySection和NtMapViewOfSection函數來修改與Handle相關的內存,從而修改CreateToolhelp32Snapshot函數返回的快照的內容。因此,我們可以開發一個Frida腳本來修改CreateToolhelp32Snapshot返回的列表,隱藏可以檢查以檢測調試器的進程、模塊和線程,但保持了其一致性。
首先,我們應該定義一個應該隱藏的進程和模塊列表。我們需要知道他們的名字。在這個例子中,我們將隱藏Visual Studio調試器以及與Frida相關的可執行文件和模塊。所以我們定義了2個列表,內容如下:
禁止進程列表:VsDebugConsole.exe、devenv.exe、frida-winjector-helper-32.exe
禁止模塊列表:frida-agent.dll
一旦我們確定要隱藏哪些進程和模塊,我們將像以前一樣掛鉤函數CreateToolhelp32Snapshot。在本例中,我們不修改任何參數或返回值。我們將在函數返回句柄之前截取函數返回句柄,並在返回發生之前執行一些代碼。正如我們之前看到的,使用函數NtQuerySection和NtMapViewOfSection,我們將定位與句柄相關的內存,我們將執行以下步驟:
進程列表修改:
我們應該從進程列表中刪除那些在禁止進程列表中找到的進程。
我們應該刪除對禁止進程的引用,以保持一致性。
例如,我們將要調試的程序可能有一個被禁止的進程作為父進程。因此,我們應該將Parent 的PID 值更改為非可疑進程的PID(如explorer.exe PID)。
模塊列表修改:
我們應該從模塊列表中刪除那些在禁止模塊列表中找到的模塊。
線程列表修改:
我們應該從線程列表中刪除那些由進程列表中刪除的進程擁有的線程;
我們應該從線程列表中刪除那些指向從模塊列表中刪除的模塊的線程;
我們可以通過使用具有THREAD_QUERY_INFORMATION 權限的函數OpenThread 和請求ThreadQuerySetWin32StartAddress 的NtQueryInformationThread 來獲取此信息。將此查詢獲得的信息與與禁止模塊關聯的內存進行比較,我們可以確定是否應該從列表中刪除線程。
Recommended Comments