那些年病毒用過的損招——反調試技術

上一篇文章跟大家分享的是反虛擬機技術,其實反虛擬機技術因為眾所周知的原因,實際上的存在可能是一些APT樣本和一些大牛寫的,其實比反虛擬機技術更常見的一種反逆向技術是反調試。n所謂反調試技術,其實就是病毒或者惡意代碼用來判斷是不是處於debug mode,或者說直接讓debugger失效。寫這些東西的作者意識到逆向分析師會經常使用類似於Immunity Debugger或者是OD這種調試工具來觀察惡意文件到底對計算機會造成什麼樣的影響。但是寫惡意代碼的人很明顯是不想讓這些人知道我到底想幹嘛,畢竟我還是要搞破壞的啊,你都知道了我還怎麼搞事情啊。所以他們就設計了這麼一個機制:當惡意程序意識到自己被調試的時候,它就會改變正常的執行路徑或者修改自審程序讓自己崩潰,從而增加調試時間和複雜度。很多反調試技術能夠達到上面說的這種效果,以下我們就來說一下基本的一些套路。

0x01 探測Windows調試器:n病毒會使用多種技術去探測調試器在調試他的痕迹或者是記錄,比如說Windows系統的API或者是去手動檢測調試器人工痕迹的內存結構、查詢調試器遺留在系統中的痕迹等等。調試器探測其實是病毒最常用的反調試技術。n使用Windows API探測調試器應該是比較簡單的,微軟爸爸在Windows系統裡面加入了一些可以供應用程序來判斷自己是不是處於Debug Mode的API以方便開發者調試程序。這些API裡面有些是專門用來探測調試器是不是存在的,而有一些是因為其他需求但是可以拿過來改一下當探測調試器的功能來用,需要注意的是,有些API沒有在官方的文檔中寫出來。n其實防止病毒使用API進行反調試最簡單的方法就是在病毒運行的時候去修改病毒,讓他不能去正常調用探測調試器的API函數,或者是修改返回值,確保它正確執行。但是病毒也不傻,所以我們可以使用rootkit技術去hook這些函數。

0x01.1 基於Windows API做調試器檢測:

其實Windows下面提供了一些可以用來探測調試器的API,我么下面來說幾個可以用來探測調試器的API:

IsDebuggerPresent:這個API的作用是查詢進程環境塊(Process Environment Block, PEB)中的IsDebugged標識。如果進程沒有運行在調試器環境中,函數返回值為0;如果調試附加了進程的話,函數返回的是一個非0值。

CheckRemoteDebuggerPresent:看名字估計會以為這個是檢測遠程調試的,其實這東西跟上面那個基本上差不多,也是用來檢測本機進程是不是運行在調試器中,但不會檢測遠程主機的調試器。這貨也檢查PEB裡面的IsDebugged屬性,這個API不僅可以探測進程自己本身是不是被調試,也可以探測本機系統的其他進程是不是被調試。函數本身將一個進程的句柄作為參數,檢查句柄對應的是不是被調試attach了。同時,這個API也可以通過傳遞自身句柄來檢查自己是不是被調試了。

NtQueryInformationProcess:這個是ntdll.dll裡面的一個API,用來提取給定進程的信息。第一個參數是進程句柄,第二個參數是需要提取進程信息的類型。舉個例子,比如把這個參數置為ProcessDebugPort(0x7),它將會告訴你這個句柄標示的進程是不是正在被調試,如果調試的話,就會返回埠,如果不是,則會返回0。

OutputDebugString:這個API的作用是在調試器中顯示一個字元串,其實說到這你大概也就明白了,我們直接拿一段代碼作為演示:

DWORD errorValue = 360;nSerLastError(errorValue);nOutputDebugString(Fxck Debugger);nIf(GetLastError() == errorValue)n{n ExitProcess();n}nelsen{n Run();n}n

使用SerLastError函數,將當前的錯誤碼設置為一個任意值,如果進程沒有被調試器附加,那麼就調用OutputDebugString函數,因為OutputDebugString函數失敗,重新設置了錯誤碼,因此GetLastError獲取的錯誤碼不應該是我們設置的任意值。單進程如果被附加並調用了OutputDebugString函數,那麼調用OutputDebugString函數應該會成功,這時候獲取的錯誤代碼應該沒有被改變。

0x01.2 手動檢測數據結構:n雖然說上面使用微軟爸爸給的API很簡單,但是手動檢查數據結構才是用的最多的方法。因為很多時候通過Windows API實現反調試的技術是沒有任何效果的,比如說上面提到的函數被Rootkit給hook掉了,並且設置了return error。n手動檢測中,PEB結構中的一些標誌暴露了調試存在的信息,這裡我們來說下調試器會存在的標誌

