脱壳的艺术
Mark Vincent Yason
概述:脱壳是门艺术——脱壳既是一种心理挑战,同时也是逆向领域最为激动人心的智力游戏之一。为了甄别或解决非常难的反逆向技巧,逆向分析人员有时不得不了解操作系统的一些底层知识,聪明和耐心也是成功脱壳的关键。这个挑战既牵涉到壳的创建者,也牵涉到那些决心躲过这些保护的脱壳者。
本文主要目的是介绍壳常用的反逆向技术,同时也探讨了可以用来躲过或禁用这些保护的技术及公开可用的工具。这些信息将使研究人员特别是恶意代码分析人员在分析加壳的恶意代码时能识别出这些技术,当这些反逆向技术阻碍其成功分析时能决定下一步的动作。第二个目的,这里介绍的信息也会被那些计划在软件中添加一些保护措施用来减缓逆向分析人员分析其受保护代码的速度的研究人员用到。当然没有什么能使一个熟练的、消息灵通的、坚定的逆向分析人员止步的。
关键词:逆向工程、壳、保护、反调试、反逆向
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内部设置内核结构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()入口使之直接返回来加以解决。