当前位置:文档之家› 脱壳的艺术+逆向工程、壳、保护、反调试、反逆向

脱壳的艺术+逆向工程、壳、保护、反调试、反逆向

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_V ALIDATE_PARAMETERS(0X40)

这些标志是在ntdll!LdrpInitializeExecutionOptions()里设置的。请注意PEB.NtGlobalFlag 的默认值可以通过gflags.exe工具或者在注册表以下位置创建条目来修改:

HKLM\Software\Microsoft\Windows Nt\CurrentVersion\Image File Execution Options

Heap Flags由于NtGlobalFlag标志的设置,堆也会打开几个标志,这个变化可以在ntdll!RtlCreateHeap()里观测到。通常情况下为进程创建的第一个堆会将其Flags和ForceFlags4分别设为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(通过CONTEXT6记录)以此来判断异常处理例程是否被调用:

; 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权限的。然而进程通过OllyDbg和WinDbg之类的调试器载入的时候,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_INFORMA TION

call [OpenProcess]

;if OpenProcess() was successful,

;process is probably being debugged

test eax,eax

jnz .debugger_found

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

对策

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

2.7 Parent Process(检测父进程)

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

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

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

2 用Process32First/Next()得到所有进程的列表,注意explorer.exe的PID(通过

PROCESSENTRY32.szExeFile)和通过PROCESSENTRY32.th32ParentProcessID获得的当前进程的父进程PID

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

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

对策

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

77E8D1C2 > 33C0 xor eax, eax

77E8D1C4 C3 retn

77E8D1C5 83EC 0C sub esp, 0C

2.8 DebugObject: NtQueryObject()

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

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

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

NTSTATUS NTAPI NtQueryObject(

HANDLE ObjectHandle,

OBJECT_INFORMA TION_CLASS ObjectInformationClass,

PVOID ObjectInformation,

ULONG Length,

PULONG ResultLength

)

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

typedef struct _OBJECT_ALL_INFORMA TION{

ULONG NumberOfObjectsTypes;

OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];

}

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

typedef struct _OBJECT_TYPE_INFORMA TION{

[00] UNICODE_STRING TypeName;

[08] ULONG TotalNumberofHandles;

[0C] ULONG TotalNumberofObjects;

...more fields...

}

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

对策

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

类似地,Olly Advanced插件向NtQueryObject() API中注入代码,如果检索的是

ObjectAllTypeInformation类型则用0清空整个返回的缓冲区。

2.9 Debugger Window

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

示例

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

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 “OLL YDBG”,0

.szWindowClassWinDbg db “WinDbgFrameClass”,0

对策

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

2.10 Debugger Process

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

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

对策

和父进程检查类似,可以通过补丁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,INV ALID_HANDLE_V ALUE

jne .debugger_found

.szDeviceNameNtice db "\\.\NTICE",0

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

对策

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

2.12 OllyDbg:Guard Pages

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

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

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

示例

下面的示例代码中,将会分配一段内存,并将待执行的代码保存在分配的内存中,然后启用页面的PAGE_GUARD属性。接着初始化标设符EAX为0,然后通过执行内存中的代码来引发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 STA TUS_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即可。

3 断点和补丁检测技术

本节列举了壳最常用的识别软件断点、硬件断点和补丁的方法。

3.1 Software Breakpoint Detection

软件断点是通过修改目标地址代码为0xCC(INT3/Breakpoint Interrupt)来设置的断点。壳通过在受保护的代码段和(或)API函数中扫描字节0xCC来识别软件断点。

示例

检测可能和下面一样简单:

cld

mov edi,Protected_Code_Start

mov ecx,Protected_Code_End - Protected_Code_Start

mov al,0xcc

repne scasb

jz .breakpoint_found

有些壳对比较的字节值作了些运算使得检测变得不明显,例如:

if ( byte XOR 0x55 == 0x99 ) then breakpoint found

Where: 0x99 == 0xCC XOR 0x55

对策

如果软件断点被发现了逆向分析人员可以使用硬件断点来代替。如果需要在API内部下断,但是壳又检测API内部的断点,逆向分析人员可以在最终被ANSI版API调用的UNICODE版的API下断(如:用LoadLibraryExW代替LoadLibraryA),或者用相应的native API来代替。

