脱壳的艺术--2 调试器检测技术

  
脱壳的艺术
Mark Vincent Yason
概述:脱壳是门艺术——脱壳既是一种心理挑战,同时也是逆向领域最为激动人心的智力游戏之一。为了甄别或解决非常难的反逆向技巧,逆向分析人员有时不得不了解操作系统的一些底层知识,聪明和耐心也是成功脱壳的关键。这个挑战既牵涉到壳的创建者,也牵涉到那些决心躲过这些保护的脱壳者。
本文主要目的是介绍壳常用的反逆向技术,同时也探讨了可以用来躲过或禁用这些保护的技术及公开可用的工具。这些信息将使研究人员特别是恶意代码分析人员在分析加壳的恶意代码时能识别出这些技术,当这些反逆向技术阻碍其成功分析时能决定下一步的动作。第二个目的,这里介绍的信息也会被那些计划在软件中添加一些保护措施用来减缓逆向分析人员分析其受保护代码的速度的研究人员用到。当然没有什么能使一个熟练的、消息灵通的、坚定的逆向分析人员止步的。
关键词:逆向工程、壳、保护、反调试、反逆向
2 调试器检测技术                                                             
本节列出了壳用来确定进程是否被调试或者系统内是否有调试器正在运行的技术。这些调试器检测技术既有非常简单(明显)的检查,也有涉及到native APIs和内核对象的。
2.1 PEB.BeingDebugged Flag : IsDebuggerPresent()
最基本的调试器检测技术就是检测进程环境块(PEB) 1中的BeingDebugged标志。kernel32!IsDebuggerPresent() API检查这个标志以确定进程是否正在被用户模式的调试器调试。
下面显示了IsDebuggerPresent() API的实现代码。首先访问线程环境块(TEB) 2得到PEB的地址,然后检查PEB偏移0x02位置的BeingDebugged标志。
mov                         eax, large fs: 18h
mov                       eax, [eax+30h]
movzx                      eax, byte ptr [eax+2]
retn
除了直接调用IsDebuggerPresent(),有些壳会手工检查PEB中的BeingDebugged标志以防逆向分析人员在这个API上设置断点或打补丁。
示例
下面是调用IsDebuggerPresent() API和使用PEB.BeingDebugged标志确定调试器是否存在的示例代码。
;call kernel32!IsDebuggerPresent()
call                   [IsDebuggerPresent]
test                   eax,eax
jnz                    .debugger_found
 
;check PEB.BeingDebugged directly
Mov                  eax,dword [fs:0x30]      ;EAX =  TEB.ProcessEnvironmentBlock
movzx               eax,byte [eax+0x02]      ;AL =  PEB.BeingDebugged
test                   eax,eax
jnz                    .debugger_found
由于这些检查很明显,壳一般都会用后面章节将会讨论的垃圾代码或者反—反编译技术进行混淆。
对策
人工将PEB.BeingDebugged标志置0可轻易躲过这个检测。在数据窗口中Ctrl+G(前往表达式)输入fs:[30],可以在OllyDbg中查看PEB数据。
另外Ollyscript命令"dbh"可以补丁这个标志。
dbh
最后,Olly Advanced3 插件有置BeingDebugged标志为0的选项。
2.2  PEB.NtGlobalFlag , Heap.HeapFlags, Heap.ForceFlags
PEB.NtGlobalFlag  PEB另一个成员被称作NtGlobalFlag(偏移0x68),壳也通过它来检测程序是否用调试器加载。通常程序没有被调试时,NtGlobalFlag成员值为0,如果进程被调试这个成员通常值为0x70(代表下述标志被设置):
FLG_HEAP_ENABLE_TAIL_CHECK(0X10)
FLG_HEAP_ENABLE_FREE_CHECK(0X20)
FLG_HEAP_VALIDATE_PARAMETERS(0X40)
这些标志是在ntdll!LdrpInitializeExecutionOptions()里设置的。请注意PEB.NtGlobalFlag的默认值可以通过gflags.exe工具或者在注册表以下位置创建条目来修改:
HKLM/Software/Microsoft/Windows Nt/CurrentVersion/Image File Execution Options
Heap Flags 由于NtGlobalFlag标志的设置,堆也会打开几个标志,这个变化可以在ntdll!RtlCreateHeap()里观测到。通常情况下为进程创建的第一个堆会将其Flags和ForceFlags 4分别设为0x02(HEAP_GROWABLE)和0 。然而当进程被调试时,这两个标志通常被设为0x50000062(取决于NtGlobalFlag)和0x40000060(等于Flags AND 0x6001007D)。默认情况下当一个被调试的进程创建堆时下列附加的堆标志将被设置:
HEAP_TAIL_CHECKING_ENABLED(0X20)
HEAP_FREE_CHECKING_ENABLED(0X40)
示例
下面的示例代码检查PEB.NtGlobalFlag是否等于0,为进程创建的第一个堆是否设置了附加标志(PEB.ProcessHeap):
;ebx = PEB
Mov                  ebx,[fs:0x30]
 
