软件反调试技术解析

10 篇文章 0 订阅
5 篇文章 0 订阅

目录

一、反调试技术

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值