軟件反調試技術解析

目錄

一、反調試技術

1.斷點 2.計算校驗和

3.檢測調試器 4.探測單步執行

5.在運行時中檢測速度衰減 6.指令預取

7.自修改代碼 8.覆蓋調試程序信息

9.解除調試器線程 10.解密

二、逆轉錄病毒

三、混合技術

四、linux反調試技術簡單示例

1. int3指令 2. 文件描述符 3. 利用getppid 4. 利用環境變量 5. 利用ptrace

五、小結

本文中,我們將向讀者介紹惡意軟件用以阻礙對其進行逆向工程的各種反調試技術,以幫助讀者很好的理解這些技術,從而能夠更有效地對惡意軟件進行動態檢測和分析。

一、反調試技術

反調試技術是一種常見的反檢測技術,因爲惡意軟件總是企圖監視自己的代碼以檢測是否自己正在被調試。爲做到這一點,惡意軟件可以檢查自己代碼是否被設置了斷點,或者直接通過系統調用來檢測調試器。

1.斷點

爲了檢測其代碼是否被設置斷點,惡意軟件可以查找指令操作碼0xcc(調試器會使用該指令在斷點處取得惡意軟件的控制權),它會引起一個SIGTRAP。如果惡意軟件代碼本身建立了一個單獨的處理程序的話,惡意軟件也可以設置僞斷點。用這種方法惡意軟件可以在被設置斷點的情況下繼續執行其指令。

惡意軟件也可以設法覆蓋斷點,例如有的病毒採用了反向解密循環來覆蓋病毒中的斷點。相反,還有的病毒則使用漢明碼自我糾正自身的代碼。漢明碼使得程序可以檢測並修改錯誤,但是在這裏卻使病毒能夠檢測並清除在它的代碼中的斷點。

2.計算校驗和

惡意軟件也可以計算自身的校驗和,如果校驗和發生變化,那麼病毒會假定它正在被調試,並且其代碼內部已被放置斷點。VAMPiRE是一款抗反調試工具,可用來逃避斷點的檢測。VaMPiRE通過在內存中維護一張斷點表來達到目的,該表記錄已被設置的所有斷點。該程序由一個頁故障處理程序(PFH),一個通用保護故障處理程序(GPFH),一個單步處理程序和一個框架API組成。當一個斷點被觸發的時候,控制權要麼傳給PFH(處理設置在代碼、數據或者內存映射I/O中的斷點),要麼傳給GPFH(處理遺留的I/O斷點)。單步處理程序用於存放斷點,使斷點可以多次使用。

3.檢測調試器

在Linux系統上檢測調試器有一個簡單的方法,只要調用Ptrace即可,因爲對於一個特定的進程而言無法連續地調用Ptrace兩次以上。在Windows中,如果程序目前處於被調試狀態的話,系統調用isDebuggerPresent將返回1,否則返回0。這個系統調用簡單檢查一個標誌位,當調試器正在運行時該標誌位被置1。直接通過進程環境塊的第二個字節就可以完成這項檢查,以下代碼爲大家展示的就是這種技術:

mov eax, fs:[30h]
move eax, byte [eax+2]
test eax, eax    
jne @DdebuggerDetected

在上面的代碼中,eax被設置爲PEB(進程環境塊),然後訪問PEB的第二個字節,並將該字節的內容移入eax。通過查看eax是否爲零,即可完成這項檢測。如果爲零,則不存在調試器;否則,說明存在一個調試器。

如果某個進程爲提前運行的調試器所創建的,那麼系統就會給ntdll.dll中的堆操作例程設置某些標誌,這些標誌分別是FLG_HEAP_ENABLE_TAIL_CHECK、FLG_HEAP_ENABLE_FREE_CHECK和FLG_HEAP_VALIDATE_PARAMETERS。我們可以通過下列代碼來檢查這些標誌:

mov eax, fs:[30h]
mov eax, [eax+68h]
and eax, 0x70
test eax, eax
jne @DebuggerDetected

在上面的代碼中,我們還是訪問PEB,然後通過將PEB的地址加上偏移量68h到達堆操作例程所使用的這些標誌的起始位置,通過檢查這些標誌就能知道是否存在調試器。

檢查堆頭部內諸如ForceFlags之類的標誌也能檢測是否有調試器在運行,如下所示:

mov eax, fs:[30h]
mov eax, [eax+18h] ;process heap
mov eax, [eax+10h] ;heap flags
test eax, eax
jne @DebuggerDetected

上面的代碼向我們展示瞭如何通過PEB的偏移量來訪問進程的堆及堆標誌,通過檢查這些內容,我們就能知道Force標誌是否已經被當前運行的調試器提前設置爲1了。

另一種檢測調試器的方法是,使用NtQueryInformationProcess這個系統調用。我們可以將ProcessInformationClass設爲7來調用該函數,這樣會引用ProcessDebugPort,如果該進程正在被調試的話,該函數將返回-1。示例代碼如下所示。

push 0
push 4
push offset isdebugged
push 7 ;ProcessDebugPort
push -1
call NtQueryInformationProcess
test eax, eax
jne @ExitError
cmp isdebugged, 0
jne @DebuggerDetected

在本例中,首先把NtQueryInformationProcess的參數壓入堆棧。這些參數介紹如下:第一個是句柄(在本例中是0),第二個是進程信息的長度(在本例中爲4字節),接下來是進程信息類別(在本例中是7,表示ProcessDebugPort),下一個是一個變量,用於返回是否存在調試器的信息。如果該值爲非零值,那麼說明該進程正運行在一個調試器下;否則,說明一切正常。最後一個參數是返回長度。使用這些參數調用NtQueryInformationProcess後的返回值位於isdebugged中。隨後測試該返回值是否爲0即可。

另外,還有其他一些檢測調試器的方法,如檢查設備列表是否含有調試器的名稱,檢查是否存在用於調試器的註冊表鍵,以及通過掃描內存以檢查其中是否含有調試器的代碼等。

另一種非常類似於EPO的方法是,通知PE加載器通過PE頭部中的線程局部存儲器(TLS)表項來引用程序的入口點。這會導致首先執行TLS中的代碼,而不是先去讀取程序的入口點。因此,TLS在程序啓動就可以完成反調試所需檢測。從TLS啓動時,使得病毒得以能夠在調試器啓動之前就開始運行,因爲一些調試器是在程序的主入口點處切入的。

4.探測單步執行

惡意軟件還能夠通過檢查單步執行來檢測調試器。要想檢測單步執行的話,我們可以把一個值放進堆棧指針,然後看看這個值是否還在那裏。如果該值在那裏,這意味着,代碼正在被單步執行。當調試器單步執行一個進程時,當其取得控制時需要把某些指令壓入棧,並在執行下一個指令之前將其出棧。所以,如果該值仍然在那裏,就意味着其它正在運行的進程已經在使用堆棧。下面的示例代碼展示了惡意軟件是如何通過堆棧狀態來檢測單步執行的:

Mov bp,sp;選擇堆棧指針
Push ax ;將ax壓入堆棧
Pop ax ;從堆棧中選擇該值
Cmp word ptr [bp -2],ax ;跟堆棧中的值進行比較
Jne debug ;如果不同,說明發現了調試器。  

如上面的註釋所述,一個值被壓入堆棧然後又被彈出。如果存在調試器,那麼堆棧指針–2位置上的值就會跟剛纔彈出堆棧的值有所不同,這時就可以採取適當的行動。

5.在運行時中檢測速度衰減