3.2 Hardware Breakpoint Detection

另一种断点称之为硬件断点,硬件断点是通过设置名为Dr0到Dr7的调试寄存器12来实现的。Dr0-Dr3包含至多4个断点的地址,Dr6是个标志,它指示哪个断点被触发了,Dr7包含了控制4个硬件断点诸如启用/禁用或者中断于读/写的标志。

由于调试寄存器无法在Ring3下访问,硬件断点的检测需要执行一小段代码。壳利用了含有调试寄存器值的CONTEXT结构,CONTEXT结构可以通过传递给异常处理例程的ContextRecord参数来访问。

示例

这是一段查询调试寄存器的示例代码:

; set up exception handler

push .exception_handler

push dword [fs:0]

mov [fs:0],esp

;eax will be 0xFFFFFFFF if hardware breakpoints are identified

xor eax,eax

;throw an exception

mov dword [eax],0

;restore exception handler

pop dword [fs:0]

add esp,4

;test if EAX was updated (breakpoint identified)

test eax,eax

jnz .breakpoint_found

:::

.exception_handler

;EAX = CONTEXT record

mov eax,[esp+0xc]

;check if Debug Registers Context.Dr0-Dr3 is not zero

cmp dword [eax+0x04],0

jne .hardware_bp_found

cmp dword [eax+0x08],0

jne .hardware_bp_found

cmp dword [eax+0x0c],0

jne .hardware_bp_found

cmp dword [eax+0x10],0

jne .hardware_bp_found

jmp .exception_ret

.hardware_bp_found

;set Context.EAX to signal breakpoint found

mov dword [eax+0xb0],0xFFFFFFFF

.exception_ret

;set Context.EIP upon return

add dword [eax+0xb8],6

xor eax,eax

retn

有些壳也利用调试寄存器的值作为解密密钥的一部分。这些调试寄存器要么初始化为一

个特定值要么为0。因此,如果这些调试寄存器被修改,解密将会失败。当解密的代码是受保护的程序或者脱壳代码的一部分的时候,将导致无效指令并造成程序一些意想不到的终止。

对策

如果壳没检测软件断点,逆向分析人员可以尝试使用软件断点,同样OllyDbg的内存读/写断点也可以使用。当逆向分析人员需要设置API断点的时候在native或者是UNICODE 版的API内部设软件断点也是可行的。

3.3 Patching Detection via Code Checksum Calculation

补丁检测技术能识别壳的代码是否被修改(代码被修改则意味着反调试例程已经被禁用了),其次也能识别是否设置了软件断点。补丁检测是通过代码校验来实现的,校验计算包括从简单到复杂的校验和/哈希算法。

示例

下面是一个比较简单的校验和计算的例子:

mov esi,Protected_Code_Start

mov ecx,Protected_Code_End - Protected_Code_Start

xor eax,eax

.checksum_loop

movzx ebx,byte [esi]

add eax,ebx

rol eax,1

inc esi

loop .checksum_loop

cmp eax,dword [.dwCorrectChecksum]

jne .patch_found

对策

如果代码校验例程识别出了软件断点,可以用硬件断点来代替。如果校验例程识别出了代码补丁,逆向分析人员可以通过在补丁地址设置内存访问断点来定位校验例程所在,一旦发现了校验例程,可以修改校验和为预期的值或者在比较失败后修改适当的标志。

4反分析技术

反分析技术的目标是减缓逆向分析人员对受保护代码和(或)加壳后的程序分析和理解的速度。我们将讨论诸如加密/压缩、垃圾代码、代码变形、反-反编译等技术,这些技术的目的是为了混淆代码、考验耐心、浪费逆向分析人员的时间,解决这些问题需要逆向分析人员拥有耐心、聪慧等品质。

4.1 Encryption and Compression

加密和压缩是最基本的反分析形式。它们初步设防,防止逆向分析人员直接在反编译器

内加载受保护的程序然后没有任何困难地开始分析。