;Check if PEB.NtGlobalFlag != 0
Cmp                 dword [ebx+0x68],0
jne                    .debugger_found
 
;eax = PEB.ProcessHeap
Mov                  eax,[ebx+0x18]
 
;Check PEB.ProcessHeap.Flags
Cmp                 dword [eax+0x0c],2
jne                    .debugger_found
 
;Check PEB.ProcessHeap.ForceFlags
Cmp                 dword [eax+0x10],0
jne                    .debugger_found
对策
可以将 PEB.NtGlobalFlag和PEB.HeapProcess标志补丁为进程未被调试时的相应值。下面是一个补丁上述标志的ollyscript示例:
Var                   peb
var                    patch_addr
var                    process_heap
 
//retrieve PEB via a hardcoded TEB address( first thread: 0x7ffde000)
Mov                  peb,[7ffde000+30]
 
//patch PEB.NtGlobalFlag
Lea                   patch_addr,[peb+68]
mov                  [patch_addr],0
 
//patch PEB.ProcessHeap.Flags/ForceFlags
Mov                  process_heap,[peb+18]
lea                    patch_addr,[process_heap+0c]
mov                  [patch_addr],2
lea                    patch_addr,[process_heap+10]
mov                  [patch_addr],0
同样地Olly Advanced插件有设置PEB.NtGlobalFlag和PEB.ProcessHeap的选项。
2.3 DebugPort: CheckRemoteDebuggerPresent()/NtQueryInformationProcess()
Kernel32!CheckRemoteDebuggerPresent()是另一个可以用于确定是否有调试器被附加到进程的API。这个API内部调用了ntdll!NtQueryInformationProcess(),调用时ProcessInformationclass参数为ProcessDebugPort(7)。而NtQueryInformationProcess()检索内核结构EPROCESS5的DebugPort成员。非0的DebugPort成员意味着进程正在被用户模式的调试器调试。如果是这样的话,ProcessInformation 将被置为0xFFFFFFFF ,否则ProcessInformation 将被置为0。
Kernel32!CheckRemoteDebuggerPresent()接受2个参数,第1个参数是进程句柄,第2个参数是一个指向boolean变量的指针,如果进程被调试,该变量将包含TRUE返回值。
BOOL CheckRemoteDebuggerPresent(
 HANDLE      hProcess,
 PBOOL       pbDebuggerPresent
)
ntdll!NtQueryInformationProcess()有5个参数。为了检测调试器的存在,需要将ProcessInformationclass参数设为ProcessDebugPort(7):
NTSTATUS   NTAPI           NtQueryInformationProcess(
HANDLE                             ProcessHandle,
PROCESSINFOCLASS         ProcessInformationClass,
PVOID                                ProcessInformation,
ULONG                               ProcessInformationLength,
PULONG                      ReturnLength
)
示例
下面的例子显示了如何调用CheckRemoteDebuggerPresent()和NtQueryInformationProcess()来检测当前进程是否被调试:
; using Kernel32!CheckRemoteDebuggerPresent()
lea                 eax,[.bDebuggerPresent]
push               eax                              ;pbDebuggerPresent
push 0xffffffff                                     ;hProcess
call                 [CheckRemoteDebuggerPresent]
cmp                dword [.bDebuggerPresent],0
jne                .debugger_found
 