通過觀察程序在運行時是否減速,惡意代碼也可以檢測出調試器。如果程序在運行時速度顯著放緩,那就很可能意味着代碼正在單步執行。因此如果兩次調用的時間戳相差甚遠,那麼惡意軟件就需要採取相應的行動了。Linux跟蹤工具包LTTng/LTTV通過觀察減速問題來跟蹤病毒。當LTTng/LTTV追蹤程序時,它不需要在程序運行時添加斷點或者從事任何分析。此外,它還是用了一種無鎖的重入機制,這意味着它不會鎖定任何Linux內核代碼,即使這些內核代碼是被跟蹤的程序需要使用的部分也是如此,所以它不會導致被跟蹤的程序的減速和等待。

6.指令預取

如果惡意代碼篡改了指令序列中的下一條指令並且該新指令被執行了的話,那麼說明一個調試器正在運行。這是指令預取所致:如果該新指令被預取,就意味着進程的執行過程中有其他程序的切入。否則,被預取和執行的應該是原來的指令。

7.自修改代碼

惡意軟件也可以讓其他代碼自行修改(自行修改其他代碼),這樣的一個例子是HDSpoof。這個惡意軟件首先啓動了一些異常處理例程,然後在運行過程中將其消除。這樣一來,如果發生任何故障的話,運行中的進程會拋出一個異常,這時病毒將終止運行。此外,它在運行期間有時還會通過清除或者添加異常處理例程來篡改異常處理例程。在下面是HDSpoof清除全部異常處理例程(默認異常處理例程除外)的代碼。

exception handlers before:

0x77f79bb8 ntdll.dll:executehandler2@20 + 0x003a
0x0041adc9 hdspoof.exe+0x0001adc9
0x77e94809 __except_handler3

exception handlers after:

0x77e94809 __except_handler3

0x41b770: 8b44240c       mov      eax,dword ptr [esp+0xc]
0x41b774: 33c9           xor      ecx,ecx              
0x41b776: 334804         xor      ecx,dword ptr [eax+0x4]
0x41b779: 334808         xor      ecx,dword ptr [eax+0x8]
0x41b77c: 33480c         xor      ecx,dword ptr [eax+0xc]
0x41b77f: 334810         xor      ecx,dword ptr [eax+0x10]
0x41b782: 8b642408       mov      esp,dword ptr [esp+0x8]
0x41b786: 648f0500000000 pop      dword ptr fs:[0x0]   

下面是HDSpoof創建一個新的異常處理程序的代碼。

