由一道CTF對10種反調試的探究

0x00 前言

最近做的有些ctf中總是出現一些反動態調試的情況。由次對一些常見的反動態調試進行一些總結。既然是調試,趁着這個機會探究了一下調試器如何與被調試進程建立聯繫的過程。

參考文章:

https://blog.csdn.net/hgy413/article/details/7996652
https://blog.csdn.net/yiyefangzhou24/article/details/6242459
https://www.52pojie.cn/thread-883664-1-1.html

https://bbs.pediy.com/thread-223857.htm

http://bbs.pediy.com/showthread.php?t=31447

https://blog.csdn.net/qq_32400847/article/details/52798050

0x01 CTF—wp

先運行一下

 

 

到ida裏看一下

 

發現4010D7的位置無緣無故跳到了4010DE,就是這塊的問題,在x32dbg搜字符串定位,把4010D7~4010E0都nop掉,

得到新的文件。重新載入ida。

 

 

經過逆向分析首先經過10次反調試,只有在不被調試的情況下才能組成正確的str1,然後對flag普通base64加密,再

進行奇偶位分別與str1與[0ff_404018^3]比較。反調試的函數主要在下面分析。

腳本如下:

import base64
str1 = "LKd8gPYWS["
str2 = "2TVBnx0lnn"
cipher = [0] * 20
for i in range(10):
    cipher[2*i] = (ord(str1[i]) ^ 3) - 2
    cipher[2*i+1] = ord(str2[i])
print''.join(map(chr,cipher))
#M2FTeV9BbnQxX0RlNnVn
end_cipher = 'M2FTeV9BbnQxX0RlNnVn'
print"D0g3{"+end_cipher.decode("base64")+"}"
#D0g3{3aSy_Ant1_De6ug}

0x02 對反調試的探究

  上題中反調試的函數在接下來具體說明

1.IsDebuggerPresent

 

用windbg看一下IsDebuggerPresent的反彙編

kernel32!IsDebuggerPresent:
7c813133 64a118000000    mov     eax,dword ptr fs:[00000018h]// fs寄存器在3環的時候指向TEB,而+18h偏移處指向teb的開頭fs:003b:00000018=7ffdf000
7c813139 8b4030                mov     eax,dword ptr [eax+30h]//+30h指向PEB

7c81313c 0fb64002             movzx   eax,byte ptr [eax+2]//peb->BeingDebugged位來判斷是否有調試器。

本題第一次的判斷直接判斷的BeingDebugged不再贅述了。

2.CheckRemoteDebuggerPresent

這個函數檢查的是你獲取的進程是否被另一個進程調試。

看一下反彙編(下面截取了關鍵的部分)

7C85AA3C    50              push eax                           //eax裏是 hProcess

7C85AA3D    6A 07           push 7                            // 這裏的7定義是 ProcessDebugPort

7C85AA3F    FF75 08        push dword ptr SS:[ebp+8]                    //hProcess

7C85AA42    FF15 AC10807C  call dword ptr DS:[<&ntdll.NtQueryInform> ]           //ntdll.NTQueryInformationProcess

7C85AA48    85C0            test eax,eax                       //判斷

可以看出實際調了NtQueryInformationProcess,它可以將指定類型的進程信息拷貝到某個緩衝,

函數原型如下:

NtQueryInformationProcess (
IN HANDLE ProcessHandle, // 獲取進程的句柄
IN PROCESSINFOCLASS InformationClass, // 信息類型
OUT PVOID ProcessInformation, // 緩衝區的指針
IN ULONG ProcessInformationLength, // 緩衝區大小
OUT PULONG ReturnLength OPTIONAL // 寫入緩衝區的字節數
);

其中ProcessInformationClass中的ProcessDebugPort,它來檢索調試器的端口號,只要是非0則有調試器。

在下文第6個反調試中會看到DebugPort的詳細說明。

 

3.SetLastError & OutputDebugStringA & GetLastError

 

OutputDebugStringA :它可以把調試信息輸出到編譯器的輸出窗口,和調試器進行交談,(還可以用DbgView來看),如果被調試,那麼調用就會成功,SetLastError設置的“12345”就會被覆蓋,那麼GetLastError也不會成功得到“12345”,由此來檢測是否被調試。

4.NtQueryInformationProcess

上文提到了,這就不說了。

5.CloseHandle異常

Windows在執行異常處理時,無論是內核異常還是用戶異常,在進行異常信息的“包裝”後,都會到KiDispatchException進行異常的分發,下面逆了此函數的一部分:

內核異常的分發(部分):

用戶異常的分發(部分):

 

可以看出來,無論是用戶異常還是內核異常,再進行VEH,SEH之前都會先判斷是否用調試器,利用該特徵可判斷進程是正常運行還是調試運行,然後根據不同的結果執行不同來判斷程序是否被調試。

6.DebugActiveProcess

 

先看一下這個函數

DebugActiveProcess會使調試器附加到獲取的進程上並且調試它。此函數的唯一參數是進程的PID。

BOOL WINAPI DebugActiveProcess(
  __in DWORD dwProcessId//要被調試的進程標識PID
  );

以下是對調試器利用DebugActiveProcess的深究

調試器調試程序的時候,一種是直接打開進程,另一種就是用Attach通過附加的形式去調試,而後者利用的就是DebugActiveProcess,廢話少說,看代碼

