反调试技术,恶意代码会用它识别自身是否被调试,或者让调试器失效,给反病毒工程师们制造麻烦,拉长提取特征码的时间线,本章将具体总结常见的反调试基础的实现原理以及如何过掉这些反调试手段,从而让我们能够继续分析恶意代码。
反调试技术的实现方式有很多,最简单的一种实现方式莫过于直接调用Windows系统提供给我们的API函数,这些API函数中有些专门用来检测调试器的,有些则是可被改造为用于探测调试器是否存在的工具,多数情况下,调用系统API函数实现反调试是不明智的,原因很简单,目标主机通常会安装主动防御系统,而作为主动防御产品默认会加载RootKit驱动挂钩这些敏感函数的使用,如果被非法调用则会提示错误信息,病毒作者通常会使用汇编自行实现这些类似于系统提供给我们的反调试函数,并不会使用系统的API,这样依附于API的主动防御的系统将会失效。
1.加载调试符号链接文件并放入d:/symbols
目录下.
0:000> .sympath srv*d:\symbols*http://msdl.microsoft.com/download/symbols 0:000> .reload Reloading current modules
2.位于fs:[0x30]
的位置就是PEB结构的指针,接着我们分析如何得到的该指针,并通过通配符找到TEB结构的名称.
0:000> dt ntdll!*teb* ntdll!_TEB ntdll!_GDI_TEB_BATCH ntdll!_TEB_ACTIVE_FRAME ntdll!_TEB_ACTIVE_FRAME_CONTEXT ntdll!_TEB_ACTIVE_FRAME_CONTEXT
3.接着可通过dt命令,查询下ntdll!_TEB
结构,如下可看到0x30
处ProcessEnvironmentBlock
存放的正是PEB结构.
0:000> dt -rv ntdll!_TEB struct _TEB, 66 elements, 0xfb8 bytes +0x000 NtTib : struct _NT_TIB, 8 elements, 0x1c bytes # NT_TIB结构 +0x018 Self : Ptr32 to struct _NT_TIB, 8 elements, 0x1c bytes # NT_TIB结构 +0x020 ClientId : struct _CLIENT_ID, 2 elements, 0x8 bytes # 保存进程与线程ID +0x02c ThreadLocalStoragePointer : Ptr32 to Void +0x030 ProcessEnvironmentBlock : Ptr32 to struct _PEB, 65 elements, 0x210 bytes # PEB结构
偏移地址0x18
是_NT_TIB
结构,也就是指向自身偏移0x0
的位置.
0:000> r $teb $teb=7ffdf000 0:000> dd $teb+0x18 7ffdf018 7ffdf000 00000000 00001320 00000c10 7ffdf028 00000000 00000000 7ffd9000 00000000
而!teb
地址加0x30
正是PEB
的位置,可以使用如下命令验证.
0:000> dd $teb+0x30 7ffdf030 7ffd9000 00000000 00000000 00000000 7ffdf040 00000000 00000000 00000000 00000000 0:000> !teb TEB at 7ffdf000 ExceptionList: 0012fd0c StackBase: 00130000 StackLimit: 0012e000 SubSystemTib: 00000000 FiberData: 00001e00 ArbitraryUserPointer: 00000000 Self: 7ffdf000 EnvironmentPointer: 00000000 ClientId: 00001320 . 00000c10 RpcHandle: 00000000 Tls Storage: 00000000 PEB Address: 7ffd9000 # 此处teb地址
上方的查询结果可得知偏移位置fs:[0x18]
正是TEB的基址TEB:7ffdf000
0:000> dd fs:[0x18] 003b:00000018 7ffdf000 00000000 000010f4 00000f6c 003b:00000028 00000000 00000000 7ffda000 00000000 0:000> dt _teb 0x7ffdf000 ntdll!_TEB +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : (null) +0x020 ClientId : _CLIENT_ID # 这里保存进程与线程信息 0:000> dt _CLIENT_ID 0x7ffdf000 # 查看进程详细结构 ntdll!_CLIENT_ID +0x000 UniqueProcess : 0x0012fd0c Void # 获取进程PID +0x004 UniqueThread : 0x00130000 Void # 获取线程PID
上方TEB首地址我们知道是fs:[0x18]
,接着我们通过以下公式计算得出本进程的进程ID.
在Windows系统中如果想要获取到PID进程号,可以使用NtCurrentTeb()
这个系统API来实现,但这里我们手动实现该API的获取过程.
获取进程PID:
#include "stdafx.h" #include <Windows.h> DWORD GetPid(){ DWORD dwPid=0; __asm { mov eax,fs:[0x18] // 获取PEB地址 add eax,0x20 // 加0x20得到进程PID mov eax,[eax] mov dwPid,eax } return dwPid; } int main() { printf("%d\n",GetPid()); return 0; }
获取线程PID:
#include "stdafx.h" #include <Windows.h> DWORD GetPid(){ DWORD dwPid=0; __asm { mov eax,fs:[0x18] // 获取PEB地址 add eax,0x20 // 加0x20得到进程PID add eax,0x04 // 加0x04得到线程PID mov eax,[eax] mov dwPid,eax } return dwPid; } int main() { printf("%d\n",GetPid()); return 0; }
通过标志反调试: 下方的调试标志BeingDebugged
是Char类型,为1表示调试状态.为0表示没有调试.可以用于反调试.
0:000> dt _peb ntdll!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 SpareBool : UChar +0x004 Mutant : Ptr32 Void
#include "stdafx.h" #include <Windows.h> int main() { DWORD dwIsDebug = 0; __asm { mov eax, fs:[0x18]; // 获取TEB mov eax, [eax + 0x30]; // 获取PEB movzx eax, [eax + 2]; // 获取调试标志 mov dwIsDebug,eax } if (1 == dwIsDebug) { printf("正在被调试"); } else { printf("没有被调试"); } return 0; }
通过API反调试:
#include <stdio.h> #include <stdlib.h> #include <Windows.h> int main() { STARTUPINFO temp; temp.cb = sizeof(temp); GetStartupInfo(&temp); if (temp.dwFlags != 1) { ExitProcess(0); } printf("程序没有被反调试"); return 0; }
反调试与绕过思路
BeingDebugged 属性反调试: 进程运行时,位置FS:[30h]指向PEB的基地址,为了实现反调试技术,恶意代码通过这个位置来检查BeingDebugged标志位是否为1,如果为1则说明进程被调试。
1.首先我们可以使用 dt _teb
命令解析一下TEB的结构,如下TEB结构的起始偏移为0x0,而0x30的位置指向的是 ProcessEnvironmentBlock
也就是指向了进程环境块。
0:000> dt _teb ntdll!_TEB +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : Ptr32 Void +0x020 ClientId : _CLIENT_ID +0x028 ActiveRpcHandle : Ptr32 Void +0x02c ThreadLocalStoragePointer : Ptr32 Void +0x030 ProcessEnvironmentBlock : Ptr32 _PEB // 此处是进程环境块 +0x034 LastErrorValue : Uint4B +0x038 CountOfOwnedCriticalSections : Uint4B +0x03c CsrClientThread : Ptr32 Void +0x040 Win32ThreadInfo : Ptr32 Void +0x044 User32Reserved : [26] Uint4B +0x0ac UserReserved : [5] Uint4B +0x0c0 WOW32Reserved : Ptr32 Void
只需要在进程环境块的基础上 +0x2
就能定位到线程环境块TEB中 BeingDebugged
的标志,此处的标志位如果为1则说明程序正在被调试,为0则说明没有被调试。
0:000> dt _peb ntdll!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 BitField : UChar +0x003 ImageUsesLargePages : Pos 0, 1 Bit +0x003 IsProtectedProcess : Pos 1, 1 Bit
我们手动来验证一下,首先线程环境块地址是007f1000
在此基础上加0x30
即可得到进程环境快的基地址007ee000
继续加0x2即可得到BeingDebugged
的状态 ffff0401
需要 byte=1
0:000> r $teb $teb=007f1000 0:000> dd 007f1000 + 0x30 007f1030 007ee000 00000000 00000000 00000000 007f1040 00000000 00000000 00000000 00000000 0:000> r $peb $peb=007ee000 0:000> dd 007ee000 + 0x2 007ee002 ffff0401 0000ffff 0c400112 19f0775f 007ee012 0000001b 00000000 09e0001b 0000775f
梳理一下知识点我们可以写出一下反调试代码,本代码单独运行程序不会出问题,一旦被调试器附加则会提示正在被调试。
#include <stdio.h> #include <windows.h> int main() { BYTE IsDebug = 0; __asm{ mov eax, dword ptr fs:[0x30] mov bl, byte ptr [eax+ 0x2] mov IsDebug, bl } /* 另一种反调试实现方式 __asm{ push dword ptr fs:[0x30] pop edx mov al, [edx + 2] mov IsDebug,al } */ if (IsDebug != 0) printf("本程序正在被调试. %d", IsDebug); else printf("程序没有被调试."); getchar(); return 0; }
如果恶意代码中使用该种技术阻碍我们正常调试,该如何绕过呢?如下我们只需要在命令行中执行dump fs:[30]+2
来定位到BeingDebugged的位置,并将其数值改为0然后运行程序,会发现反调试已经被绕过了。
ProcessHeap 属性反调试: 该属性是一个未公开的属性,它被设置为加载器为进程分配的第一个堆的位置,ProcessHeap位于PEB结构的0x18处,第一个堆头部有一个属性字段,这个属性叫做ForceFlags和Flags属性偏移为10,该属性为0说明程序没有被调试,非0则说明被调试。
0:000> dt !_peb ntdll!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x018 ProcessHeap : Ptr32 Void +0x01c FastPebLock : Ptr32 _RTL_CRITICAL_SECTION 0:000> r $peb $peb=006e1000 0:000> dd 006e1000+18 006e1018 00ca0000 775f09e0 00000000 00000000 0:000> dd 00ca0000 + 10 00ca0010 00ca00a4 00ca00a4 00ca0000 00ca0000
要实现反反调试,只需要将 00ca0000 + 10
位置的值修改为0即可,执行dump ds:[fs:[30] + 0x18] + 0x10
定位到修改即可。