脱壳的艺术php
Mark Vincent Yasonhtml
概述:脱壳是门艺术——脱壳既是一种心理挑战,同时也是逆向领域最为激动人心的智力游戏之一。为了甄别或解决很是难的反逆向技巧,逆向分析人员有时不得不了解操做系统的一些底层知识,聪明和耐心也是成功脱壳的关键。这个挑战既牵涉到壳的建立者,也牵涉到那些决心躲过这些保护的脱壳者。react
本文主要目的是介绍壳经常使用的反逆向技术,同时也探讨了能够用来躲过或禁用这些保护的技术及公开可用的工具。这些信息将使研究人员特别是恶意代码分析人员在分析加壳的恶意代码时能识别出这些技术,当这些反逆向技术阻碍其成功分析时能决定下一步的动做。第二个目的,这里介绍的信息也会被那些计划在软件中添加一些保护措施用来减缓逆向分析人员分析其受保护代码的速度的研究人员用到。固然没有什么能使一个熟练的、消息灵通的、坚决的逆向分析人员止步的。算法
关键词:逆向工程、壳、保护、反调试、反逆向数组
1简介 sass
在逆向工程领域,壳是最有趣的谜题之一。在解谜的过程当中,逆向分析人员会得到许多关于系统底层、逆向技巧等知识。安全
壳(这个术语在本文中既指压缩壳也包括加密壳)是用来防止程序被分析的。它们被商业软件合法地用于防止信息披露、篡改及盗版。惋惜恶意软件也基于一样的理由在使用壳,只不过动机不良。网络
因为大量恶意软件存在加壳现象,研究人员和恶意代码分析人员为了分析代码,开始学习脱壳的技巧。可是随着时间的推移,为防止逆向分析人员分析受保护的程序并成功脱壳,新的反逆向技术也被不断地添加到壳中。而且战斗还在继续,新的反逆向技术被开发的同时逆向分析人员也在针锋相对地发掘技巧、研究技术并开发工具来对付它们。数据结构
本文主要关注于介绍壳所使用的反逆向技术,同时也探讨了躲过/禁用这些保护措施的工具及技术。可能有些壳经过抓取进程映像(dump)可以轻易被搞定,这时处理反逆向技术彷佛没有必要,可是有些状况下加密壳的代码须要加以跟踪和分析,例如:多线程
须要躲过部分加密壳代码以便抓取进程映像、让输入表重建工具正确地工做。
深刻分析加密壳代码以便在一个反病毒产品中整合进脱壳支持。
此外,当反逆向技术被恶意程序直接应用,以防止跟踪并分析其恶意行为时,熟悉反逆向技术也是颇有价值的。
本文毫不是一个完整的反逆向技术的清单,由于它只涵盖了壳中经常使用的、有趣的一些技术。建议读者参阅最后一节的连接和图书资料,以了解更多其余逆向及反逆向的技术。
笔者但愿您以为这些材料有用,并能应用其中的技术。脱壳快乐!
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和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_INFORMATION
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_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的是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
.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会在设备名称后附加数字致使这种检查失败,逆向论坛中相关的描述是穷举附加的数字直到发现正确的设备名称。新版壳也用设备驱动检测技术检测诸如Regmon和Filemon之类的系统监视程序的存在。
对策
一种简单的方法就是在kernel32!CreateFileW()内设置断点,断下来后,要么操纵FileName参数要么改变其返回值为INVALID_HANDLE_VALUE(0xFFFFFFFF)。
2.12 OllyDbg:Guard Pages
这个检查是针对OllyDbg的,由于它和OllyDbg的内存访问/写入断点特性相关。
除了硬件断点和软件断点外,OllyDbg容许设置一个内存访问/写入断点,这种类型的断点是经过页面保护11来实现的。简单地说,页面保护提供了当应用程序的某块内存被访问时得到通知这样一个途径。
页面保护是经过PAGE_GUARD页面保护修改符来设置的,若是访问的内存地址是受保护页面的一部分,将会产生一个STATUS_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 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便可。
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
对策
垃圾代码和代码变形是一种用来考验耐心和浪费逆向分析人员的时间的方式。所以,重要的是知道这些混淆技术背后隐藏的指令是否值得去理解(是否是仅仅执行解密、壳的初始化等动做)。
避免跟踪进入这些难懂的指令的方法之一是在壳最经常使用的API下断点(如:VirtualAlloc,VitualProtect,LoadLibrary,GetProcAddress等)并把这些API看成跟踪的标志。若是在这些跟踪标志之间出了错,这时候就对这一段代码进行详细的跟踪。另外,设置内存访问/写入断点也让逆向分析人员能有针对性地分析那些修改/访问受保护进程最有趣的部分的代码,而不是跟踪大量的代码最终却(极可能)发现是一个肯定的例程。
最后,在VMWare中运行OllyDbg并不时地保存调试会话快照,这样一来逆向分析人员就能够回到某一个特定的跟踪状态。若是出了错,能够返回到某一特定的跟踪状态继续跟踪分析。
4.3 Anti-Disassembly
用来困惑逆向分析人员的另外一种方法就是混乱反编译输出。反-反编译是使经过静态分析理解二进制代码的过程大大复杂化的有效方式。若是结合垃圾代码和代码变形一块儿使用将会更具效果。
反-反编译技术的一个具体的例子是插入一个垃圾字节而后增长一个条件分支使执行跳转到垃圾字节(译者注:即咱们常说的花指令)。可是这个分支的条件永远为FALSE。这样垃圾代码将永远不会被执行,可是反编译引擎会开始反编译垃圾字节的地址,最终致使不正确的反编译输出。
示例
这是一个加了一些反-反编译代码的简单PEB.BeingDebugged标志检查例子。高亮的行是主要指令,其他的是反-反编译代码。它用到了垃圾字节0xff并增长了用来迷惑反编译引擎的跳到垃圾字节的假的条件跳转。
;Anti-disassembly sequence #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-disassembly sequence #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中的反汇编输出:
0040194A 6854194000 PUSH 0X401954
0040194F F9 STC
00401950 7301 JNB image00400000+0x1953(00401953)
00401952 C3 RET
00401953 FF64A118 JMP DWORD PTR [ECX+0X18]
00401957 0000 ADD [EAX],AL
00401959 006864 ADD [EAX+0X64],CH
0040195C 194000 SBB [EAX],EAX
0040195F F8 CLC
00401960 7201 JB image00400000+0x1963 (00401963)
00401962 C3 RET
00401963 FF8B40300FB6 DEC DWORD PTR [EBX+0XB60F3040]
00401969 40 INC EAX
0040196A 0285C0750731 ADD AL,[EBP+0X310775C0]
OllyDbg中的反汇编输出:
0040194A 6854194000 PUSH 00401954
0040194F F9 STC
00401950 7301 JNB SHORT 00401953
00401952 C3 RETN
00401953 FF64A118 JMP DWORD PTR DS:[ECX+18]
00401957 0000 ADD BYTE PTR DS:[EAX],AL
00401959 006864 ADD BYTE PTR DS:[EAX+0X64],CH
0040195C 194000 SBB DWORD PTR DS:[EAX],EAX
0040195F F8 CLC
00401960 7201 JB SHORT 00401963
00401962 C3 RETN
00401963 FF8B40300FB6 DEC DWORD PTR DS:[EBX+B60F3040]
00401969 40 INC EAX
0040196A 0285C0750731 ADD AL,BYTE PTR SS:[EBP+310775C0]
最后IDAPro中的反汇编输出:
0040194A push (offset loc_401953+1)
0040194F stc
00401950 jnb short loc_401953
00401952 retn
00401953 ;------------------------------------------------------------------
00401953
00401953 loc-401953: ;CODE XREF: sub_401946+A
00401953 ;DATA XREF: sub_401946+4
00401953 jmp dword ptr [ecx+18h]
00401953 sub_401946 endp
00401953
00401953 ;------------------------------------------------------------------
00401957 db 0
00401958 db 0
00401959 db 0
0040195A db 68h; h
0040195B dd offset unk_401964
0040195F db 0F8h;
00401960 db 72h; r
00401961 db 1
00401962 db 0C3h;+
00401963 db 0FFh
00401964 unk_401964 db 8Bh; i ;DATA XREF: text:0040195B
00401965 db 40h; @
00401966 db 30h; 0
00401967 db 0Fh
00401968 db 0B6h;|
00401969 db 40h; @
0040196A db 2
0040196B db 85h;
0040196C db 0C0h;+
0040196D db 75h; u
注意全部这三个反编译引擎/调试器是如何落入反-反编译陷阱的,分析这样的反汇编代码对于逆向分析人员来讲是很不容易的。还有其它的几种干扰反编译引擎的手段,这只是一个例子。另外这些反-反编译代码能够编码成一个宏,这样汇编源码就清晰多了。
建议读者参考Eldad Eliam13的一本精彩的逆向书籍,里面包含了反-反编译的详细信息和其它一些逆向话题。
5 调试器攻击技术
本节罗列了壳用来主动攻击调试器的技术,若是进程正在被调试那么执行会忽然中止、断点将被禁用。和前面描述的技术相似,结合反-反编译技术隐藏起来使用效果会更佳。
5.1 Misdirection and Stopping Execution via Exceptions
线性地跟踪可以让逆向分析人员容易理解并掌握代码的真正目的。所以壳使用一些技术使得跟踪代码再也不是线性的且更加费时。
一个广泛使用的技巧是在脱壳的过程当中抛出一些异常,经过抛出一些可捕获的异常,逆向分析人员必需熟悉异常发生的时候EIP指向何处,当异常处理例程执行完以后EIP又指向何处。
另外异常是壳用来反复中止脱壳代码执行的手段之一,由于当进程被调试时抛出异常,调试器会暂停脱壳代码的执行。
壳一般使用结构化异常处理(SEH)14做为异常处理的机制,然而新壳也开始使用向量化异常15。
示例
下面示例代码抛出溢出异常(经过INTO)产生错误,经过数轮循环后由ROL指令来修改溢出标志。可是因为溢出异常是一个陷阱异常,EIP将指向JMP指令。若是逆向分析人员使用OllyDbg而且没有将异常传递给进程(经过Shift+F7/F8/F9)而是继续步进,进程将会进入一个死循环。
;set up exception handler
push .exception_handler
push dword [fs:0]
mov [fs:0],esp
;throw an exception
mov ecx,1
.loop:
rol ecx,1
into
jmp .loop
;restore exception handler
pop dword [fs:0]
add esp,4
:::
.exception_handler
;EAX = CONTEXT record
mov eax,[esp+0xc]
;set Context.EIP upon return
add dword [eax+0xb8],2
xor eax,eax
retn
壳一般会抛出违规访问(0xC0000005)、断点(0x80000003)和单步(0x80000004)异常。
对策
当壳使用可捕获的异常仅仅是为了执行不一样的代码时,能够经过选项-> 调试选项 -> 异常选项卡配置OllyDbg使得异常处理例程自动被调用。下面是异常处理配置对话框的屏幕截图。逆向分析人员也能够添加那些不能经过复选框选择的自定义的异常。
当壳在异常处理例程内部执行重要操做时,逆向分析人员能够在异常处理例程中下断,其地址能够在OllyDbg中经过视图->SEH链看到。而后Shift+F7/F8/F9将控制移交给异常处理例程。
5.2 Blocking Input
为了防止逆向分析人员控制调试器,当脱壳主例程运行的时候,壳能够经过调用user32!BlockInput() API 来阻断键盘和鼠标的输入。经过垃圾代码和反-反编译技术进行隐藏使用这种方法,若是逆向分析人员没有识别出来的话是颇有效的。一旦生效系统看上去没有反应,只剩下逆向分析人员在那里莫名其妙。
典型的场景多是逆向分析人员在GetProcAddress()内下断,而后运行脱壳代码直到被断下。可是跳过一段垃圾代码以后壳调用BlockInput()。当GetProcAddress()断点断下来后,逆向分析人员会忽然困惑地发现没法控制调试器了,不知究竟发生了什么。
示例
BlockInput()须要一个boolean型的参数fBlockIt。若是这个参数是true,键盘和鼠标事件被阻断;若是是false,键盘和鼠标事件被解除阻断:
; Block input
push TRUE
call [BlockInput]
;...Unpacking code...
;Unblock input
push FALSE
call [BlockInput]
对策
幸亏最简单的方法就是补丁 BlockInput()使它直接返回。这是补丁user32!BlockInput()入口的ollyscript脚本:
gpa "BlockInput","user32.dll"
mov [$RESULT],#C20400# //retn 4
Olly Advanced插件一样有补BlockInput()的选项。另外,能够同时按CTRL+ALT+DELETE键手工解除阻断。
5.3 ThreadHideFromDebugger
这项技术用到了经常被用来设置线程优先级的API ntdll!NtSetInformationThread(),不过这个API也可以用来防止调试事件被发往调试器。
NtSetInformationThread()的参数列表以下。要实现这一功能,ThreadHideFromDebugger(0x11)被看成ThreadInformationClass参数传递,ThreadHandle一般设为当前线程的句柄(0xFFFFFFFE):
NTSTATUS NTAPI NtSetInformationThread(
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来自动完成。另外,Olly Advanced插件也有补这个API的选项。补过以后一旦ThreadInformaitonClass参数为HideThreadFromDebugger,API将再也不深刻内核仅仅执行一个简单的返回。
5.4 Disabling Breakpoints
另一种攻击调试器的方法就是禁用断点。壳经过CONTEXT结构修改调试寄存器来禁用硬件断点。
示例
在这个示例中,经过传入异常处理例程的CONTEXT记录,调试寄存器被清空了。
;set up exception handler
push .exception_handler
push dword [fs:0]
mov [fs:0],esp
;throw an exception
xor eax,eax
mov dword [eax],0
;restore exception handler
pop dword [fs:0]
add esp,4
:::
.exception_handler
;EAX = CONTEXT record
mov eax,[esp+0xc]
;Clear Debug Registers: 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
;set Context.EIP upon return
add dword [eax+0xb8],6
xor eax,eax
retn
对于软件断点,壳能够直接搜索INT3(0xCC)并用任意/随机的操做码加以替换。这样作之后,软件断点失效而且原始的指令将会被破坏。
对策
显然当硬件断点被检测之后能够用软件断点来代替,反之亦然。若是二者都被检测,能够试试OllyDbg的内存访问/写入断点功能。
5.5 Unhandled Exception Filter
MSDN文档声明当一个异常到达Unhandled Exception Filter(kernel32!UnhandledExceptionFilter)而且程序没有被调试时,Unhandled Exception Filter将会调用在kernel32!SetUnhandledExceptionFilter()API做为参数指定的高层exception Filter。壳利用了这一点,经过设置exception Filter而后抛出异常,若是程序被调试那么这个异常将会被调试器接收,不然,控制被移交到exception Filter运行得以继续。
示例
下面的示例中经过SetUnhandledExceptionFilter()设置了一个高层的exception Filter,而后抛出一个违规访问异常。若是进程被调试,调试器将收到两次异常通知,不然exception Filter将修改CONTEXT.EIP并继续执行。
;set the exception filter
push .exception_filter
call [SetUnhandledExceptionFilter]
mov [.original_filter],eax
;throw an exception
xor eax,eax
mov dword [eax],0
;restore exception filter
push dword [.original_filter]
call [SetUnhandledExceptionFilter]
:::
.exception_filter:
;EAX = ExceptionInfo.ContextRecord
mov eax,[esp+4]
mov eax,[eax+4]
;set return EIP upon return
add dword [eax+0xb8],6
;return EXCEPTION_CONTINUE_EXECUTION
mov eax,0xffffffff
retn
有些壳并不调用SetUnhandledExceptionFilter()而是直接经过kernel32!_BasepCurrentTopLevelFilter手工设置exception Filter,以防逆向分析人员在那个API上下断。
对策
有意思的是kernel32!UnhandledExceptionFilter()内部实现代码是使用ntdll!NtQueryInformationProcess(ProcessDebugPort)来肯定进程是否被调试,从而决定是否调用已注册的exception Filter。所以,处理方法和DebugPort调试器检测技术相同。
5.6 OllyDbg:OutputDebugString() Format String Bug
这个调试器攻击手段只对OllyDbg有效。已知OllyDbg面对能致使崩溃或执行任意代码的格式化字符串漏洞是脆弱的,这个漏洞是因为向kernel32!OutputDebugString()传递了不当的字符串参数引发的。这个漏洞在当前OllyDbg(1.10)依然存在而且仍然没有打补丁。
示例
下面这个简单的示例将致使OllyDbg抛出违规访问异常或不可预期的终止。
push .szFormatString
call [OutputDebugStringA]
:::
.szFormatString db "%s%s",0
对策
能够经过补丁 kernel32!OutputDebugStringA()入口使之直接返回来加以解决。
6. 高级及其它技术
本节罗列了不属于前面任一分类的一些高级和其它的反逆向技术。
6.1 Process Injection
进程注入已经成为某些壳的一个特色。脱壳代码打开一个选定的宿主进程(自身、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.2 Debugger Blocker
Armadillo壳引入了称之为Debugger Blocker的功能,它能够阻止逆向分析人员将调试器附加到一个受保护的进程。这个保护是经过调用Windows提供的调试函数来实现的。
具体来讲就是脱壳代码扮演一个调试器的角色(父进程),经过它打开、调试/控制包含脱壳后程序的子进程。
因为受保护的进程已经被调试,经过kernel32!DebugActiveProcess()来附加调试器将会失败,缘由是相应的native API ntdll!NtDebugActiveProcess()将返回STATUS_PORT_ALREADY_SET。 NtDebugActiveProcess()的失败的根本缘由在于内核结构EPROCESS的DebugPort成员已经被设置过了。
为了附加调试器到受保护的进程,好几个逆向论坛发布的解决方法是在父进程的上下文里调用dernel32!DebugActiveProcessStop()。能够经过附加调试器到父进程,在kernel32!WaitForDebugEvent()内部下断,断下来后,注入一段调用DebugActiveProcessStop(childProcessID)的代码并执行,一旦调用成功,这时就能够附加调试器到受保护的进程了。
6.3 TLS Callbacks
另外一个被壳使用的技术就是在实际的入口点代码执行以前执行代码,这是经过使用Thread Local Storage (TLS)回调函数来实现的。壳经过这些回调函数执行调试器检测及解密例程,这样逆向分析人员将没法跟踪这些例程。
TLS回调可使用诸如pedump之类的PE文件分析工具来识别。若是可执行文件中存在TLS条目,数据条目将会显示出来。
Data directory
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结尾的回调函数数组。
TLS directory:
StartAddressOfRawData: 00000000
EndAddressOfRawData: 00000000
AddressOfIndex: 004610F8
AddressOfCallBacks: 004610FC
SizeOfZeroFill: 00000000
Characteristics: 00000000
在这个例子中,RVA 0x4610fc指向回调函数指针(0x490f43和0x44654e):
默认状况下OllyDbg载入这个例子将会暂停在入口点。因为TLS回调函数是在实际的入口点执行以前被调用的,OllyDbg应该配置一下使其在TLS回调被调用以前中断在实际的loader。
能够经过选择选项->调试选项->事件->第一次中断于->系统断点来设置中断于ntdll.dll内的实际loader代码。
这样设置之后,OllyDbg将会中断在位于执行TLS回调的ntdll!LdrpRunInitializeRoutines()以前的ntdll!_LdrpInitializeProcess(),这时就能够在回调例程中下断并跟踪了。
关于PE文件格式的更多信息及包括pedump的二进制/源码能够在以下的连接得到:
An In-Depth Look into the Win32 Portable Executable File Format by Matt Pietrek
http://msdn.microsoft.com/msdnmag/issues/02/02/PE/default.aspx
An In-Depth Look into the Win32 Portable Executable File Format,Part 2 by Matt Pietrek
http://msdn.microsoft.com/msdnmag/issues/02/03/PE2/
最新版本的微软PE文件格式能够经过以下连接得到:
Microsoft Portable Executable and Common Object File Format Specification
http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx
6.4 Stolen Bytes
代码抽取基本上就是壳移走受保护程序的一部分(一般是入口点的少许指令),这部分指令被复制并在分配的内存中执行。这在某种程度上保护了程序,由于若是从内存中dump受保护进程,被抽取的指令将不会被恢复。
这是一个可执行文件的原始入口点代码:
004011CB MOV EAX,DWORD PTR FS:[0]
004011D1 PUSH EBP
004011D2 MOV EBP,ESP
004011D4 PUSH -1
004011D6 PUSH 0047401C
004011DB PUSH 0040109A
004011E0 PUSH EAX
004011E1 MOV DWORD PTR FS:[0],ESP
004011E8 SUB ESP,10
004011EB PUSH EBX
004011EC PUSH ESI
004011ED PUSH EDI
下面是被Enigma加密壳偷取了前两个指令的同一段代码:
004011CB POP EBX
004011CC CMP EBX,EBX
004011CE DEC ESP
004011CF POP ES
004011D0 JECXZ SHORT 00401169
004011D2 MOV EBP,ESP
004011D4 PUSH -1
004011D6 PUSH 0047401C
004011DB PUSH 0040109A
004011E0 PUSH EAX
004011E1 MOV DWORD PTR FS:[0],ESP
004011E8 SUB ESP,10
004011EB PUSH EBX
004011EC PUSH ESI
004011ED PUSH EDI
这是被ASProtect壳偷取了数条指令的相同例子。它增长了一条jump指令,指向内存中一段执行被偷代码的过程,被偷的指令和垃圾代码搀杂在一块儿,想要恢复被偷的代码困难重重。
004011CB JMP 00B70361
004011D0 JNO SHORT 00401198
004011D3 INC EBX
004011D4 ADC AL,0B3
004011D6 JL SHORT 00401196
004011D8 INT1
004011D9 LAHF
004011DA PUSHFD
004011DB MOV EBX,1D0F0294
004011E0 PUSH ES
004011E1 MOV EBX,A732F973
004011E6 ADC BYTE PTR DS:[EDX-E],CH
004011E9 MOV ECX,EBP
004011EB DAS
004011EC DAA
004011ED AND DWORD PTR DS:[EBX+58BA76D7],ECX
6.5 API Redirection
API重定向是用来防止逆向分析人员轻易重建受保护程序输入表的一种方法。原始的输入表被销毁,对API的调用被重定向到位于内存中的例程,而后由这些例程负责调用实际的API。
在这个例子中代码调用了kernel32!CopyFileA() API:
00404F05 LEA EDI,DWORD PTR SS:[EBP-20C]
00404FOB PUSH EDI
00404FOC PUSH DWORD PTR SS:[EBP-210]
00404F12 CALL <JMP.&KERNEL32.CopyFileA>
被调用的代码是一个JMP指令,跳转到输入表中的函数地址。
004056B8 JMP DWORD PTR DS:[<&KERNEL32.CopyFileA>]
然而当ASProtect壳重定向KERNEL32!CopyFileA() API时,这段代码被修改成一个call指令,调用壳本身分配的内存中的过程。
004056B8 CALL 00D90000
下图说明了被偷的指令是如何被安置的。前7条KERNEL32!CopyFileA()代码中的指令被复制过来,另外0x7C83005E Call指令指向的代码也被复制过来。经过一个RETN指令,将控制移交回kernel32.dll领空KERNEL32!CopyFileA()中间的0x7C830063地址处:
有些壳则更进一步将整个DLL映像载入到一段分配的内存中,而后重定向API调用到这些DLL映像的拷贝。 这个技术使得在实际的API中下断点变难了。
6.6 Multi-Threaded Packers
对于多线程壳,另外一个线程经常用于执行一些诸如解密受保护程序这样必需的操做。多线程壳复杂度增长了,因为跟踪代码变得复杂,理解代码的难度也大大增长了。
PECrypt是一款多线程壳壳,它用第2个线程来解密数据,而后这些数据被主线程使用,这些线程之间经过事件对象进行同步。
PECrypt壳操做并同步线程:
6.7 Virtual Machines
使用虚拟机的概念很简单:逆向分析人员最终会想出如何躲过/解决反调试和反逆向技术,当受保护的程序最终须要在内存中解密并执行时,面对静态分析就显得脆弱不堪了。
随着虚拟机的出现,受保护部分的代码被转换成了p-code,p-code在执行时能够转换成机器码。原始的机器指令被替换,理解代码所做所为的复杂度成指数上升。
下面是这个概念的简单图示:
像Oreans technologies的CodeVirtualizer和StraForce这些最新的壳都应用了虚拟机的概念来保护程序。
对付虚拟机须要分析p-code是若是组织并被虚拟机转换的,尽管这一切并不简单。得到足够的信息以后,就能够开发一款反编译引擎来分析P-code并将它们转换成机器码或者是可理解的指令。
一个开发p-code反编译引擎的例子和虚拟机实现的详细信息能够经过以下连接得到:
Defeating HyperUnpackMe2 With an IDA Processor Module, Rolf Rolles III
http://www.openrce.org/articles/full_view/28
7. 工具
本节列举了逆向分析人员和恶意代码分析人员能够用来分析、脱壳的公开可用的工具。
免责声明:这些都是第三方工具,笔者对这些工具可能致使的系统不稳定和可能影响系统的其余问题不负任何责任。建议老是在测试或恶意代码分析环境中运行这些工具。
7.1 OllyDbg
http://www.ollydbg.de/
逆向分析人员和恶意代码分析人员使用的一款强大Ring3调试器。它的插件功能容许其余的逆向分析人员建立更多的插件,使得逆向和脱壳变得愈来愈简单。
7.2 Ollyscript
http://www.openrce.org/downloads/details/106/OllyScript
一个OllyDbg的插件,容许经过使用相似于汇编语言的脚本实现自动设置/处理断点、补丁代码/数据等。在执行重复性的工做或者是自动脱壳是尤为有用。
7.3 Olly Advanced
http://www.openrce.org/downloads/details/241/Olly_Advanced
针对逆向分析人员若是说壳有盔甲的话,那么这个OllyDbg的插件就是逆向分析人员调试器的盔甲。它有不少选项用来躲过反调试技术,隐藏OllyDbg使其不被壳检测到。
7.4 OllyDump
http://www.openrce.org/downloads/details/108/OllyDump
成功脱壳后,这个OllyDbg插件能够用来dump进程而且重建输入表。
7.5 ImpRec
http://www.woodmann.com/crackz/Unpackers/Imprec16.zip
最后,这是另外一款dump进程和重建输入表的工具。它是一款独立的工具,它提供了最出色的输入表重建能力。
8 参考
书籍:逆向工程,软件保护
Reversing: Secrets of Reverse Engineering. E.Eilam.Wiley, 2005
Crackproof Your Software , P.Cerven.No Starch Press, 2002
书籍:Windows和处理器底层
Microsoft Windows Internal, 4th Edition . M. Russinovich, D. Solomon, Microsoft Press,
IA-32 Intel Architecture Software Developer’s Manual. Volume 1-3, Intel Corporation, 2006
连接:Windows底层
ReactOS Project
http://www.reactos.org/en/index.html
Source Search: http://www.reactos.org/generated/doxygen/
Wine Project
http://www.winehq.org/
Source Search: http://source.winehq.org/source/
The Undocumented Functions
http://undocumented.ntinternals.net
MSDN
http://msdn2.microsoft.com/en-us/default.aspx
连接:逆向工程,软件保护,脱壳
OpenRCE
http://www.openrce.org
OpenRCE Anti Reverse Engineering Techniques Database
http://www.openrce.org/reference_library/anti_reversing
RCE Forums
http://www.woodmann.com/forum/index.php
EXETOOLS Forums
http://forum.exetools.com