加密壳通常都既加密本身代码也加密受保护的程序。不同的壳所采用的加密算法大不相同,有非常简单的XOR循环,也有执行数次运算的非常复杂的循环。对于某些多态变形壳,为了防止查壳工具正确地识别壳,每次加壳所采用的加密算法都不同,解密代码也通过变形显得很不一样。

解密例程作为一个取数、计算、存诸操作的循环很容易辨认。下面是一个对加密过的DWORD值执行数次XOR操作的简单的解密例程。

0040A07C LODS DWORD PTR DS:[ESI]

0040A07D XOR EAX,EBX

0040A07F SUB EAX,12338CC3

0040A084 ROL EAX,10

0040A087 XOR EAX,799F82D0

0040A08C STOS DWORD PTR ES:[EDI]

0040A08D INC EBX

0040A08E LOOPD SHORT 0040A07C ;decryption loop

这里是另一个多态变形壳的解密例程:

00476056 MOV BH,BYTE PTR DS:[EAX]

00476058 INC ESI

00476059 ADD BH,0BD

0047605C XOR BH,CL

0047605E INC ESI

0047605F DEC EDX

00476060 MOV BYTE PTR DS:[EAX],BH

00476062 CLC

00476063 SHL EDI,CL

:::More garbage code

00476079 INC EDX

0047607A DEC EDX

0047607B DEC EAX

0047607C JMP SHORT 0047607E

0047607E DEC ECX

0047607F JNZ 00476056 ;decryption loop

下面是由同一个多态壳生成的另一段解密例程:

0040C045 MOV CH,BYTE PTR DS:[EDI]

0040C047 ADD EDX,EBX

0040C049 XOR CH,AL

0040C04B XOR CH,0D9

0040C04E CLC

0040C04F MOV BYTE PTR DS:[EDI],CH

0040C051 XCHG AH,AH

0040C053 BTR EDX,EDX

0040C056 MOVSX EBX,CL

::: More garbage code

0040C067 SAR EDX,CL

0040C06C NOP

0040C06D DEC EDI

0040C06E DEC EAX

0040C06F JMP SHORT 0040C071

0040C071 JNZ 0040C045 ;decryption loop

上面两个示例中高亮的行是主要的解密指令,其余的指令都是用来迷惑逆向分析人员的垃圾代码。注意寄存器是如何交换的,还有两个示例之间解密方法是如何改变的。

Compression压缩的主要目的是为了缩小可执行文件代码和数据的大小,但是由于原始的包含可读字符串的可执行文件变成了压缩数据,因此也有那么一些混淆的作用。看看几款壳所使用的压缩引擎:UPX使用NRV(Not Really Vanished)和LZMA(Lempel-Ziv-Markov chain-Algorithm),FSG使用aPLib,Upack使用LZMA,yoda加密壳使用LZO。这其中有些压缩引擎可以自由地使用于非商业应用,但是商业应用需要许可/注册。

对策

解密和解压缩循环很容易就能被躲过,逆向分析人员只需要知道解密和解压缩循环何时结束,然后在循环结束后面的指令上下断点。记住,有些壳会在解密循环中检测断点。

4.2 Garbage Code and Code Permutation

Garbage Code在脱壳的例程中插入垃圾代码是另一种有效地迷惑逆向分析人员的方法。它的目的是在加密例程或者诸如调试器检测这样的反逆向例程中掩盖真正目的的代码。通过将本文描述过的调试器/断点/补丁检测技术隐藏在一大堆无关的、不起作用的、混乱的指令中,垃圾代码可以增加这些检测的效果。此外,有效的垃圾代码是那些看似合法/有用的代码。

示例

下面是一段在相关的指令中插入了垃圾代码的解密例程:

0044A21A JMP SHORT sample.0044A21F

0044A21C XOR DWORD PTR SS:[EBP],6E4858D

0044A223 INT 23

0044A225 MOV ESI,DWORD PTR SS:[ESP]

0044A228 MOV EBX,2C322FF0

0044A22D LEA EAX,DWORD PTR SS:[EBP+6EE5B321]

0044A233 LEA ECX DWORD PTR DS:[ESI+543D583E]

0044A239 ADD EBP,742C0F15

0044A23F ADD DWORD PTR DS:[ESI],3CB3AA25

0044A245 XOR EDI,7DAC77E3