; using ntdll!NtQueryInformationProcess(ProcessDebugPort)
lea                 eax,[.dwReturnLen]
push               eax                              ;ReturnLength
push               4                                 ;ProcessInformationLength
lea                 eax,[.dwDebugPort]
push               eax                              ;ProcessInformation
push               ProcessDebugPort         ;ProcessInformationClass(7)
push                  0xffffffff                      ;ProcessHandle
call                   [NtQueryInformationProcess]
cmp                dword [.dwDebugPort],0
jne                  .debugger_found
对策
一种方法是在NtQueryInformationProcess()返回的地方设置断点,当这个断点被断下来后,将ProcessInformation 补丁为0。 下面是自动执行这个方法的ollyscript示例:
var                    bp_NtQueryInformationProcess
 
// set a breakpoint handler
eob                 bp_handler_NtQueryInformationProcess
 
// set a breakpoint where NtQueryInformationProcess returns
gpa                 "NtQueryInformationProcess","ntdll.dll"
find          $RESULT,#C21400# //retn 14
mov         bp_NtQueryInformationProcess,$RESULT
bphws             bp_NtQueryInformationProcess,"X"
run
 
bp_handler_NtQueryInformationProcess:
 
//ProcessInformationClass == ProcessDebugPort?
cmp                [esp+8],7
jne                  bp_handler_NtQueryInformationProcess_continue
 
//patch ProcessInformation to 0
mov         patch_addr,[esp+c]
mov         [patch_addr],0
 
// clear breakpoint
bphwc             bp_NtQueryInformationProcess
 
bp_handler_NtQueryInformationProcess_continue:
run
Olly Advanced插件有一个patch NtQueryInformationProcess()的选项,这个补丁涉及注入一段代码来操纵NtQueryInformationProcess()的返回值。
2.4 Debugger Interrupts
在调试器中步过INT3和INT1指令的时候,由于调试器通常会处理这些调试中断,所以异常处理例程默认情况下将不会被调用,Debugger Interrupts就利用了这个事实。这样壳可以在异常处理例程中设置标志,通过INT指令后如果这些标志没有被设置则意味着进程正在被调试。另外,kernel32!DebugBreak()内部是调用了INT3来实现的,有些壳也会使用这个API。
示例
这个例子在异常处理例程中设置EAX的值为0xFFFFFFFF(通过CONTEXT 6记录)以此来判断异常处理例程是否被调用:
; set exception handler
push         .exeception_handler
push         dword [fs:0]
mov         [fs:0],esp
 
;reset flag(EAX) invoke int3
xor           eax,eax
int3
 
;restore exception handler
pop          dword [fs:0]
add          esp,4
 
; check if the flag had been set
test           eax,eax
je             .debugger_found
:::
.exeception_handler:
;EAX = ContextRecord
mov         eax,[esp+0xc]
;set flag (ContextRecord.EAX)
mov         dword [eax+0xb0],0xffffffff
;set ContextRecord.EIP
inc           dword [eax+0xb8]
xor           eax,eax
retn
对策
由于调试中断而导致执行停止时,在OllyDbg中识别出异常处理例程(通过视图->SEH链)并下断点,然后Shift+F9将调试中断/异常传递给异常处理例程,最终异常处理例程中的断点会断下来,这时就可以跟踪了。

另一个方法是允许调试中断自动地传递给异常处理例程。在OllyDbg中可以通过 选项-> 调试选项 -> 异常 -> 忽略下列异常 选项卡中钩选"INT3中断""单步中断"复选框来完成设置。

2.5 Timing Checks

当进程被调试时,调试器事件处理代码、步过指令等将占用CPU循环。如果相邻指令之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试,而壳正好利用了这一点。

示例

下面是一个简单的时间检查的例子。在某一段指令的前后用RDTSC指令(Read Time-Stamp Counter)并计算相应的增量。增量值0x200取决于两个RDTSC指令之间的代码执行量。

rdtsc

mov         ecx,eax

mov         ebx,edx

 

;...more instructions

nop

push         eax

pop          eax

nop

;...more instructions

 

;compute delta between RDTSC instructions

rdtsc

 

;Check high order bits

cmp         edx,ebx

ja             .debugger_found

;Check low order bits

sub           eax,ecx

cmp         eax,0x200

ja             .debugger_found

