textbox

IT博客 联系 聚合 管理
  103 Posts :: 7 Stories :: 22 Comments :: 0 Trackbacks

脱壳的艺术

Mark Vincent Yason

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

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

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

1 简介                                                                         

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

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

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

本文主要关注于介绍壳所使用的反逆向技术,同时也探讨了躲过 / 禁用这些保护措施的工具及技术。可能有些壳通过抓取进程映像( 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 ForceFlags 4 分别设为 0x02 HEAP_GROWABLE) 0 。然而当进程被调试时,这两个标志通常被设为 0x50000062 (取决于 NtGlobalFlag )和 0x40000060 (等于 Flags AND 0x6001007D )。默认情况下当一个被调试的进程创建堆时下列附加的堆标志将被设置:

HEAP_TAIL_CHECKING_ENABLED(0X20)

HEAP_FREE_CHECKING_ENABLED(0X40)

示例

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

;ebx = PEB

Mov                      ebx,[fs:0x30]

 

;Check if PEB.NtGlobalFlag != 0

Cmp                     dword [ebx+0x68],0

jne                        .debugger_found

 

;eax = PEB.ProcessHeap

Mov                      eax,[ebx+0x18]

 

;Check PEB.ProcessHeap.Flags

Cmp                     dword [eax+0x0c],2

jne                        .debugger_found

 

;Check PEB.ProcessHeap.ForceFlags

Cmp                     dword [eax+0x10],0

jne                        .debugger_found

对策

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

Var                        peb

var                        patch_addr

var                        process_heap

 

//retrieve PEB via a hardcoded TEB address( first thread: 0x7ffde000)

Mov                      peb,[7ffde000+30]

 

//patch PEB.NtGlobalFlag

Lea                       patch_addr,[peb+68]

mov                      [patch_addr],0

 

//patch PEB.ProcessHeap.Flags/ForceFlags

Mov                      process_heap,[peb+18]

lea                        patch_addr,[process_heap+0c]

mov                      [patch_addr],2

lea                        patch_addr,[process_heap+10]

mov                      [patch_addr],0

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

2.3 DebugPort: CheckRemoteDebuggerPresent()/NtQueryInformationProcess()

Kernel32!CheckRemoteDebuggerPresent() 是另一个可以用于确定是否有调试器被附加到进程的 API 。这个 API 内部调用了 ntdll!NtQueryInformationProcess() ,调用时 ProcessInformationclass 参数为 ProcessDebugPort(7) 。而 NtQueryInformationProcess() 检索内核结构 EPROCESS5 DebugPort 成员。非 0 DebugPort 成员意味着进程正在被用户模式的调试器调试。如果是这样的话, ProcessInformation 将被置为 0xFFFFFFFF ,否则 ProcessInformation 将被置为 0

Kernel32!CheckRemoteDebuggerPresent() 接受 2 个参数,第 1 个参数是进程句柄,第 2 个参数是一个指向 boolean 变量的指针,如果进程被调试,该变量将包含 TRUE 返回值。

BOOL CheckRemoteDebuggerPresent(

  HANDLE   hProcess,

  PBOOL    pbDebuggerPresent

)

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

NTSTATUS   NTAPI           NtQueryInformationProcess(

HANDLE                                     ProcessHandle,

PROCESSINFOCLASS        ProcessInformationClass,

PVOID                                ProcessInformation,

ULONG                                      ProcessInformationLength,

PULONG                            ReturnLength

)

示例

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

; using Kernel32!CheckRemoteDebuggerPresent()

lea                       eax,[.bDebuggerPresent]

push                     eax                                   ;pbDebuggerPresent

push     0xffffffff                                           ;hProcess

call                       [CheckRemoteDebuggerPresent]

cmp            dword [.bDebuggerPresent],0

jne                       .debugger_found

 

; using ntdll!NtQueryInformationProcess(ProcessDebugPort)

lea                       eax,[.dwReturnLen]

push                     eax                                   ;ReturnLength

push                     4                                      ;ProcessInformationLength

lea                       eax,[.dwDebugPort]

push                     eax                                   ;ProcessInformation

push                     ProcessDebugPort              ;ProcessInformationClass(7)

push                     0xffffffff                           ;ProcessHandle

call                       [NtQueryInformationProcess]

cmp            dword [.dwDebugPort],0

jne                       .debugger_found

对策

一种方法是在 NtQueryInformationProcess() 返回的地方设置断点,当这个断点被断下来后,将 ProcessInformation 补丁为 0 下面是自动执行这个方法的 ollyscript 示例:

var                        bp_NtQueryInformationProcess

 

// set a breakpoint handler

eob                      bp_handler_NtQueryInformationProcess

 

// set a breakpoint where NtQueryInformationProcess returns

gpa                      "NtQueryInformationProcess","ntdll.dll"

find              $RESULT,#C21400#  //retn 14

mov             bp_NtQueryInformationProcess,$RESULT

bphws          bp_NtQueryInformationProcess,"X"

run

 

bp_handler_NtQueryInformationProcess:

 

//ProcessInformationClass == ProcessDebugPort?

cmp                    [esp+8],7

jne                       bp_handler_NtQueryInformationProcess_continue

 

//patch ProcessInformation to 0

mov             patch_addr,[esp+c]

mov             [patch_addr],0

 

// clear breakpoint

bphwc          bp_NtQueryInformationProcess

 

bp_handler_NtQueryInformationProcess_continue:

run

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

2.4 Debugger Interrupts

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

示例

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

; set exception handler

push             .exeception_handler

push             dword [fs:0]

mov             [fs:0],esp

 

;reset flag(EAX) invoke int3