(1)檢測BeingDebugged屬性:Windows系統維護著每個正在運行進程的的PEB,一個進程的PEB數據結構可以用以下的代碼來表示:

typedef struct _PEBn{n BYTE Reserved1[2];n BYTE BeingDebugged;n BYTE Reserved2[1];n BYTE Reserved3[2];n PPEB_LDR_DATA Ldr;n PRTL_USER_PROCESS_PARAMETERS ProcessParameters;n BYTE Reserved4[104];n BYTE Reserved5[52];n PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;n BYTE Reserved6[128];n PVOID Reserved7[1];n ULONG SessionId;n} PEB, *PPEB;n

這裡面的信息包括了進程的環境數據,也就是環境變數、載入的模塊列表、內存地址和調試器狀態。下面我們來看一下調用的方法:

①mov方法

mov eax, dword ptr fs:[30h]nmov ebx, byte ptr [eax+2]ntest ebx, ebxnjz NoDebuggerDetectedn

②push/pop方法

push dword ptr fs:[30h]npop edxncmp byte ptr [edx+2], 1nje DebuggerDetectedn

進程運行的時候,位置fs:[30h]指向PEB的基址,要實現反調試技術,病毒會通過這個位置檢查BeingDebugged標誌,這個標誌是為了標示被調試的進程。n在①里,PEB基址被載入了EAX寄存器中,截下來PEB基址偏移量2處的值載入到了EBX寄存器,它對應PEB結構中的偏移量其實就是BeingDebugged的標誌的內存未知,接著檢查EBX是不是為0,如果是那就說明這進程不是中出的叛徒(沒有被調試器所附加),然後就可以放心大膽的去幹壞事了。②裡面使用的是push/pop指令組合,意思是將PEB的基址載入EDX寄存器,然後把偏移量為2的BeingDebugged標誌和1進行比較。

當然還有別的利用方法,這裡就不說了。要避免這種方法有兩個小的操作:在執行跳轉指令前先把BeingDebugged手動改成0或者是強制跳轉。其實OD裡面的Hide Debugger可以解決BeingDebugged的探測。

(2)檢測ProcessHeap的屬性:

在上面的那個PEB數據結構裡面,有一個BYTE類型的Reserved4數組,這個數組裡面有一個未公開的位置叫做ProcessHeap,這個作用是載入器為進程分配的第一個堆的位置,ProcessHeap在PEB結構的0x18處,第一個堆頭都有一個屬性欄位,目的是為了告訴內核這個堆有沒有在調試器中創建,而這裡的屬性叫做ForceFlags和Flags。

已經被淘汰的XP中,ForceFlag屬性位於堆頭部偏移量0x10處,Win7裡面是在0x44處(32位),病毒檢查XP系統中偏移量0x0c處或者是win7裡面0x40處的Flags屬性,這個屬性總是跟ForceFlags屬性大致相同,一般情況下是和2進行或運算。表示一下就是:

mov eax, large fs:30hnmov eax dword ptr [eax+18h], 0ncmp dword ptr ds:[eax+10h]//比較值njne DebuggerDetectedn

對付這種套路的方法,其實就是手動修改ProcessHeap的值,或者是使用OD裡面的隱藏調試插件,如果說你用的是WinDBG的話,可以禁用調試堆棧來啟動進程。比如說:

windbg-hd calc.exen

(3)檢查NTGlobalFlag:

由於調試器啟動進程和正常啟動進程有那麼點區別,所以創建內存堆的方法也不一樣。Windows系統下面用PEB結構偏移0x68地方的一個未公開位置來決定怎麼去創建堆結構,如果這個位置的值為0x70,我們就知道集成是被調試器啟動的。調試器創建堆的時候,下面一個標誌位的組合就會被設置:

(FLG_HEAP_ENABLE_TAIL_CHECK|FLG_HEAP_ENABLE_FREE_CHECK|FLG_HEAP_VALIDATE_PARAMETERS)n

檢測的方法也不難:

mov eax, large fs:30hncmp dword ptr ds:[eax+68h], 70h//比較0x68偏移是不是0x70njz DebuggerDetectedn

解決方法同上,禁用debug heap選項來啟動調試。

(3)痕迹檢測:

調試工具畢竟是工具,工具的話修改註冊表是個很正常的情況,比如說在HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersion中,有些調試器就會留下相關的信息。當然除了這個之外,病毒也會去查找系統文件和目錄,比如說Ollydbg.exe這種東西,其實我們還可以用更猥瑣的方法,WindowsAPI中提供了一個FindWindow函數用來查看打開的窗口,我們可以這麼干:

if(FindWindow(OLLYDBG, 0) == NULL)n{n return 1//發現調試器n}nelsen{n return 0//沒發現調試器n}n

如果沒有發現調試器的話,就返回1,有的話就返回0,程序退出。

0x02 調試器行為識別:

調試器其實也是有方法可以識別的,病毒分析師分析病毒的時候,不是要設置斷點,就是要單步執行進程。但是這種時候,調試器會修改進程當中的代碼,所以下面說的就是幾個調試器的行為。

(1)INT掃描:

先來說一下調試器設置斷點的時候會幹什麼,調試器設置斷點的機制是用軟中斷INT 3去臨時替換運行程序中的一條指令,然後當程序執行到這條指令的時候,調試器會調用異常處理機制來完成終端。INT 3的機器碼是0xCC,你只要設置了斷點,調試器就會插入一個0xCC去修改代碼。除了INT 3之外,INT X指令可以設置任意中斷(X可以是一個寄存器),INT X使用兩個操作碼:0xCD value,但是調試器比較笨,不經常使用這種雙位元組機器碼。所以我們要是實現反調試的話,可以先從0xCC下手。首先先來掃描一下斷點:

call $+5npop edinsub edi, 5nmov ecx, 400hnmov eax, 0CChnrepne scasbnjz DebuggerDetectedn

這段代碼首先先執行一個函數調用,然後用pop指令把EIP寄存器的值放入EDI,之後將EDI設置為代碼的起始位置。緊接著,掃描這段代碼下的0xCC位元組,如果發現,就證明有調試器的存在。對抗這種反調試的方法,我們可以用硬斷點去代替軟斷點來中斷程序。

(2)執行代碼校驗與檢查:

有些比較牛x的病毒可以計算代碼段的笑顏然後實現和掃描中斷一樣的目的,這個就比掃描0xCC高大上了很多,但是這種檢查僅執行病毒中機器碼的CRC和MD5校驗和檢查。雖然說這個見的確實比較少,我也是道聽途說才知道了原來還有這種操作,但是不代表不存在。

(3)時鐘監測:

調試的時候,勢必會影響程序執行的速度,這個是肯定的。所以時鐘監測也是用的比較多的一種方法。我們有兩種方案來實現利用時鐘監測進行調試器檢測的方法:

方案A:記錄一段操作前後的時間戳,然後比較兩個時間戳,如果存在滯後,我們可以認為我們被調試了。

方案B:記錄一個觸發異常前後的時間戳,如果不調試進程的話,這個異常處理應該用不了多長時間。但是調試器處理的話需要人為干預,所以會很慢,就算是單身20年的高級操作選手,也不會跟計算機一樣快。

好了既然我們有了方案,那麼就來搞一下吧。

①使用rdtsc去檢測:rdtsc指令的操作碼是0x0F31,這個指令返回至系統重啟以來的時鐘數並把它作為一個64位的值存放到EDX:EAX中。病毒可以運行兩次rdstc指令去比較差值看看自己有沒有被調試了。上代碼(這次的代碼真多啊):

rdtscnxor ecx, ecxnadd ecx, eaxnrdtscnsub eax, ecxncmp eax, 0xFFFnjb NoDebuggernrdstcnpush eaxnretn

病毒通過調用兩次rdstc看看差值是不是大於0xFF,如果延遲很多,跳轉就不會被執行,如果跳轉沒執行,那麼再次調用rdstc並把結果放入eax寄存器的堆棧,這樣會返回隨機位置的值。

②使用QueryPerformanceCounter和GetTickCount:這兩個API是Windows自帶的,QPC用來儲存處理器活躍的時鐘數,調用QueryPerformanceCount兩次花費時間如果太長的話,可以認定為被調試了;GetTickCount函數返回最近系統重啟時間和當前時間相差的毫秒數,但是因為時鐘計數器大小的原因,49.7天就會被重置一次。貼一段使用的code:

a = GetTickCount();nRunVirus();//運行病毒nb = GetTickCount();ndelta = b-a;nif(delta > 0x1A)n return 1;//檢測到Debuggernelsen return 0;//沒有檢測到Debuggern

其實這種利用時鐘檢測調試器有個前提:當且僅當在獲取時間增量的兩次調用之間,我們單步執行了程序或者是設置了斷點。避免這種檢測的方法就是在始終檢測之後再設置斷點,緊接著再去但不執行,如果說沒得選,那麼就面向ret做跳轉吧。

下一篇我們來說說中斷調試技術

推薦閱讀:

要脫掉VMP3.1的殼子需要掌握哪些知識?
閑扯ARM指令集一
自動化二進位文件分析框架:angr
那些年病毒用過的損招——攻擊調試器
安卓逆向入門(一)

TAG:逆向工程 | 计算机病毒 | 信息安全 |