0044A24B CMP EAX,ECX

0044A24D MOV EAX,5ACAC514

0044A252 JMP SHORT sample.0044A257

0044A254 XOR DWORD PTR SS:[EBP],AAE47425

0044A25B PUSH ES

0044A25C ADD EBP,5BAC5C22

0044A262 ADC ECX,3D71198C

0044A268 SUB ESI,-4

0044A26B ADC ECX,3795A210

0044A271 DEC EDI

0044A272 MOV EAX,2F57113F

0044A277 PUSH ECX

0044A278 POP ECX

0044A279 LEA EAX,DWORD PTR SS:[EBP+3402713D]

0044A27F EDC EDI

0044A280 XOR DWORD PTR DS:[ESI],33B568E3

0044A286 LEA EBX,DWORD PTR DS:[EDI+57DEFEE2]

0044A28C DEC EDI

0044A28D SUB EBX,7ECDAE21

0044A293 MOV EDI,185C5C6C

0044A298 MOV EAX,4713E635

0044A29D MOV EAX,4

0044A2A2 ADD ESI,EAX

0044A2A4 MOV ECX,1010272F

0044A2A9 MOV ECX,7A49B614

0044A2AE CMP EAX,ECX

0044A2B0 NOT DWORD PTR DS:[ESI]

示例中相关的解密指令是:

0044A225 MOV ESI,DWORD PTR SS:[ESP]

0044A23F ADD DWORD PTR DS:[ESI],3CB3AA25

0044A268 SUB ESI,-4

0044A280 XOR DWORD PTR DS:[ESI],33B568E3

0044A29D MOV EAX,4

0044A2A2 ADD ESI,EAX

0044A2B0 NOT DWORD PTR DS:[ESI]

Code Permutation代码变形是更高级壳使用的另一种技术。通过代码变形,简单的指令变成了复杂的指令序列。这要求壳理解原有的指令并能生成新的执行相同操作的指令序列。

一个简单的指令置换示例:

mov eax,ebx

test eax,eax

转换成下列等价的指令:

push ebx

pop eax

or eax,eax

结合垃圾代码使用,代码变形是一种有效地减缓逆向分析人员理解受保护代码速度的技

术。

示例

为了说明,下面是一个通过代码变形并在置换后的代码间插入了垃圾代码的调试器检测例程:

004018A8 MOV ECX,A104B412

004018AD PUSH 004018C1

004018B2 RETN

004018B3 SHR EDX,5

004018B6 ADD ESI,EDX

004018B8 JMP SHORT 004018BA

004018BA XOR EDX,EDX

004018BC MOV EAX,DWORD PTR DS:[ESI]

004018BE STC

004018BF JB SHORT 004018DE

004018C1 SUB ECX,EBX

004018C3 MOV EDX,9A01AB1F

004018C8 MOV ESI,DWORD PTR FS:[ECX]

004018CB LEA ECX DWORD PTR DS:[EDX+FFFF7FF7]

004018D1 MOV EDX,600

004018D6 TEST ECX,2B73

004018DC JMP SHORT 004018B3

004018DE MOV ESI,EAX

004018E0 MOV EAX,A35ABDE4

004018E5 MOV ECX,FAD1203A

004018EA MOV EBX,51AD5EF2

004018EF DIV EBX

004018F1 ADD BX,44A5

004018F6 ADD ESI,EAX

004018F8 MOVZX EDI,BYTE PTR DS:[ESI]

004018FB OR EDI,EDI

004018FD JNZ SHORT 00401906

其实这是一个很简单的调试器检测例程:

00401081 MOV EAX,DWORD PTR FS:[18]

00401087 MOV EAX,DWORD PTR DS:[EAX+30]

0040108A MOVZX EAX,BYTE PTR DS:[EAX+2]

0040108E TEST EAX,EAX

00401090 JNZ SHORT 00401099

对策

垃圾代码和代码变形是一种用来考验耐心和浪费逆向分析人员的时间的方式。因此,重要的是知道这些混淆技术背后隐藏的指令是否值得去理解(是不是仅仅执行解密、壳的初始化等动作)。

相关主题
文本预览
相关文档 最新文档