配色: 字号:
脱壳艺术
2014-05-26 | 阅:  转:  |  分享 
  
脱壳的艺术

MarkVincentYason

概述:脱壳是门艺术——脱壳既是一种心理挑战,同时也是逆向领域最为激动人心的智力游戏之一。为了甄别或解决非常难的反逆向技巧,逆向分析人员有时不得不了解操作系统的一些底层知识,聪明和耐心也是成功脱壳的关键。这个挑战既牵涉到壳的创建者,也牵涉到那些决心躲过这些保护的脱壳者。

本文主要目的是介绍壳常用的反逆向技术,同时也探讨了可以用来躲过或禁用这些保护的技术及公开可用的工具。这些信息将使研究人员特别是恶意代码分析人员在分析加壳的恶意代码时能识别出这些技术,当这些反逆向技术阻碍其成功分析时能决定下一步的动作。第二个目的,这里介绍的信息也会被那些计划在软件中添加一些保护措施用来减缓逆向分析人员分析其受保护代码的速度的研究人员用到。当然没有什么能使一个熟练的、消息灵通的、坚定的逆向分析人员止步的。

关键词:逆向工程、壳、保护、反调试、反逆向

1简介

在逆向工程领域,壳是最有趣的谜题之一。在解谜的过程中,逆向分析人员会获得许多关于系统底层、逆向技巧等知识。

壳(这个术语在本文中既指压缩壳也包括加密壳)是用来防止程序被分析的。它们被商业软件合法地用于防止信息披露、篡改及盗版。可惜恶意软件也基于同样的理由在使用壳,只不过动机不良。

由于大量恶意软件存在加壳现象,研究人员和恶意代码分析人员为了分析代码,开始学习脱壳的技巧。但是随着时间的推移,为防止逆向分析人员分析受保护的程序并成功脱壳,新的反逆向技术也被不断地添加到壳中。并且战斗还在继续,新的反逆向技术被开发的同时逆向分析人员也在针锋相对地发掘技巧、研究技术并开发工具来对付它们。

本文主要关注于介绍壳所使用的反逆向技术,同时也探讨了躲过/禁用这些保护措施的工具及技术。可能有些壳通过抓取进程映像(dump)能够轻易被搞定,这时处理反逆向技术似乎没有必要,但是有些情况下加密壳的代码需要加以跟踪和分析,例如:

需要躲过部分加密壳代码以便抓取进程映像、让输入表重建工具正确地工作。

深入分析加密壳代码以便在一个反病毒产品中整合进脱壳支持。

此外,当反逆向技术被恶意程序直接应用,以防止跟踪并分析其恶意行为时,熟悉反逆向技术也是很有价值的。

本文绝不是一个完整的反逆向技术的清单,因为它只涵盖了壳中常用的、有趣的一些技术。建议读者参阅最后一节的链接和图书资料,以了解更多其他逆向及反逆向的技术。

笔者希望您觉得这些材料有用,并能应用其中的技术。脱壳快乐!

2调试器检测技术

本节列出了壳用来确定进程是否被调试或者系统内是否有调试器正在运行的技术。这些调试器检测技术既有非常简单(明显)的检查,也有涉及到nativeAPIs和内核对象的。

2.1PEB.BeingDebuggedFlag:IsDebuggerPresent()

最基本的调试器检测技术就是检测进程环境块(PEB)1中的BeingDebugged标志。kernel32!IsDebuggerPresent()API检查这个标志以确定进程是否正在被用户模式的调试器调试。

下面显示了IsDebuggerPresent()API的实现代码。首先访问线程环境块(TEB)2得到PEB的地址,然后检查PEB偏移0x02位置的BeingDebugged标志。

mov eax,largefs:18h

mov eax,[eax+30h]

movzx eax,byteptr[eax+2]

retn

除了直接调用IsDebuggerPresent(),有些壳会手工检查PEB中的BeingDebugged标志以防逆向分析人员在这个API上设置断点或打补丁。

示例

下面是调用IsDebuggerPresent()API和使用PEB.BeingDebugged标志确定调试器是否存在的示例代码。

;callkernel32!IsDebuggerPresent()

call [IsDebuggerPresent]

test eax,eax

jnz .debugger_found



;checkPEB.BeingDebuggeddirectly

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

最后,OllyAdvanced3插件有置BeingDebugged标志为0的选项。

2.2PEB.NtGlobalFlag,Heap.HeapFlags,Heap.ForceFlags

PEB.NtGlobalFlagPEB另一个成员被称作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\WindowsNt\CurrentVersion\ImageFileExecutionOptions

HeapFlags由于NtGlobalFlag标志的设置,堆也会打开几个标志,这个变化可以在ntdll!RtlCreateHeap()里观测到。通常情况下为进程创建的第一个堆会将其Flags和ForceFlags4分别设为0x02(HEAP_GROWABLE)和0。然而当进程被调试时,这两个标志通常被设为0x50000062(取决于NtGlobalFlag)和0x40000060(等于FlagsAND0x6001007D)。默认情况下当一个被调试的进程创建堆时下列附加的堆标志将被设置:

HEAP_TAIL_CHECKING_ENABLED(0X20)

HEAP_FREE_CHECKING_ENABLED(0X40)

示例

下面的示例代码检查PEB.NtGlobalFlag是否等于0,为进程创建的第一个堆是否设置了附加标志(PEB.ProcessHeap):

;ebx=PEB

Mov ebx,[fs:0x30]



;CheckifPEB.NtGlobalFlag!=0

Cmp dword[ebx+0x68],0

jne .debugger_found



;eax=PEB.ProcessHeap

Mov eax,[ebx+0x18]



;CheckPEB.ProcessHeap.Flags

Cmp dword[eax+0x0c],2

jne .debugger_found



;CheckPEB.ProcessHeap.ForceFlags

Cmp dword[eax+0x10],0

jne .debugger_found

对策

可以将PEB.NtGlobalFlag和PEB.HeapProcess标志补丁为进程未被调试时的相应值。下面是一个补丁上述标志的ollyscript示例:

Var peb

var patch_addr

var process_heap