先進入kernel32!DbgUiConnectToDbg()這個函數,這裏面調用的是ntdll裏的DbgUiConnectToDbg(),我們把ntall.dll載到IDA裏。

在進入ntdll!DbgUiConnectToDbg() 裏的ntdll!ZwCreateDebugObject(),

會發現進0環了,7FFE0300,沒記錯的話是SystemCall,程序由此進0環(這個地方可以參考我以前的文章API函數調用)。

這個函數進0環就是爲了創造對象--DebugObject (從ReactOS上找的),在0時無非就是添結構體,並且返回一個句柄。

typedef struct _DEBUG_OBJECT {
    KEVENT EventsPresent;
    FAST_MUTEX Mutex;
    LIST_ENTRY EventList;
    union
    {
        ULONG Flags;
        struct
        {
            UCHAR DebuggerInactive:1;
            UCHAR KillProcessOnExit:1;
        };
    }
} DEBUG_OBJECT, *PDEBUG_OBJECT;

關鍵是這個這個句柄在3環時放哪了。我們重新回到ntdll裏的DbgUiConnectToDbg()。

發現是存到了TEB+0xF24的地方,此時,DebugObject與調試器建立起了關係。

(做反調試的話,可以遍歷所有TEB+F24h的位置,如果有值,那肯定在被調試(嘴角瘋狂上揚))

OK,差不多了,我們重新回到夢開始的地方DebugActiveProcess,往下看

用到了傳進去的參數PID,在下面轉換成了被調試進程的句柄存到esi裏,緊接着傳入了 kernel32!DbgUiDebugActiveProcess(被調試進程句柄),此時,調試器和被調試建立起了聯繫

之後同樣進入了ntdllUiDebugActiveProcess(被調試進程句柄)。

驚喜來了,創造對象--DebugObject 的句柄被調試進程的句柄都傳入了ntdll!NtDebugActiveProcess

跟進去之後同樣通過SystemCall進NtDebugActiveProcess(0環),

以下是NtDebugActiveProcess的逆向結果:

進入 _DbgkpSetProcessDebugObject

建立上了調試關係,任務結束。

7.GetStartupInfoA

在使用 CreateProcess 創建進程時,需要傳遞 
STARTUPINFO 的結構的指針,
而常常我們並不會一個一個設置其結構的值,
連把其他不用的值清0都會忽略,
而 ollydbg 也這樣做了,
我們可以使用 GetStartupInfo 檢查啓動信息,
如果很多值爲"不可理解"的,那麼就說明自己不是由 explorer 來創建的.(explorer.exe 使用 shell32 中 ShellExecute 的來運行程序, ShellExecute 會清不用的值)

還有一點 ollydbg 會向 STARTUPINFO 中的   dwFlags 設置 STARTF_FORCEOFFFEEDBACK,而 explorer 不會。

這篇文章中要實現的想法,就是這個這道ctf設置的反調試,好巧,網絡一線牽,珍惜這段緣~~)。

8.檢測系統留下的痕跡

進入sub_401580個函數

這種反調試在ctf中很常見。

首先CreateToolhelp32Snapshot照下快照,記錄當前進程,線程信息

利用process32First函數來獲得第一個進程的句柄。

BOOL WINAPI Process32First(
    HANDLE hSnapshot,//_in
    LPPROCESSENTRY32 lppe//_out
);

PROCESSENTRY32結構爲:

typedef struct tagPROCESSENTRY32 {
    DWORD dwSize; // 結構體大小;
    DWORD cntUsage; // 此進程的引用次數;
    DWORD th32ProcessID; // 進程PID;
    DWORD th32DefaultHeapID; // 進程默認堆ID;
    DWORD th32ModuleID; // 進程模塊ID;
    DWORD cntThreads; // 此進程開啓的線程次數;
    DWORD th32ParentProcessID;// 父進程PID;
    LONG pcPriClassBase; // 線程優先權;
    DWORD dwFlags;
    WCHAR szExeFile[MAX_PATH]; // 進程全名;
} PROCESSENTRY32;

可以看出本題比較的是szExeFile進程名稱這一參數,來判斷是否被調試。

除了本題中查找進程信息,還可以查找窗體信息,查找調試器引用的註冊表項

查找調試器引用的註冊表項:

下面是調試器在註冊表中的一個常用位置。
SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系統)
SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系統)
該註冊表項指定當應用程序發生錯誤時,觸發哪一個調試器。默認情況下,它被設置爲Dr.Watson。如果該這冊表的鍵值被修改爲OllyDbg,則惡意代碼就可能確定它正在被調試。

查找窗體信息:

FindWindow函數檢索處理頂級窗口的類名和窗口名稱匹配指定的字符串。

EnumWindows函數枚舉所有屏幕上的頂層窗口,並將窗口句柄傳送給應用程序定義的回調函數

9.時鐘檢測

本題採用的是__rdtsc進行的檢測。

rdtsc指令將時間標籤計數器讀入 EDX:EAX。

Windows系列操作系統的時間間隔10 - 20 毫秒,軟件正常運行時的速度比我們分析代碼時快得多,所以可以比較上下兩句代碼的時間戳,來判斷程序是否被調試。

0x03 總結

因爲一些反調試原理的本質是一樣的,所以把一些反調試放到了一塊說。本文因爲是探究性質,所以會有很多彙編級的逆向,可以注意下在IDA裏的註釋說明。如有不正確的地方,還請路過的大牛斧正,希望我的總結可以幫助到看官。

(CTF的原題稍後添加到附件)

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章