其它的时间检查手段包括使用kernel32!GetTickCount() API 或者手工检查位于0x7FFE0000地址的SharedUserData7数据结构的TickCountLow TickCountMultiplier 成员。

使用垃圾代码或者其它混淆技术进行隐藏以后,这些时间检查手段尤其是使用RDTSC将会变得难于识别。

对策

一种方法就是找出时间检查代码的确切位置,避免步过这些代码。逆向分析人员可以在增量比较代码之前下断然后用 运行 代替 步过 直到断点断下来。另外也可以下GetTickCount()断点以确定这个API在什么地方被调用或者用来修改其返回值。

Olly Advanced采用另一种方法——它安装了一个内核模式驱动程序做以下工作:

1 设置控制寄存器CR48中的时间戳禁止位(TSD),当这个位被设置后如果RDTSC指令在非Ring0下执行将会触发一个通用保护异常(GP)。

2 中断描述表(IDT)被设置以挂钩GP异常并且RTDSC的执行被过滤。如果是由于RDTSC指令引发的GP,那么仅仅将前次调用返回的时间戳加1

值得注意的是上面讨论的驱动可能会导致系统不稳定,应该始终在非生产机器或虚拟机中进行尝试。

2.6 SeDebugPrivilege

默认情况下进程是没有SeDebugPrivilege权限的。然而进程通过OllyDbgWinDbg之类的调试器载入的时候,SeDebugPrivilege权限被启用了。这种情况是由于调试器本身会调整并启用SeDebugPrivilege权限,当被调试进程加载时SeDebugPrivilege权限也被继承了。

一些壳通过打开CSRSS.EXE进程间接地使用SeDebugPrivilege确定进程是否被调试。如果能够打开CSRSS.EXE意味着进程启用了SeDebugPrivilege权限,由此可以推断进程正在被调试。这个检查能起作用是因为CSRSS.EXE进程安全描述符只允许SYSTEM访问,但是一旦进程拥有了SeDebugPrivilege权限,就可以忽视安全描述符9而访问其它进程。注意默认情况下这一权限仅仅授予了Administrators组的成员。

示例

下面是SeDebugPrivilege检查的例子:

;query for the PID of CSRSS.EXE

call                 [CsrGetProcessId]

 

;try to open the CSRSS.EXE process

push                eax

push                FALSE

push                PROCESS_QUERY_INFORMATION

call                 [OpenProcess]

 

;if OpenProcess() was successful,

;process is probably being debugged

test                  eax,eax

jnz                  .debugger_found

这里使用了ntdll!CsrGetProcessId() API获取CSRSS.EXEPID,但是壳也可能通过手工枚举进程来得到CSRSS.EXEPID。如果OpenProcess()成功则意味着SeDebugPrivilege权限被启用,这也意味着进程很可能被调试。

对策

一种方法是在ntdll!NtOpenProcess()返回的地方设断点,一旦断下来后,如果传入的是CSRSS.EXEPID则修改EAX值为0xC0000022STATUS_ACCESS_DENIED)

2.7 Parent Process(检测父进程)

通常进程的父进程是explorer.exe(双击执行的情况下),父进程不是explorer.exe说明程序是由另一个不同的应用程序打开的,这很可能就是程序被调试了。

下面是实现这种检查的一种方法:

1 通过TEB(TEB.ClientId)或者使用GetCurrentProcessId()来检索当前进程的PID

2 Process32First/Next()得到所有进程的列表,注意explorer.exePID(通过PROCESSENTRY32.szExeFile)和通过PROCESSENTRY32.th32ParentProcessID获得的当前进程的父进程PID

3 如果父进程的PID不是explorer.exePID,则目标进程很可能被调试

但是请注意当通过命令行提示符或默认外壳非explorer.exe的情况下启动可执行程序时,这个调试器检查会引起误报。

对策

Olly Advanced提供的方法是让Process32Next()总是返回fail,这样壳的进程枚举代码将会失效,由于进程枚举失效PID检查将会被跳过。这些是通过补丁 kernel32!Process32NextW()的入口代码(将EAX值设为0然后直接返回)实现的。

77E8D 1C 2 >  33C 0            xor     eax, eax

77E8D 1C 4     C 3              retn