//retrievePEBviaahardcodedTEBaddress(firstthread:0x7ffde000)

Mov peb,[7ffde000+30]



//patchPEB.NtGlobalFlag

Lea patch_addr,[peb+68]

mov [patch_addr],0



//patchPEB.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

同样地OllyAdvanced插件有设置PEB.NtGlobalFlag和PEB.ProcessHeap的选项。

2.3DebugPort: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返回值。

BOOLCheckRemoteDebuggerPresent(

HANDLE hProcess,

PBOOL pbDebuggerPresent

)

ntdll!NtQueryInformationProcess()有5个参数。为了检测调试器的存在,需要将ProcessInformationclass参数设为ProcessDebugPort(7):

NTSTATUSNTAPI NtQueryInformationProcess(

HANDLE ProcessHandle,

PROCESSINFOCLASS ProcessInformationClass,

PVOID ProcessInformation,

ULONG ProcessInformationLength,

PULONG ReturnLength

)

示例

下面的例子显示了如何调用CheckRemoteDebuggerPresent()和NtQueryInformationProcess()来检测当前进程是否被调试:

;usingKernel32!CheckRemoteDebuggerPresent()

lea eax,[.bDebuggerPresent]

push eax ;pbDebuggerPresent

push 0xffffffff ;hProcess

call [CheckRemoteDebuggerPresent]

cmp dword[.bDebuggerPresent],0

jne .debugger_found



;usingntdll!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



//setabreakpointhandler

eob bp_handler_NtQueryInformationProcess



//setabreakpointwhereNtQueryInformationProcessreturns

gpa "NtQueryInformationProcess","ntdll.dll"

find $RESULT,#C21400#//retn14

mov bp_NtQueryInformationProcess,$RESULT

bphws bp_NtQueryInformationProcess,"X"

run



bp_handler_NtQueryInformationProcess:



//ProcessInformationClass==ProcessDebugPort?

cmp [esp+8],7

jne bp_handler_NtQueryInformationProcess_continue



//patchProcessInformationto0

mov patch_addr,[esp+c]

mov [patch_addr],0



//clearbreakpoint

bphwc bp_NtQueryInformationProcess



bp_handler_NtQueryInformationProcess_continue:

run

OllyAdvanced插件有一个patchNtQueryInformationProcess()的选项,这个补丁涉及注入一段代码来操纵NtQueryInformationProcess()的返回值。

2.4DebuggerInterrupts

在调试器中步过INT3和INT1指令的时候,由于调试器通常会处理这些调试中断,所以异常处理例程默认情况下将不会被调用,DebuggerInterrupts就利用了这个事实。这样壳可以在异常处理例程中设置标志,通过INT指令后如果这些标志没有被设置则意味着进程正在被调试。另外,kernel32!DebugBreak()内部是调用了INT3来实现的,有些壳也会使用这个API。

示例

这个例子在异常处理例程中设置EAX的值为0xFFFFFFFF(通过CONTEXT6记录)以此来判断异常处理例程是否被调用:

;setexceptionhandler

push .exeception_handler

push dword[fs:0]

mov [fs:0],esp



;resetflag(EAX)invokeint3

xor eax,eax

int3



;restoreexceptionhandler

pop dword[fs:0]

add esp,4



;checkiftheflaghadbeenset

test eax,eax

je .debugger_found

:::

.exeception_handler:

;EAX=ContextRecord

mov eax,[esp+0xc]

;setflag(ContextRecord.EAX)

mov dword[eax+0xb0],0xffffffff

;setContextRecord.EIP

inc dword[eax+0xb8]

xor eax,eax

retn

对策

由于调试中断而导致执行停止时,在OllyDbg中识别出异常处理例程(通过视图->SEH链)并下断点,然后Shift+F9将调试中断/异常传递给异常处理例程,最终异常处理例程中的断点会断下来,这时就可以跟踪了。

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





2.5TimingChecks

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

示例

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

rdtsc

mov ecx,eax

mov ebx,edx



;...moreinstructions

nop

push eax

pop eax

nop

;...moreinstructions



;computedeltabetweenRDTSCinstructions

rdtsc



;Checkhighorderbits

cmp edx,ebx

ja .debugger_found

;Checkloworderbits

sub eax,ecx

cmp eax,0x200

ja .debugger_found

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

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

对策

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

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

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

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

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

2.6SeDebugPrivilege

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

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

示例

下面是SeDebugPrivilege检查的例子:

;queryforthePIDofCSRSS.EXE

call [CsrGetProcessId]



;trytoopentheCSRSS.EXEprocess

push eax

push FALSE

push PROCESS_QUERY_INFORMATION

call [OpenProcess]



;ifOpenProcess()wassuccessful,

;processisprobablybeingdebugged

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.7ParentProcess(检测父进程)

通常进程的父进程是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的情况下启动可执行程序时,这个调试器检查会引起误报。

对策

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

77E8D1C2>33C0xoreax,eax

77E8D1C4C3retn

77E8D1C583EC0Csubesp,0C

2.8DebugObject:NtQueryObject()

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

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

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

NTSTATUSNTAPINtQueryObject(

HANDLE ObjectHandle,

OBJECT_INFORMATION_CLASS ObjectInformationClass,

PVOID ObjectInformation,

ULONG Length,

PULONG ResultLength

)

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

typedefstruct_OBJECT_ALL_INFORMATION{

ULONG NumberOfObjectsTypes;

OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];

}

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

typedefstruct_OBJECT_TYPE_INFORMATION{

[00]UNICODE_STRING TypeName;

[08]ULONG TotalNumberofHandles;

[0C]ULONG TotalNumberofObjects;

...morefields...

}

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

对策

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

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

2.9DebuggerWindow