xor               eax,eax

int3

 

;restore exception handler

pop              dword [fs:0]

add              esp,4

 

; check if the flag had been set

test              eax,eax

je                 .debugger_found

:::

.exeception_handler:

;EAX = ContextRecord

mov             eax,[esp+0xc]

;set flag (ContextRecord.EAX)

mov             dword [eax+0xb0],0xffffffff

;set ContextRecord.EIP

inc               dword [eax+0xb8]

xor               eax,eax

retn

对策

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

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

 

 

2.5 Timing Checks

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

示例

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

rdtsc

mov             ecx,eax

mov             ebx,edx

 

;...more instructions

nop

push             eax

pop              eax

nop

;...more instructions

 

;compute delta between RDTSC instructions

rdtsc

 

;Check high order bits

cmp             edx,ebx

ja                 .debugger_found

;Check low order bits

sub              eax,ecx

cmp             eax,0x200

ja                 .debugger_found

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

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

对策

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

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

1 设置控制寄存器 CR4 8 中的时间戳禁止位( 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()

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

逆向论坛中讨论的一个有趣的方法就是检查 DebugObject 10 类型内核对象的数量。这种方法之所以有效是因为每当一个应用程序被调试的时候,将会为调试对话在内核中创建一个 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 操作的简单的解密例程。

0040A 07C    LODS DWORD PTR DS:[ESI]

0040A 07D    XOR EAX,EBX

0040A 07F     SUB EAX,12338CC3

0040A 084     ROL EAX,10

0040A 087     XOR EAX,799F82D0

0040A 08C    STOS DWORD PTR ES:[EDI]

0040A 08D    INC EBX

0040A 08E     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

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

0040C 045     MOV CH,BYTE PTR DS:[EDI]

0040C 047     ADD EDX,EBX

0040C 049     XOR CH,AL

0040C 04B     XOR CH,0D9

0040C 04E     CLC

0040C 04F     MOV BYTE PTR DS:[EDI],CH

0040C 051     XCHG AH,AH

0040C 053     BTR EDX,EDX

0040C 056     MOVSX EBX,CL

::: More garbage code

0040C 067     SAR EDX,CL

0040C 06C    NOP

0040C 06D    DEC EDI

0040C 06E     DEC EAX

0040C 06F     JMP SHORT 0040C071

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

示例

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

0044A 21A     JMP SHORT sample.0044A21F

0044A 21C    XOR DWORD PTR SS:[EBP],6E4858D

0044A 223     INT 23

0044A 225     MOV ESI,DWORD PTR SS:[ESP]

0044A 228     MOV EBX,2C322FF0

0044A 22D             LEA EAX,DWORD PTR SS:[EBP+6EE5B321]

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

0044A 239     ADD EBP,742C0F15

0044A 23F     ADD DWORD PTR DS:[ESI],3CB3AA25

0044A 245     XOR EDI,7DAC77E3

0044A 24B     CMP EAX,ECX

0044A 24D    MOV EAX,5ACAC514

0044A 252     JMP SHORT sample.0044A257

0044A 254     XOR DWORD PTR SS:[EBP],AAE47425

0044A 25B     PUSH ES

0044A 25C    ADD EBP,5BAC5C22

0044A 262              ADC ECX,3D71198C

0044A 268     SUB ESI,-4

0044A 26B     ADC ECX,3795A210

0044A 271     DEC EDI

0044A 272     MOV EAX,2F57113F

0044A 277     PUSH ECX

0044A 278     POP ECX

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

0044A 27F     EDC EDI

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

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

0044A 28C    DEC EDI

0044A 28D    SUB EBX,7ECDAE21

0044A 293     MOV EDI,185C5C6C

0044A 298     MOV EAX,4713E635

0044A 29D    MOV EAX,4

0044A 2A 2    ADD ESI,EAX

0044A 2A 4    MOV ECX,1010272F

0044A 2A 9    MOV ECX,7A49B614

0044A 2AE    CMP EAX,ECX

0044A 2B0     NOT DWORD PTR DS:[ESI]

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

0044A 225     MOV ESI,DWORD PTR SS:[ESP]

0044A 23F     ADD DWORD PTR DS:[ESI],3CB3AA25

0044A 268     SUB ESI,-4

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

0044A 29D    MOV EAX,4

0044A 2A 2    ADD ESI,EAX

0044A 2B0     NOT DWORD PTR DS:[ESI]

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

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

mov             eax,ebx

test              eax,eax

转换成下列等价的指令:

push             ebx

pop              eax

or                eax,eax

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

示例

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

004018A 8     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

004018C 1     SUB ECX,EBX

004018C 3     MOV EDX,9A01AB1F

004018C 8     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

004018F 1             ADD BX,44A5

004018F 6             ADD ESI,EAX

004018F 8             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 C 3                         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 C 3                         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 C 3                         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 C 3                         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 Eliam 13 的一本精彩的逆向书籍,里面包含了反 - 反编译的详细信息和其它一些逆向话题。

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_CLASSThreadInformaitonClass,

PVOID                                                         ThreadInformation,

ULONG                                                        ThreadInformationLength

);

ThreadHideFromDebugger 内部设置内核结构 ETHREAD 16 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

00404F 05              LEA EDI,DWORD PTR SS:[EBP-20C]

00404FOB             PUSH EDI

00404FOC             PUSH DWORD PTR SS:[EBP-210]

00404F 12              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, 4 th 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

posted on 2010-04-06 09:14 零度 阅读(1447) 评论(0)  编辑 收藏 引用 所属分类: 调试/壳
只有注册用户登录后才能发表评论。