77E8D 1C 5    83EC 0C          sub     esp, 0C

2.8 DebugObject: NtQueryObject()

除了识别进程是否被调试之外,其他的调试器检测技术牵涉到检查系统当中是否有调试器正在运行。

逆向论坛中讨论的一个有趣的方法就是检查DebugObject10类型内核对象的数量。这种方法之所以有效是因为每当一个应用程序被调试的时候,将会为调试对话在内核中创建一个DebugObject类型的对象。

DebugObject的数量可以通过ntdll!NtQueryObject()检索所有对象类型的信息而获得。NtQueryObject接受5个参数,为了查询所有的对象类型,ObjectHandle参数被设为NULLObjectInformationClass参数设为ObjectAllTypeInformation(3)

NTSTATUS NTAPI NtQueryObject(

HANDLE                                           ObjectHandle,

OBJECT_INFORMATION_CLASS       ObjectInformationClass,

PVOID                                               ObjectInformation,

ULONG                                             Length,

PULONG                                                  ResultLength

)

这个API返回一个OBJECT_ALL_INFORMATION结构,其中NumberOfObjectsTypes成员为所有的对象类型在ObjectTypeInformation数组中的计数:

typedef struct _OBJECT_ALL_INFORMATION{

ULONG                                             NumberOfObjectsTypes;

OBJECT_TYPE_INFORMATION        ObjectTypeInformation[1];

}

检测例程将遍历拥有如下结构的ObjectTypeInformation数组:

typedef struct _OBJECT_TYPE_INFORMATION{

[00] UNICODE_STRING      TypeName;

[08] ULONG                        TotalNumberofHandles;

[ 0C ] ULONG                        TotalNumberofObjects;

...more fields...

}

TypeName成员与UNICODE字符串"DebugObject"比较,然后检查TotalNumberofObjects TotalNumberofHandles 是否为非0值。

对策

NtQueryInformationProcess()解决方法类似,在NtQueryObject()返回处设断点,然后补丁 返回的OBJECT_ALL_INFORMATION结构,另外NumberOfObjectsTypes成员可以置为0以防止壳遍历ObjectTypeInformation数组。可以通过创建一个类似于NtQueryInformationProcess()解决方法的ollyscript脚本来执行这个操作。

类似地,Olly Advanced插件向NtQueryObject() API中注入代码,如果检索的是ObjectAllTypeInformation类型则用0清空整个返回的缓冲区。

2.9 Debugger Window

调试器窗口的存在标志着有调试器正在系统内运行。由于调试器创建的窗口拥有特定类名(OllyDbg的是OLLYDBGWinDbg的是WinDbgFrameClass),使用user32!FindWindow()或者user32!FindWindowEx()能很容易地识别这些调试器窗口。

示例

下面的示例代码使用FindWindow()查找OllyDbgWinDbg创建的窗口来识别他们是否正在系统中运行。

push                NULL

push                .szWindowClassOllyDbg

call                 [FindWindowA]

test                  eax,eax

jnz                  .debugger_found

 

push                NULL

push                .szWindowClassWinDbg

call                 [FindWindowA]

test                  eax,eax

jnz                  .debugger_found

 

.szWindowClassOllyDbg  db “OLLYDBG”,0

.szWindowClassWinDbg  db “WinDbgFrameClass”,0

对策

一种方法是在FindWindow()/FindWindowEx()的入口处设断点,断下来后,改变lpClassName参数的内容,这样API将会返回fail,另一种方法就是直接将返回值设为NULL

2.10 Debugger Process

另外一种识别系统内是否有调试器正在运行的方法是列出所有的进程,检查进程名是否与调试器(如 OLLYDBG.EXE,windbg.exe等)的相符。实现很直接,利用Process32First/Next()然后检查映像名称是否与调试器相符就行了。

有些壳也会利用kernel32!ReadProcessMemory()读取进程的内存,然后寻找调试器相关的字符串(如”OLLYDBG”)以防止逆向分析人员修改调试器的可执行文件名。一旦发现调试器的存在,壳要么显示一条错误信息,要么默默地退出或者终止调试器进程。

对策

和父进程检查类似,可以通过补丁 kernel32!Process32NextW() 使其总是返回fail值来防止壳枚举进程。