0x41f52b: add      dword ptr [esp],0x9ca
0x41f532: push     dword ptr [dword ptr fs:[0x0]
0x41f539: mov      dword ptr fs:[0x0],esp

8.覆蓋調試程序信息

一些惡意軟件使用各種技術來覆蓋調試信息,這會導致調試器或者病毒本身的功能失常。通過鉤住中斷INT 1和INT 3(INT 3是調試器使用的操作碼0xCC),惡意軟件還可能致使調試器丟失其上下文。這對正常運行中的病毒來說毫無妨礙。另一種選擇是鉤住各種中斷,並調用另外的中斷來間接運行病毒代碼。

下面是Tequila 病毒用來鉤住INT 1的代碼:

new_interrupt_one:

   push bp
   mov bp,sp
   cs cmp b[0a],1      ;masm mod. needed
   je 0506             ;masm mod. needed
   cmp w[bp+4],09b4
   ja 050b             ;masm mod. needed
   push ax
   push es
   les ax,[bp+2]
   cs mov w[09a0],ax   ;masm mod. needed
   cs mov w[09a2],es   ;masm mod. needed
   cs mov b[0a],1
   pop es
   pop ax
   and w[bp+6],0feff
   pop bp
   iret

一般情況下,當沒有安裝調試器的時候,鉤子例程被設置爲IRET。V2Px使用鉤子來解密帶有INT 1和INT 3的病毒體。在代碼運行期間,會不斷地用到INT 1和INT 3向量,有關計算是通過中斷向量表來完成的。

一些病毒還會清空調試寄存器(DRn的內容。有兩種方法達此目的,一是使用系統調用NtGetContextThread和NtSetContextThread。而是引起一個異常,修改線程上下文,然後用新的上下文恢復正常運行,如下所示:

push offset handler
push dword ptr fs:[0]
mov fs:[0],esp
xor eax, eax
div eax ;generate exception
pop fs:[0]
add esp, 4
;continue execution
;...
handler:
mov ecx, [esp+0Ch] ;skip div
add dword ptr [ecx+0B8h], 2 ;skip div
mov dword ptr [ecx+04h], 0 ;clean dr0
mov dword ptr [ecx+08h], 0 ;clean dr1
mov dword ptr [ecx+0Ch], 0 ;clean dr2
mov dword ptr [ecx+10h], 0 ;clean dr3
mov dword ptr [ecx+14h], 0 ;clean dr6
mov dword ptr [ecx+18h], 0 ;clean dr7
xor eax, eax
ret

上面的第一行代碼將處理程序的偏移量壓入堆棧,以確保當異常被拋出時它自己的處理程序能取得控制權。之後進行相應設置,包括用自己異或自己的方式將eax設爲0,以將控制權傳送給該處理程序。div eax 指令會引起異常,因爲eax爲0,所以AX將被除以零。該處理程序然後跳過除法指令,清空dr0-dr7,同樣也把eax置0,表示異常將被處理,然後恢復運行。

9.解除調試器線程

我們可以通過系統調用NtSetInformationThread從調試器拆卸線程。爲此,將ThreadInformationClass設爲0x11(ThreadHideFromDebugger)來調用NtSetInformationThread,如果存在調試器的話,這會將程序的線程從調試器拆下來。以下代碼就是一個例子:

push 0
push 0
push 11h ;ThreadHideFromDebugger
push -2
call NtSetInformationThread

在本例中,首先將NtSetInformationThread的參數壓入堆棧,然後調用該函數來把程序的線程從調試器中去掉。這是因爲這裏的0用於線程的信息長度和線程信息,傳遞的-2用於線程句柄,傳遞的11h用於線程信息類別,這裏的值表示ThreadHideFromDebugger。

10.解密

解密可以通過各種防止調試的方式來進行。有的解密依賴於特定的執行路徑。如果這個執行路徑沒被沿用,比如由於在程序中的某個地方啓動了一個調試器,那麼解密算法使用的值就會出錯,因此程序就無法正確進行自身的解密。HDSpoof使用的就是這種技術。

一些病毒使用堆棧來解密它們的代碼,如果在這種病毒上使用調試器,就會引起解密失敗,因爲在調試的時候堆棧爲INT 1所用。使用這種技術的一個例子是W95/SK病毒,它在堆棧中解密和構建其代碼;另一個例子是Cascade病毒,它將堆棧指針寄存器作爲一個解密密鑰使用。代碼如下所示:

lea   si, Start   ; position to decrypt
mov   sp, 0682  ; length of encrypted body

Decrypt:

xor   [si], si    ; decryption key/counter 1
xor   [si], sp  ; decryption key/counter 2
inc   si    ; increment one counter
dec   sp    ; decrement the other
jnz   Decrypt   ; loop until all bytes are decrypted
Start:            ; Virus body

對於Cascade病毒如何使用堆棧指針來解密病毒體,上面代碼中的註釋已經做了很好的說明。相反,Cryptor病毒將其密鑰存儲在鍵盤緩衝區中,這些密鑰會被調試器破壞。Tequila使用解密器的代碼作爲解密鑰,因此如果解密器被調試器修改後,那麼該病毒就無法解密了。下面是Tequila用於解密的代碼:

perform_encryption_decryption:

   mov bx,0
   mov si,0960
   mov cx,0960
  mov dl,b[si]
   xor b[bx],dl
   inc si
   inc bx
   cmp si,09a0
   jb 0a61             ;masm mod. needed
   mov si,0960
   loop 0a52           ;masm mod. needed
   ret

the_file_decrypting_routine:

   push cs
   pop ds
   mov bx,4
   mov si,0964
   mov cx,0960
   mov dl,b[si]
   add b[bx],dl
   inc si
   inc bx
   cmp si,09a4
   jb 0a7e             ;masm mod. needed
   mov si,0964
   loop 0a6f           ;masm mod. needed
   jmp 0390            ;masm mod. needed


人們正在研究可用於將來的新型反調試技術,其中一個項目的課題是關於多處器計算機的,因爲當進行調試時,多處理器中的一個會處於閒置狀態。這種新技術使用並行處理技術來解密代碼。

二、逆轉錄病毒

逆轉錄病毒會設法禁用反病毒軟件,比如可以通過攜帶一列進程名,並殺死正在運行的與表中同名的那些進程。許多逆轉錄病毒還把進程從啓動列表中踢出去,這樣該進程就無法在系統引導期間啓動了。這種類型的惡意軟件還會設法擠佔反病毒軟件的CPU時間,或者阻止反病毒軟件連接到反病毒軟件公司的服務器以使其無法更新病毒庫。

三、混合技術

W32.Gobi病毒是一個多態逆轉錄病毒,它結合了EPO和其他一些反調試技術。該病毒還會在TCP端口666上打開一個後門。

Simile(又名Metaphor)是一個非常有名的複合型病毒,它含有大約14,000行彙編代碼。這個病毒通過尋找API調用ExitProcess()來使用EPO,它還是一個多態病毒,因爲它使用多態解密技術。它的90%代碼都是用於多態解密,該病毒的主體和多態解密器在每次感染新文件時,都會放到一個半隨機的地方。Simile的第一個有效載荷只在3月、6月、9月或12月份纔會激活。在這些月份的17日變體A和B顯示它們的消息。變體C在這些月份的第18日顯示它的消息。變體A和B中的第二個有效載荷只有在五月14日激活,而變體C中的第二個有效載荷只在7月14日激活。

Ganda是一個使用EPO的逆轉錄病毒。它檢查啓動進程列表,並用一個return指令替換每個啓動進程的第一個指令。這會使所有防病毒程序變得毫無用處。

四、linux反調試技術簡單示例

1. int3指令

Intel Software Developer’s Manual Volume 2A中提到:

The INT 3 instruction generates a special one byte opcode (CC) that is intended for
calling the debug exception handler. (This one byte form is valuable because it can be
used to replace the first byte of any instruction with a breakpoint, including other one
byte instructions, without over-writing other code).

int3是一個特殊的中斷指令(從名字上也看得出來),專門用來給調試器使用。這時,我們應該很容易想到,要反調試,只要插入int3來迷惑調試器即可。不過,這會不會影響正常的程序?會!因爲int3會在用戶空間產生SIGTRAP。沒關係,我們只要忽略這個信號就可以了。

  1. #include <stdio.h>
  2. #include <signal.h>
  3.  
  4. void handler(int signo)
  5. {}
  6.  
  7. int main(void)
  8. {
  9.     signal(SIGTRAPhandler);
  10.     __asm__("nop\n\t"
  11.         "int3\n\t");
  12.     printf("Hello from main!\n");
  13.     return 0;
  14. }

2. 文件描述符

這是一個很巧妙的辦法,不過它只對gdb之類的調試器有效。方法如下:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4.  
  5. int main(void)
  6. {
  7.     if(close(3) == -1) {
  8.         printf("OK\n");
  9.     } else {
  10.         printf("traced!\n");
  11.         exit(-1);
  12.     }
  13.     return 0;
  14. }

gdb要調試這個程序時會打開一個額外的文件描述符來讀這個可執行文件,而這個程序正是利用了這個“弱點”。當然,你應該能猜到,這個技巧對strace是無效的。

3. 利用getppid

和上面一個手法類似,不過這個更高明,它利用getppid來進行探測。我們知道,在Linux上要跟蹤一個程序,必須是它的父進程才能做到,因此,如果一個程序的父進程不是意料之中的bash等(而是gdb,strace之類的),那就說明它被跟蹤了。程序代碼如下:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6. #include <sys/stat.h>
  7. #include <fcntl.h>
  8.  
  9. int get_name_by_pid(pid_t pidcharname)
  10. {
  11.     int fd;
  12.     char buf[1024] = {0};
  13.     snprintf(buf1024"/proc/%d/cmdline"pid);
  14.     if ((fd = open(bufO_RDONLY)) == -1)
  15.         return -1;
  16.     read(fdbuf1024);
  17.     strncpy(namebuf1023);
  18.     return 0;
  19. }
  20.  
  21. int main(void)
  22. {
  23.     char name[1024];
  24.     pid_t ppid = getppid();
  25.     printf("getppid: %d\n"ppid);
  26.  
  27.         if (get_name_by_pid(ppidname))
  28.         return -1;
  29.     if (strcmp(name"bash") == 0 ||
  30.         strcmp(name"init") == 0)
  31.             printf("OK!\n");
  32.     else if (strcmp(name"gdb") == 0 ||
  33.         strcmp(name"strace") == 0 ||
  34.         strcmp(name"ltrace") == 0)
  35.         printf("Traced!\n");
  36.     else
  37.         printf("Unknown! Maybe traced!\n");
  38.  
  39.     return 0;
  40. }

同樣的手法,一個更簡單的方式是利用session id。我們知道,不論被跟蹤與否,session id是不變的,而ppid會變!下面的程序就利用了這一點。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4.  
  5. int main(void)
  6. {
  7.     printf("getsid: %d\n"getsid(getpid()));
  8.     printf("getppid: %d\n"getppid());
  9.  
  10.         if (getsid(getpid()) != getppid()) {
  11.         printf("traced!\n");
  12.         exit(EXIT_FAILURE);
  13.     }
  14.         printf("OK\n");
  15.  
  16.     return 0;
  17. }

4. 利用環境變量

bash有一個環境變量叫$_,它保存的是上一個執行的命令的最後一個參數。如果在被跟蹤的狀態下,這個變量的值是會發生變化的(爲什麼?)。下面列出了幾種情況:

                argv[0]                    getenv("_")
shell           ./test                     ./test
strace          ./test                     /usr/bin/strace
ltrace          ./test                     /usr/bin/ltrace
gdb              /home/user/test           (NULL)

所以我們也可以據此來判斷。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4.  
  5. int main( int argcchar *argv[])
  6. {
  7.     printf("getenv(_): %s\n"getenv("_"));
  8.     printf("argv[0]: %s\n"argv[0]);
  9.  
  10.     if(strcmp(argv[0](char *)getenv("_"))) {
  11.         printf("traced!\n");
  12.         exit(-1);
  13.     }
  14.  
  15.     printf("OK\n");
  16.         return 0;
  17. }

5. 利用ptrace

很簡單,如果被跟蹤了還再調用ptrace(PTRACE_TRACEME…)自然會不成功。

  1. #include <stdio.h>
  2. #include <sys/ptrace.h>
  3.  
  4. int main(void)
  5. {
  6.      if ( ptrace(PTRACE_TRACEME010) < 0 ) {
  7.         printf("traced!\n");
  8.         return 1;
  9.     }
  10.     printf("OK\n");
  11.     return 0;
  12. }

四、小結

本文中,我們介紹了惡意軟件用以阻礙對其進行逆向工程的若干反調試技術,同時介紹了逆轉錄病毒和各種反檢測技術的組合。我們應該很好的理解這些技術,只有這樣才能夠更有效地對惡意軟件進行動態檢測和分析。


參考與轉載:http://netsecurity.51cto.com/art/200810/92668_all.htm

    http://wangcong.org/blog/archives/310

發佈了17 篇原創文章 · 獲贊 5 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章