调试器窗口的存在标志着有调试器正在系统内运行。由于调试器创建的窗口拥有特定类名(OllyDbg的是OLLYDBG,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



.szWindowClassOllyDbgdb“OLLYDBG”,0

.szWindowClassWinDbgdb“WinDbgFrameClass”,0

对策

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

2.10DebuggerProcess

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

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

对策

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

2.11DeviceDrivers

检测内核模式的调试器是否活跃于系统中的典型技术是访问他们的设备驱动程序。该技术相当简单,仅涉及调用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



.szDeviceNameNticedb"\\.\NTICE",0

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

对策

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

2.12OllyDbg:GuardPages

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

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

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

示例

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

;setupexceptionhandler

push .exception_handle

push dword[fs:0]

mov [fs:0],esp



;allocatememory

push PAGE_READWRITE

push MEM_COMMIT

push 0x1000

push NULL

call [VirtualAlloc]

test eax,eax

jz .failed

mov [.pAllocatedMem],eax



;storeaRETNontheallocatedmemory

mov byte[eax],0xC3

;thensetthePAGE_GUARDattributeoftheallocatedmemory

lea eax,[.dwOldProtect]

push eax

push PAGE_EXECUTE_READ|PAGE_GUARD

push 0x1000

push dword[.pAllocatedMem]

call [VirtualProtect]



;setmarker(EAX)as0

xor eax,eax

;triggeraSTATUS_GUARD_PAGE_VIOLATIONexception

call [.pAllocatedMem]

;checkifmarkerhadnotbeenchanged(exceptionhandlernotcalled)

test eax,eax

je .debugger_found



.exception_handler

;EAX=CONTEXTrecord

mov eax,[esp+0xC]

;setmarker(CONTEXT.EAX)to0xFFFFFFFF

;tosignalthattheexceptionhandlerwascalled

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.1SoftwareBreakpointDetection

软件断点是通过修改目标地址代码为0xCC(INT3/BreakpointInterrupt)来设置的断点。壳通过在受保护的代码段和(或)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(byteXOR0x55==0x99)thenbreakpointfound

Where:0x99==0xCCXOR0x55

对策

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

3.2HardwareBreakpointDetection

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

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

示例

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

;setupexceptionhandler

push .exception_handler

push dword[fs:0]

mov [fs:0],esp



;eaxwillbe0xFFFFFFFFifhardwarebreakpointsareidentified

xor eax,eax



;throwanexception

mov dword[eax],0



;restoreexceptionhandler

pop dword[fs:0]

add esp,4



;testifEAXwasupdated(breakpointidentified)

test eax,eax

jnz .breakpoint_found

:::

.exception_handler

;EAX=CONTEXTrecord

mov eax,[esp+0xc]



;checkifDebugRegistersContext.Dr0-Dr3isnotzero

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

;setContext.EAXtosignalbreakpointfound

mov dword[eax+0xb0],0xFFFFFFFF



.exception_ret

;setContext.EIPuponreturn

add dword[eax+0xb8],6

xor eax,eax

retn

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

对策

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

3.3PatchingDetectionviaCodeChecksumCalculation

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

示例

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

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.1EncryptionandCompression

加密和压缩是最基本的反分析形式。它们初步设防,防止逆向分析人员直接在反编译器内加载受保护的程序然后没有任何困难地开始分析。

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

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

0040A07C LODSDWORDPTRDS:[ESI]

0040A07D XOREAX,EBX

0040A07F SUBEAX,12338CC3

0040A084 ROLEAX,10

0040A087 XOREAX,799F82D0

0040A08C STOSDWORDPTRES:[EDI]

0040A08D INCEBX

0040A08E LOOPDSHORT0040A07C;decryptionloop

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

00476056 MOVBH,BYTEPTRDS:[EAX]

00476058 INCESI

00476059 ADDBH,0BD

0047605C XORBH,CL

0047605E INCESI

0047605F DECEDX

00476060 MOVBYTEPTRDS:[EAX],BH

00476062 CLC

00476063 SHLEDI,CL

:::Moregarbagecode

00476079 INCEDX

0047607A DECEDX

0047607B DECEAX

0047607C JMPSHORT0047607E

0047607E DECECX

0047607F JNZ00476056;decryptionloop

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

0040C045 MOVCH,BYTEPTRDS:[EDI]

0040C047 ADDEDX,EBX

0040C049 XORCH,AL

0040C04B XORCH,0D9

0040C04E CLC

0040C04F MOVBYTEPTRDS:[EDI],CH

0040C051 XCHGAH,AH

0040C053 BTREDX,EDX

0040C056 MOVSXEBX,CL

:::Moregarbagecode

0040C067 SAREDX,CL

0040C06C NOP

0040C06D DECEDI

0040C06E DECEAX

0040C06F JMPSHORT0040C071

0040C071 JNZ0040C045;decryptionloop

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

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

对策

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

4.2GarbageCodeandCodePermutation

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

示例

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

0044A21A JMPSHORTsample.0044A21F

0044A21C XORDWORDPTRSS:[EBP],6E4858D

0044A223 INT23

0044A225 MOVESI,DWORDPTRSS:[ESP]

0044A228 MOVEBX,2C322FF0

0044A22D LEAEAX,DWORDPTRSS:[EBP+6EE5B321]

0044A233 LEAECXDWORDPTRDS:[ESI+543D583E]

0044A239 ADDEBP,742C0F15

0044A23F ADDDWORDPTRDS:[ESI],3CB3AA25

0044A245 XOREDI,7DAC77E3

0044A24B CMPEAX,ECX

0044A24D MOVEAX,5ACAC514

0044A252 JMPSHORTsample.0044A257

0044A254 XORDWORDPTRSS:[EBP],AAE47425

0044A25B PUSHES

0044A25C ADDEBP,5BAC5C22

0044A262 ADCECX,3D71198C

0044A268 SUBESI,-4

0044A26B ADCECX,3795A210

0044A271 DECEDI

0044A272 MOVEAX,2F57113F

0044A277 PUSHECX

0044A278 POPECX

0044A279 LEAEAX,DWORDPTRSS:[EBP+3402713D]

0044A27F EDCEDI

0044A280 XORDWORDPTRDS:[ESI],33B568E3

0044A286 LEAEBX,DWORDPTRDS:[EDI+57DEFEE2]

0044A28C DECEDI

0044A28D SUBEBX,7ECDAE21

0044A293 MOVEDI,185C5C6C

0044A298 MOVEAX,4713E635

0044A29D MOVEAX,4

0044A2A2 ADDESI,EAX

0044A2A4 MOVECX,1010272F

0044A2A9 MOVECX,7A49B614

0044A2AE CMPEAX,ECX

0044A2B0 NOTDWORDPTRDS:[ESI]

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

0044A225 MOVESI,DWORDPTRSS:[ESP]

0044A23F ADDDWORDPTRDS:[ESI],3CB3AA25

0044A268 SUBESI,-4

0044A280 XORDWORDPTRDS:[ESI],33B568E3

0044A29D MOVEAX,4

0044A2A2 ADDESI,EAX

0044A2B0 NOTDWORDPTRDS:[ESI]

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

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

mov eax,ebx

test eax,eax

转换成下列等价的指令:

push ebx

pop eax

or eax,eax

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

示例

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

004018A8 MOVECX,A104B412

004018AD PUSH004018C1

004018B2 RETN

004018B3 SHREDX,5

004018B6 ADDESI,EDX

004018B8 JMPSHORT004018BA

004018BA XOREDX,EDX

004018BC MOVEAX,DWORDPTRDS:[ESI]

004018BE STC

004018BF JBSHORT004018DE

004018C1 SUBECX,EBX

004018C3 MOVEDX,9A01AB1F

004018C8 MOVESI,DWORDPTRFS:[ECX]

004018CB LEAECXDWORDPTRDS:[EDX+FFFF7FF7]

004018D1 MOVEDX,600

004018D6 TESTECX,2B73

004018DC JMPSHORT004018B3

004018DE MOVESI,EAX

004018E0 MOVEAX,A35ABDE4

004018E5 MOVECX,FAD1203A

004018EA MOVEBX,51AD5EF2

004018EF DIVEBX

004018F1 ADDBX,44A5

004018F6 ADDESI,EAX

004018F8 MOVZXEDI,BYTEPTRDS:[ESI]

004018FB OREDI,EDI

004018FD JNZSHORT00401906

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

00401081 MOVEAX,DWORDPTRFS:[18]

00401087 MOVEAX,DWORDPTRDS:[EAX+30]

0040108A MOVZXEAX,BYTEPTRDS:[EAX+2]

0040108E TESTEAX,EAX

00401090 JNZSHORT00401099

对策

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

避免跟踪进入这些难懂的指令的方法之一是在壳最常用的API下断点(如:VirtualAlloc,VitualProtect,LoadLibrary,GetProcAddress等)并把这些API当作跟踪的标志。如果在这些跟踪标志之间出了错,这时候就对这一段代码进行详细的跟踪。另外,设置内存访问/写入断点也让逆向分析人员能有针对性地分析那些修改/访问受保护进程最有趣的部分的代码,而不是跟踪大量的代码最终却(很可能)发现是一个确定的例程。

最后,在VMWare中运行OllyDbg并不时地保存调试会话快照,这样一来逆向分析人员就可以回到某一个特定的跟踪状态。如果出了错,可以返回到某一特定的跟踪状态继续跟踪分析。

4.3Anti-Disassembly

用来困惑逆向分析人员的另一种方法就是混乱反编译输出。反-反编译是使通过静态分析理解二进制代码的过程大大复杂化的有效方式。如果结合垃圾代码和代码变形一起使用将会更具效果。

反-反编译技术的一个具体的例子是插入一个垃圾字节然后增加一个条件分支使执行跳转到垃圾字节(译者注:即我们常说的花指令)。但是这个分支的条件永远为FALSE。这样垃圾代码将永远不会被执行,但是反编译引擎会开始反编译垃圾字节的地址,最终导致不正确的反编译输出。

示例

这是一个加了一些反-反编译代码的简单PEB.BeingDebugged标志检查例子。高亮的行是主要指令,其余的是反-反编译代码。它用到了垃圾字节0xff并增加了用来迷惑反编译引擎的跳到垃圾字节的假的条件跳转。

;Anti-disassemblysequence#1

push .jmp_real_01

stc

jnc .jmp_fake_01

retn

.jmp_fake_01:

db 0xff

.jmp_real_01:

;--------------------------------

mov eax,dword[fs:0x18]



;Anti-disassemblysequence#2

push .jmp_real_02

clc

jc .jmp_fake_02

retn

.jmp_fake_02:

db 0xff

.jmp_real_02:

;--------------------------------

mov eax,dword[eax+0x30]

movzx eax,byte[eax+0x02]

test eax,eax

jnz .debugger_found

下面是WinDbg中的反汇编输出:

0040194A6854194000 PUSH0X401954

0040194FF9 STC

004019507301 JNBimage00400000+0x1953(00401953)

00401952C3 RET

00401953FF64A118 JMPDWORDPTR[ECX+0X18]

004019570000 ADD[EAX],AL

00401959006864 ADD[EAX+0X64],CH

0040195C194000 SBB[EAX],EAX

0040195FF8 CLC

004019607201 JBimage00400000+0x1963(00401963)

00401962C3 RET

00401963FF8B40300FB6 DECDWORDPTR[EBX+0XB60F3040]

0040196940 INCEAX

0040196A0285C0750731 ADDAL,[EBP+0X310775C0]

OllyDbg中的反汇编输出:

0040194A6854194000 PUSH00401954

0040194FF9 STC

004019507301 JNBSHORT00401953

00401952C3 RETN

00401953FF64A118 JMPDWORDPTRDS:[ECX+18]

004019570000 ADDBYTEPTRDS:[EAX],AL

00401959006864 ADDBYTEPTRDS:[EAX+0X64],CH

0040195C194000 SBBDWORDPTRDS:[EAX],EAX

0040195FF8 CLC

004019607201 JBSHORT00401963

00401962C3 RETN

00401963FF8B40300FB6 DECDWORDPTRDS:[EBX+B60F3040]

0040196940 INCEAX

0040196A0285C0750731 ADDAL,BYTEPTRSS:[EBP+310775C0]

最后IDAPro中的反汇编输出:

0040194A push(offsetloc_401953+1)

0040194F stc

00401950 jnbshortloc_401953

00401952 retn

00401953;------------------------------------------------------------------

00401953

00401953loc-401953: ;CODEXREF:sub_401946+A

00401953 ;DATAXREF:sub_401946+4

00401953 jmpdwordptr[ecx+18h]

00401953sub_401946 endp

00401953

00401953;------------------------------------------------------------------

00401957 db0

00401958 db0

00401959 db0

0040195A db68h;h

0040195B ddoffsetunk_401964

0040195F db0F8h;

00401960 db72h;r

00401961 db1

00401962 db0C3h;+

00401963 db0FFh

00401964unk_401964 db8Bh;i ;DATAXREF:text:0040195B

00401965 db40h;@

00401966 db30h;0

00401967 db0Fh

00401968 db0B6h;|

00401969 db40h;@

0040196A db2

0040196B db85h;

0040196C db0C0h;+

0040196D db75h;u

注意所有这三个反编译引擎/调试器是如何落入反-反编译陷阱的,分析这样的反汇编代码对于逆向分析人员来说是很不容易的。还有其它的几种干扰反编译引擎的手段,这只是一个例子。另外这些反-反编译代码可以编码成一个宏,这样汇编源码就清晰多了。

建议读者参考EldadEliam13的一本精彩的逆向书籍,里面包含了反-反编译的详细信息和其它一些逆向话题。

5调试器攻击技术

本节罗列了壳用来主动攻击调试器的技术,如果进程正在被调试那么执行会突然停止、断点将被禁用。和前面描述的技术类似,结合反-反编译技术隐藏起来使用效果会更佳。

5.1MisdirectionandStoppingExecutionviaExceptions

线性地跟踪能够让逆向分析人员容易理解并掌握代码的真正目的。因此壳使用一些技术使得跟踪代码不再是线性的且更加费时。

一个普遍使用的技巧是在脱壳的过程中抛出一些异常,通过抛出一些可捕获的异常,逆向分析人员必需熟悉异常发生的时候EIP指向何处,当异常处理例程执行完之后EIP又指向何处。

另外异常是壳用来反复停止脱壳代码执行的手段之一,因为当进程被调试时抛出异常,调试器会暂停脱壳代码的执行。

壳通常使用结构化异常处理(SEH)14作为异常处理的机制,然而新壳也开始使用向量化异常15。

示例

下面示例代码抛出溢出异常(通过INTO)产生错误,通过数轮循环后由ROL指令来修改溢出标志。但是由于溢出异常是一个陷阱异常,EIP将指向JMP指令。如果逆向分析人员使用OllyDbg并且没有将异常传递给进程(通过Shift+F7/F8/F9)而是继续步进,进程将会进入一个死循环。

;setupexceptionhandler

push .exception_handler

push dword[fs:0]

mov [fs:0],esp



;throwanexception

mov ecx,1

.loop:

rol ecx,1

into

jmp .loop



;restoreexceptionhandler

pop dword[fs:0]

add esp,4

:::

.exception_handler

;EAX=CONTEXTrecord

mov eax,[esp+0xc]

;set Context.EIPuponreturn

add dword[eax+0xb8],2

xor eax,eax

retn

壳通常会抛出违规访问(0xC0000005)、断点(0x80000003)和单步(0x80000004)异常。

对策

当壳使用可捕获的异常仅仅是为了执行不同的代码时,可以通过选项->调试选项->异常选项卡配置OllyDbg使得异常处理例程自动被调用。下面是异常处理配置对话框的屏幕截图。逆向分析人员也可以添加那些不能通过复选框选择的自定义的异常。

当壳在异常处理例程内部执行重要操作时,逆向分析人员可以在异常处理例程中下断,其地址可以在OllyDbg中通过视图->SEH链看到。然后Shift+F7/F8/F9将控制移交给异常处理例程。

5.2BlockingInput

为了防止逆向分析人员控制调试器,当脱壳主例程运行的时候,壳可以通过调用user32!BlockInput()API来阻断键盘和鼠标的输入。通过垃圾代码和反-反编译技术进行隐藏使用这种方法,如果逆向分析人员没有识别出来的话是很有效的。一旦生效系统看上去没有反应,只剩下逆向分析人员在那里莫名其妙。

典型的场景可能是逆向分析人员在GetProcAddress()内下断,然后运行脱壳代码直到被断下。但是跳过一段垃圾代码之后壳调用BlockInput()。当GetProcAddress()断点断下来后,逆向分析人员会突然困惑地发现无法控制调试器了,不知究竟发生了什么。

示例

BlockInput()需要一个boolean型的参数fBlockIt。如果这个参数是true,键盘和鼠标事件被阻断;如果是false,键盘和鼠标事件被解除阻断:

;Blockinput

push TRUE

call [BlockInput]



;...Unpackingcode...



;Unblockinput

push FALSE

call [BlockInput]

对策

幸好最简单的方法就是补丁BlockInput()使它直接返回。这是补丁user32!BlockInput()入口的ollyscript脚本:

gpa "BlockInput","user32.dll"

mov [$RESULT],#C20400#//retn4

OllyAdvanced插件同样有补BlockInput()的选项。另外,可以同时按CTRL+ALT+DELETE键手工解除阻断。

5.3ThreadHideFromDebugger

这项技术用到了常常被用来设置线程优先级的APIntdll!NtSetInformationThread(),不过这个API也能够用来防止调试事件被发往调试器。

NtSetInformationThread()的参数列表如下。要实现这一功能,ThreadHideFromDebugger(0x11)被当作ThreadInformationClass参数传递,ThreadHandle通常设为当前线程的句柄(0xFFFFFFFE):

NTSTATUSNTAPINtSetInformationThread(

HANDLE ThreadHandle,

THREAD_INFORMATION_CLASS ThreadInformaitonClass,

PVOID ThreadInformation,

ULONG ThreadInformationLength

);

ThreadHideFromDebugger内部设置内核结构ETHREAD16的HideThreadFromDebugger成员。一旦这个成员设置以后,主要用来向调试器发送事件的内核函数_DbgkpSendApiMessage()将不再被调用。

示例

调用NtSetInformationThread()的一个典型示例:

push 0 ;InformationLength

push NULL ;ThreadInformation

push ThreadHideFromDebugger ;0x11

push 0xfffffffe ;GetCurrentThread()

call [NtSetInformationThread]

对策

可以在ntdll!NtSetInformationThread()里下断,断下来后,逆向分析人员可以操纵EIP防止API调用到达内核,这些都可以通过ollyscript来自动完成。另外,OllyAdvanced插件也有补这个API的选项。补过之后一旦ThreadInformaitonClass参数为HideThreadFromDebugger,API将不再深入内核仅仅执行一个简单的返回。

5.4DisablingBreakpoints

另外一种攻击调试器的方法就是禁用断点。壳通过CONTEXT结构修改调试寄存器来禁用硬件断点。

示例

在这个示例中,通过传入异常处理例程的CONTEXT记录,调试寄存器被清空了。

;setupexceptionhandler

push .exception_handler

push dword[fs:0]

mov [fs:0],esp



;throwanexception

xor eax,eax

mov dword[eax],0



;restoreexceptionhandler

pop dword[fs:0]

add esp,4

:::



.exception_handler

;EAX=CONTEXTrecord

mov eax,[esp+0xc]



;ClearDebugRegisters:Context.Dr0-Dr3,Dr6,Dr7

mov dword[eax+0x04],0

mov dword[eax+0x08],0

mov dword[eax+0x0C],0

mov dword[eax+0x10],0

mov dword[eax+0x14],0

mov dword[eax+0x18],0



;setContext.EIPuponreturn

add dword[eax+0xb8],6

xor eax,eax

retn

对于软件断点,壳可以直接搜索INT3(0xCC)并用任意/随机的操作码加以替换。这样做以后,软件断点失效并且原始的指令将会被破坏。

对策

显然当硬件断点被检测以后可以用软件断点来代替,反之亦然。如果两者都被检测,可以试试OllyDbg的内存访问/写入断点功能。

5.5UnhandledExceptionFilter

MSDN文档声明当一个异常到达UnhandledExceptionFilter(kernel32!UnhandledExceptionFilter)并且程序没有被调试时,UnhandledExceptionFilter将会调用在kernel32!SetUnhandledExceptionFilter()API作为参数指定的高层exceptionFilter。壳利用了这一点,通过设置exceptionFilter然后抛出异常,如果程序被调试那么这个异常将会被调试器接收,否则,控制被移交到exceptionFilter运行得以继续。

示例

下面的示例中通过SetUnhandledExceptionFilter()设置了一个高层的exceptionFilter,然后抛出一个违规访问异常。如果进程被调试,调试器将收到两次异常通知,否则exceptionFilter将修改CONTEXT.EIP并继续执行。

;settheexceptionfilter

push .exception_filter

call [SetUnhandledExceptionFilter]

mov [.original_filter],eax



;throwanexception

xor eax,eax

mov dword[eax],0



;restoreexceptionfilter

push dword[.original_filter]

call [SetUnhandledExceptionFilter]



:::



.exception_filter:

;EAX=ExceptionInfo.ContextRecord

mov eax,[esp+4]

mov eax,[eax+4]



;setreturnEIPuponreturn

add dword[eax+0xb8],6



;returnEXCEPTION_CONTINUE_EXECUTION

mov eax,0xffffffff

retn

有些壳并不调用SetUnhandledExceptionFilter()而是直接通过kernel32!_BasepCurrentTopLevelFilter手工设置exceptionFilter,以防逆向分析人员在那个API上下断。

对策

有意思的是kernel32!UnhandledExceptionFilter()内部实现代码是使用ntdll!NtQueryInformationProcess(ProcessDebugPort)来确定进程是否被调试,从而决定是否调用已注册的exceptionFilter。因此,处理方法和DebugPort调试器检测技术相同。

5.6OllyDbg:OutputDebugString()FormatStringBug

这个调试器攻击手段只对OllyDbg有效。已知OllyDbg面对能导致崩溃或执行任意代码的格式化字符串漏洞是脆弱的,这个漏洞是由于向kernel32!OutputDebugString()传递了不当的字符串参数引起的。这个漏洞在当前OllyDbg(1.10)依然存在并且仍然没有打补丁。

示例

下面这个简单的示例将导致OllyDbg抛出违规访问异常或不可预期的终止。

push .szFormatString

call [OutputDebugStringA]

:::

.szFormatStringdb"%s%s",0

对策

可以通过补丁kernel32!OutputDebugStringA()入口使之直接返回来加以解决。

6.高级及其它技术

本节罗列了不属于前面任一分类的一些高级和其它的反逆向技术。

6.1ProcessInjection

进程注入已经成为某些壳的一个特点。脱壳代码打开一个选定的宿主进程(自身、explorer.exe、iexplorer.exe等)然后将脱壳后的程序注入到这个宿主进程。



下面是一个支持进程注入的壳的屏幕截图。



恶意代码利用壳的这个特点使它们能躲过一些防火墙,这些防火墙通过检查进程是否在获准进行外部网络连接的应用程序列表中而决定是否放行。

壳所采用的执行进程注入的一种方法如下:

1.向kernel32!CreateProcess()传递CREATE_SUSPENDED进程创建标志,将宿主进程作为一个挂起的子进程打开。这时一个初始化了的线程被创建并挂起,由于loader例程(ntdll!LrdInitializeThunk)还没有被调用,DLL还没有被载入。这个线程的上下文中包含PEB地址、宿主进程入口点信息的寄存器值被设置。

2.使用kernel32!GetThreadContext()获取子进程初始化线程的上下文。

3.通过CONTEXT.EBX获取子进程的PEB地址。

4.读PEB.ImageBase(PEB+0x8)获取子进程的映像基址。

5.将BaseAddress参数指向检索到的映像基址,调用ntdll!NtUnmapViewOfSection()来unmap子进程中的原始宿主映像。

6.脱壳代码使用kernel32!VirtualAllocEx()在子进程中分配一段内存,dwSize参数等于脱壳后程序的映像大小。

7.使用kernel32!WriteProcessMemory()将脱壳后的程序的PE头和每个节写入子进程。

8.将子进程的PEB.ImageBase更新以匹配脱壳后的程序映像基址。

9.通过kernel32!SetThreadContext()更新子进程初始化线程的上下文,将其中的CONTEXT.EAX设置为脱壳后程序的入口点。

10.通过kernel32!ResumeThread()恢复子进程的执行。

为了从入口点开始调试打开的子进程,逆向分析人员可以在WriteProcessMemory()中设置断点,当包含入口点的节被写入子进程的时候,将入口点代码补丁为”跳往自身”指令(0xEB0xFE)。当子进程的主线程被恢复,子进程将在入口点进入一个死循环。这时逆向分析人员就可以附加一个调试器到子进程,恢复被修改的指令,继续正常的调试。

6.2DebuggerBlocker

Armadillo壳引入了称之为DebuggerBlocker的功能,它可以阻止逆向分析人员将调试器附加到一个受保护的进程。这个保护是通过调用Windows提供的调试函数来实现的。

具体来说就是脱壳代码扮演一个调试器的角色(父进程),通过它打开、调试/控制包含脱壳后程序的子进程。



由于受保护的进程已经被调试,通过kernel32!DebugActiveProcess()来附加调试器将会失败,原因是相应的nativeAPIntdll!NtDebugActiveProcess()将返回STATUS_PORT_ALREADY_SET。NtDebugActiveProcess()的失败的根本原因在于内核结构EPROCESS的DebugPort成员已经被设置过了。

为了附加调试器到受保护的进程,好几个逆向论坛发布的解决方法是在父进程的上下文里调用dernel32!DebugActiveProcessStop()。可以通过附加调试器到父进程,在kernel32!WaitForDebugEvent()内部下断,断下来后,注入一段调用DebugActiveProcessStop(childProcessID)的代码并执行,一旦调用成功,这时就可以附加调试器到受保护的进程了。

6.3TLSCallbacks

另一个被壳使用的技术就是在实际的入口点代码执行之前执行代码,这是通过使用ThreadLocalStorage(TLS)回调函数来实现的。壳通过这些回调函数执行调试器检测及解密例程,这样逆向分析人员将无法跟踪这些例程。

TLS回调可以使用诸如pedump之类的PE文件分析工具来识别。如果可执行文件中存在TLS条目,数据条目将会显示出来。

Datadirectory

EXPORT rva:00000000 size:00000000

IMPORT rva:00061000 size:000000E0

:::

TLS rva:000610E0 size:00000018

:::

IAT rva:00000000 size:00000000

DELAY_IMPORT rva:00000000 size:00000000

COM_DESCRPTR rva:00000000 size:00000000

unused rva:00000000 size:00000000

接着显示TLS条目的实际内容。AddressOfCallBacks成员指向一个以null结尾的回调函数数组。

TLSdirectory:

StartAddressOfRawData: 00000000

EndAddressOfRawData: 00000000

AddressOfIndex: 004610F8

AddressOfCallBacks: 004610FC

SizeOfZeroFill: 00000000

Characteristics: 00000000

在这个例子中,RVA0x4610fc指向回调函数指针(0x490f43和0x44654e):



默认情况下OllyDbg载入这个例子将会暂停在入口点。由于TLS回调函数是在实际的入口点执行之前被调用的,OllyDbg应该配置一下使其在TLS回调被调用之前中断在实际的loader。

可以通过选择选项->调试选项->事件->第一次中断于->系统断点来设置中断于ntdll.dll内的实际loader代码。



这样设置以后,OllyDbg将会中断在位于执行TLS回调的ntdll!LdrpRunInitializeRoutines()之前的ntdll!_LdrpInitializeProcess(),这时就可以在回调例程中下断并跟踪了。

关于PE文件格式的更多信息及包括pedump的二进制/源码可以在如下的链接获得:

AnIn-DepthLookintotheWin32PortableExecutableFileFormatbyMattPietrek

http://msdn.microsoft.com/msdnmag/issues/02/02/PE/default.aspx

AnIn-DepthLookintotheWin32PortableExecutableFileFormat,Part2byMattPietrek

http://msdn.microsoft.com/msdnmag/issues/02/03/PE2/

最新版本的微软PE文件格式可以通过如下链接获得:

MicrosoftPortableExecutableandCommonObjectFileFormatSpecification

http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx

6.4StolenBytes

代码抽取基本上就是壳移走受保护程序的一部分(通常是入口点的少量指令),这部分指令被复制并在分配的内存中执行。这在某种程度上保护了程序,因为如果从内存中dump受保护进程,被抽取的指令将不会被恢复。

这是一个可执行文件的原始入口点代码:

004011CB MOVEAX,DWORDPTRFS:[0]

004011D1 PUSHEBP

004011D2 MOVEBP,ESP

004011D4 PUSH-1

004011D6 PUSH0047401C

004011DB PUSH0040109A

004011E0 PUSHEAX

004011E1 MOVDWORDPTRFS:[0],ESP

004011E8 SUBESP,10

004011EB PUSHEBX

004011EC PUSHESI

004011ED PUSHEDI

下面是被Enigma加密壳偷取了前两个指令的同一段代码:

004011CB POPEBX

004011CC CMPEBX,EBX

004011CE DECESP

004011CF POPES

004011D0 JECXZSHORT00401169

004011D2 MOVEBP,ESP

004011D4 PUSH-1

004011D6 PUSH0047401C

004011DB PUSH0040109A

004011E0 PUSHEAX

004011E1 MOVDWORDPTRFS:[0],ESP

004011E8 SUBESP,10

004011EB PUSHEBX

004011EC PUSHESI

004011ED PUSHEDI

这是被ASProtect壳偷取了数条指令的相同例子。它增加了一条jump指令,指向内存中一段执行被偷代码的过程,被偷的指令和垃圾代码搀杂在一起,想要恢复被偷的代码困难重重。

004011CB JMP00B70361

004011D0 JNOSHORT00401198

004011D3 INCEBX

004011D4 ADCAL,0B3

004011D6 JLSHORT00401196

004011D8 INT1

004011D9 LAHF

004011DA PUSHFD

004011DB MOVEBX,1D0F0294

004011E0 PUSHES

004011E1 MOVEBX,A732F973

004011E6 ADCBYTEPTRDS:[EDX-E],CH

004011E9 MOVECX,EBP

004011EB DAS

004011EC DAA

004011ED ANDDWORDPTRDS:[EBX+58BA76D7],ECX

6.5APIRedirection

API重定向是用来防止逆向分析人员轻易重建受保护程序输入表的一种方法。原始的输入表被销毁,对API的调用被重定向到位于内存中的例程,然后由这些例程负责调用实际的API。

在这个例子中代码调用了kernel32!CopyFileA()API:

00404F05 LEAEDI,DWORDPTRSS:[EBP-20C]

00404FOB PUSHEDI

00404FOC PUSHDWORDPTRSS:[EBP-210]

00404F12 CALL

被调用的代码是一个JMP指令,跳转到输入表中的函数地址。

004056B8 JMPDWORDPTRDS:[<&KERNEL32.CopyFileA>]

然而当ASProtect壳重定向KERNEL32!CopyFileA()API时,这段代码被修改为一个call指令,调用壳自己分配的内存中的过程。

004056B8 CALL00D90000

下图说明了被偷的指令是如何被安置的。前7条KERNEL32!CopyFileA()代码中的指令被复制过来,另外0x7C83005ECall指令指向的代码也被复制过来。通过一个RETN指令,将控制移交回kernel32.dll领空KERNEL32!CopyFileA()中间的0x7C830063地址处:



有些壳则更进一步将整个DLL映像载入到一段分配的内存中,然后重定向API调用到这些DLL映像的拷贝。这个技术使得在实际的API中下断点变难了。

6.6Multi-ThreadedPackers

对于多线程壳,另一个线程常常用于执行一些诸如解密受保护程序这样必需的操作。多线程壳复杂度增加了,由于跟踪代码变得复杂,理解代码的难度也大大增加了。

PECrypt是一款多线程壳壳,它用第2个线程来解密数据,然后这些数据被主线程使用,这些线程之间通过事件对象进行同步。

PECrypt壳操作并同步线程:



6.7VirtualMachines

使用虚拟机的概念很简单:逆向分析人员最终会想出如何躲过/解决反调试和反逆向技术,当受保护的程序最终需要在内存中解密并执行时,面对静态分析就显得脆弱不堪了。

随着虚拟机的出现,受保护部分的代码被转换成了p-code,p-code在执行时可以转换成机器码。原始的机器指令被替换,理解代码所作所为的复杂度成指数上升。

下面是这个概念的简单图示:



像Oreanstechnologies的CodeVirtualizer和StraForce这些最新的壳都应用了虚拟机的概念来保护程序。

对付虚拟机需要分析p-code是如果组织并被虚拟机转换的,尽管这一切并不简单。获得足够的信息之后,就可以开发一款反编译引擎来分析P-code并将它们转换成机器码或者是可理解的指令。

一个开发p-code反编译引擎的例子和虚拟机实现的详细信息可以通过如下链接获得:

DefeatingHyperUnpackMe2WithanIDAProcessorModule,RolfRollesIII

http://www.openrce.org/articles/full_view/28

7.工具

本节列举了逆向分析人员和恶意代码分析人员可以用来分析、脱壳的公开可用的工具。

免责声明:这些都是第三方工具,笔者对这些工具可能导致的系统不稳定和可能影响系统的其他问题不负任何责任。建议总是在测试或恶意代码分析环境中运行这些工具。

7.1OllyDbg

http://www.ollydbg.de/

逆向分析人员和恶意代码分析人员使用的一款强大Ring3调试器。它的插件功能允许其他的逆向分析人员创建更多的插件,使得逆向和脱壳变得越来越简单。



7.2Ollyscript

http://www.openrce.org/downloads/details/106/OllyScript

一个OllyDbg的插件,允许通过使用类似于汇编语言的脚本实现自动设置/处理断点、补丁代码/数据等。在执行重复性的工作或者是自动脱壳是尤其有用。



7.3OllyAdvanced

http://www.openrce.org/downloads/details/241/Olly_Advanced

针对逆向分析人员如果说壳有盔甲的话,那么这个OllyDbg的插件就是逆向分析人员调试器的盔甲。它有很多选项用来躲过反调试技术,隐藏OllyDbg使其不被壳检测到。



7.4OllyDump

http://www.openrce.org/downloads/details/108/OllyDump

成功脱壳后,这个OllyDbg插件可以用来dump进程并且重建输入表。



7.5ImpRec

http://www.woodmann.com/crackz/Unpackers/Imprec16.zip

最后,这是另一款dump进程和重建输入表的工具。它是一款独立的工具,它提供了最出色的输入表重建能力。



8参考

书籍:逆向工程,软件保护

Reversing:SecretsofReverseEngineering.E.Eilam.Wiley,2005

CrackproofYourSoftware,P.Cerven.NoStarchPress,2002

书籍:Windows和处理器底层

MicrosoftWindowsInternal,4thEdition.M.Russinovich,D.Solomon,MicrosoftPress,

IA-32IntelArchitectureSoftwareDeveloper’sManual.Volume1-3,IntelCorporation,2006

链接:Windows底层

ReactOSProject

http://www.reactos.org/en/index.html

SourceSearch:http://www.reactos.org/generated/doxygen/

WineProject

http://www.winehq.org/

SourceSearch:http://source.winehq.org/source/

TheUndocumentedFunctions

http://undocumented.ntinternals.net

MSDN

http://msdn2.microsoft.com/en-us/default.aspx

链接:逆向工程,软件保护,脱壳

OpenRCE

http://www.openrce.org

OpenRCEAntiReverseEngineeringTechniquesDatabase

http://www.openrce.org/reference_library/anti_reversing

RCEForums

http://www.woodmann.com/forum/index.php

EXETOOLSForums

http://forum.exetools.com



献花(0)
+1
(本文系HKBXOIC首藏)