2.11 Device Drivers

检测内核模式的调试器是否活跃于系统中的典型技术是访问他们的设备驱动程序。该技术相当简单,仅涉及调用kernel32!CreateFile()检测内核模式调试器(如SoftICE)使用的那些众所周知的设备名称。

示例

一个简单的检查如下:

push         NULL

push         0

push         OPEN_EXISTING

push         NULL

push         FILE_SHARE_READ

push         GENERIC_READ

push         .szDeviceNameNtice

call          [CreateFileA]

cmp         eax,INVALID_HANDLE_VALUE

jne           .debugger_found

 

.szDeviceNameNtice   db "//./NTICE",0

某些版本的SoftICE会在设备名称后附加数字导致这种检查失败,逆向论坛中相关的描述是穷举附加的数字直到发现正确的设备名称。新版壳也用设备驱动检测技术检测诸如RegmonFilemon之类的系统监视程序的存在。

对策

一种简单的方法就是在kernel32!CreateFileW()内设置断点,断下来后,要么操纵FileName参数要么改变其返回值为INVALID_HANDLE_VALUE0xFFFFFFFF)。

2.12 OllyDbgGuard Pages

这个检查是针对OllyDbg的,因为它和OllyDbg的内存访问/写入断点特性相关。

除了硬件断点和软件断点外,OllyDbg允许设置一个内存访问/写入断点,这种类型的断点是通过页面保护11来实现的。简单地说,页面保护提供了当应用程序的某块内存被访问时获得通知这样一个途径。

页面保护是通过PAGE_GUARD页面保护修改符来设置的,如果访问的内存地址是受保护页面的一部分,将会产生一个STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。如果进程被OllyDbg调试并且受保护的页面被访问,将不会抛出异常,访问将会被当作内存断点来处理,而壳正好利用了这一点。

示例

下面的示例代码中,将会分配一段内存,并将待执行的代码保存在分配的内存中,然后启用页面的PAGE_GUARD属性。接着初始化标设符EAX0,然后通过执行内存中的代码来引发STATUS_GUARD_PAGE_VIOLATION异常。如果代码在OllyDbg中被调试,因为异常处理例程不会被调用所以标设符将不会改变。

;set up exception handler

push         .exception_handle

push         dword [fs:0]

mov         [fs:0],esp

 

;allocate memory

push         PAGE_READWRITE

push         MEM_COMMIT

push         0x1000

push         NULL

call          [VirtualAlloc]

test           eax,eax

jz             .failed

mov         [.pAllocatedMem],eax

 

;store a RETN on the allocated memory

mov         byte [eax],0xC3

;then set the PAGE_GUARD attribute of the allocated memory

lea           eax,[.dwOldProtect]

push         eax

push         PAGE_EXECUTE_READ | PAGE_GUARD

push         0x1000

push         dword [.pAllocatedMem]

call          [VirtualProtect]

 

;set marker (EAX) as 0

xor           eax,eax

;trigger a STATUS_GUARD_PAGE_VIOLATION exception

call          [.pAllocatedMem]

;check if marker had not been changed (exception handler not called)

test           eax,eax

je             .debugger_found

 

.exception_handler

;EAX = CONTEXT record

mov         eax,[esp+0xC]

;set marker (CONTEXT.EAX) to 0xFFFFFFFF

;to signal that the exception handler was called

mov         dword [eax+0xb0],0xFFFFFFFF

xor           eax,eax

retn

对策

由于页面保护引发一个异常,逆向分析人员可以故意引发一个异常,这样异常处理例程将会被调用。在示例中,逆向分析人员可以用INT3指令替换掉RETN指令,一旦INT3指令被执行,Shift+F9强制调试器执行异常处理代码。这样当异常处理例程调用后,EAX将被设为正确的值,然后RETN指令将会被执行。

如果异常处理例程里检查异常是否真地是STATUS_GUARD_PAGE_VIOLATION,逆向分析人员可以在异常处理例程中下断点然后修改传入的ExceptionRecord参数,具体来说就是ExceptionCode 手工将ExceptionCode设为STATUS_GUARD_PAGE_VIOLATION 即可。

你可能感兴趣的:(脱壳的艺术--2 调试器检测技术)