更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
脱壳的艺术
Mark Vincent Yason
概述:脱壳是门艺术——脱壳既是一种心理挑战,同时也是逆向领域最为激动人心的智力游
戏之一。为了甄别或解决非常难的反逆向技巧,逆向
分析
定性数据统计分析pdf销售业绩分析模板建筑结构震害分析销售进度分析表京东商城竞争战略分析
人员有时不得不了解操作系统的一
些底层知识,聪明和耐心也是成功脱壳的关键。这个挑战既牵涉到壳的创建者,也牵涉到那
些决心躲过这些保护的脱壳者。
本文主要目的是介绍壳常用的反逆向技术,同时也探讨了可以用来躲过或禁用这些保护
的技术及公开可用的工具。这些信息将使研究人员特别是恶意代码分析人员在分析加壳的恶
意代码时能识别出这些技术,当这些反逆向技术阻碍其成功分析时能决定下一步的动作。第
二个目的,这里介绍的信息也会被那些计划在软件中添加一些保护措施用来减缓逆向分析人
员分析其受保护代码的速度的研究人员用到。当然没有什么能使一个熟练的、消息灵通的、
坚定的逆向分析人员止步的。
关键词:逆向工程、壳、保护、反调试、反逆向
1111简介
在逆向工程领域,壳是最有趣的谜题之一。在解谜的过程中,逆向分析人员会获得许多
关于系统底层、逆向技巧等知识。
壳(这个术语在本文中既指压缩壳也包括加密壳)是用来防止程序被分析的。它们被商
业软件合法地用于防止信息披露、篡改及盗版。可惜恶意软件也基于同样的理由在使用壳,
只不过动机不良。
由于大量恶意软件存在加壳现象,研究人员和恶意代码分析人员为了分析代码,开始学
习脱壳的技巧。但是随着时间的推移,为防止逆向分析人员分析受保护的程序并成功脱壳,
新的反逆向技术也被不断地添加到壳中。并且战斗还在继续,新的反逆向技术被开发的同时
逆向分析人员也在针锋相对地发掘技巧、研究技术并开发工具来对付它们。
本文主要关注于介绍壳所使用的反逆向技术,同时也探讨了躲过/禁用这些保护措施的工
具及技术。可能有些壳通过抓取进程映像(dump)能够轻易被搞定,这时处理反逆向技术
似乎没有必要,但是有些情况下加密壳的代码需要加以跟踪和分析,例如:
需要躲过部分加密壳代码以便抓取进程映像、让输入
表
关于同志近三年现实表现材料材料类招标技术评分表图表与交易pdf视力表打印pdf用图表说话 pdf
重建工具正确地工作。
深入分析加密壳代码以便在一个反病毒产品中整合进脱壳支持。
此外,当反逆向技术被恶意程序直接应用,以防止跟踪并分析其恶意行为时,熟悉反逆
向技术也是很有价值的。
本文绝不是一个完整的反逆向技术的清单,因为它只涵盖了壳中常用的、有趣的一些技
术。建议读者参阅最后一节的链接和图书资料,以了解更多其他逆向及反逆向的技术。
笔者希望您觉得这些材料有用,并能应用其中的技术。脱壳快乐!
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
2222 调试器检测技术
本节列出了壳用来确定进程是否被调试或者系统内是否有调试器正在运行的技术。这些调试
器检测技术既有非常简单(明显)的检查,也有涉及到 native APIs和内核对象的。
2.12.12.12.1 PEB.BeingDePEB.BeingDePEB.BeingDePEB.BeingDebbbbuggeduggeduggedugged FlagFlagFlagFlag :::: IsDebuggerPresent()IsDebuggerPresent()IsDebuggerPresent()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
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
最后,Olly Advanced3 插件有置 BeingDebugged标志为 0的选项。
2.22.22.22.2 PEB.NtGlobalFlagPEB.NtGlobalFlagPEB.NtGlobalFlagPEB.NtGlobalFlag ,,,, Heap.HeapFlags,Heap.HeapFlags,Heap.HeapFlags,Heap.HeapFlags, Heap.ForceFlagsHeap.ForceFlagsHeap.ForceFlagsHeap.ForceFlags
PEB.NtGlobalFlagPEB.NtGlobalFlagPEB.NtGlobalFlagPEB.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
HeapHeapHeapHeap FlagsFlagsFlagsFlags 由于 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
对策
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
可以将 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.32.32.32.3 DebugPort:DebugPort:DebugPort:DebugPort: CheckRemoteDebuggerPresent()/NtQueryInformationProcess()CheckRemoteDebuggerPresent()/NtQueryInformationProcess()CheckRemoteDebuggerPresent()/NtQueryInformationProcess()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,
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
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:
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
//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.42.42.42.4 DebuggerDebuggerDebuggerDebugger InterruptsInterruptsInterruptsInterrupts
在调试器中步过 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
:::
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
.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.52.52.52.5 TimingTimingTimingTiming ChecksChecksChecksChecks
当进程被调试时,调试器事件处理代码、步过指令等将占用 CPU循环。如果相邻指令
之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试,而壳正好利用了这
一点。
示例
下面是一个简单的时间检查的例子。在某一段指令的前后用 RDTSC 指令(Read
Time-Stamp Counter)并计算相应的增量。增量值 0x200取决于两个 RDTSC指令之间的代
码执行量。
rdtsc
mov ecx,eax
mov ebx,edx
;...more instructions
nop
push eax
pop eax
nop
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
;...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.62.62.62.6 SeDebugPrivilegeSeDebugPrivilegeSeDebugPrivilegeSeDebugPrivilege
默认情况下进程是没有 SeDebugPrivilege权限的。然而进程通过 OllyDbg和WinDbg之
类的调试器载入的时候,SeDebugPrivilege权限被启用了。这种情况是由于调试器本身会调
整并启用 SeDebugPrivilege权限,当被调试进程加载时 SeDebugPrivilege权限也被继承了。
一些壳通过打开 CSRSS.EXE进程间接地使用 SeDebugPrivilege确定进程是否被调试。
如果能够打开 CSRSS.EXE意味着进程启用了 SeDebugPrivilege权限,由此可以推断进程正
在被调试。这个检查能起作用是因为 CSRSS.EXE进程安全描述符只允许 SYSTEM访问,
但是一旦进程拥有了 SeDebugPrivilege权限,就可以忽视安全描述符 9而访问其它进程。注
意默认情况下这一权限仅仅授予了 Administrators组的成员。
示例
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
下面是 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.72.72.72.7 ParentParentParentParent ProcessProcessProcessProcess(检测父进程)
通常进程的父进程是 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
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
77E8D1C5 83EC 0C sub esp, 0C
2.82.82.82.8 DebugObject:DebugObject:DebugObject:DebugObject: NtQueryObject()NtQueryObject()NtQueryObject()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 中注入代码,如果检索的是
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
ObjectAllTypeInformation类型则用 0清空整个返回的缓冲区。
2.92.92.92.9 DebuggerDebuggerDebuggerDebuggerWindowWindowWindowWindow
调试器窗口的存在标志着有调试器正在系统内运行。由于调试器创建的窗口拥有特定类
名(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.102.102.102.10 DebuggerDebuggerDebuggerDebugger ProcessProcessProcessProcess
另外一种识别系统内是否有调试器正在运行的方法是列出所有的进程,检查进程名是否
与调试器(如 OLLYDBG.EXE,windbg.exe等)的相符。实现很直接,利用 Process32First/Next()
然后检查映像名称是否与调试器相符就行了。
有些壳也会利用 kernel32!ReadProcessMemory()读取进程的内存,然后寻找调试器相关的
字符串(如”OLLYDBG”)以防止逆向分析人员修改调试器的可执行文件名。一旦发现调试
器的存在,壳要么显示一条错误信息,要么默默地退出或者终止调试器进程。
对策
和父进程检查类似,可以通过补丁 kernel32!Process32NextW() 使其总是返回 fail值来防
止壳枚举进程。
2.112.112.112.11 DeviceDeviceDeviceDevice DriversDriversDriversDrivers
检测内核模式的调试器是否活跃于系统中的典型技术是访问他们的设备驱动程序。该技
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
术相当简单,仅涉及调用 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.122.122.122.12 OllyDbgOllyDbgOllyDbgOllyDbg:GuardGuardGuardGuard PagesPagesPagesPages
这个检查是针对 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
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
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将
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
被设为正确的值,然后 RETN指令将会被执行。
如果异常处理例程里检查异常是否真地是 STATUS_GUARD_PAGE_VIOLATION,逆向
分析人员可以在异常处理例程中下断点然后修改传入的 ExceptionRecord参数,具体来说就
是 ExceptionCode,手工将 ExceptionCode设为 STATUS_GUARD_PAGE_VIOLATION即可。
3333 断点和补丁检测技术
本节列举了壳最常用的识别软件断点、硬件断点和补丁的方法。
3.13.13.13.1 SoftwareSoftwareSoftwareSoftware BreakpointBreakpointBreakpointBreakpoint DetectionDetectionDetectionDetection
软件断点是通过修改目标地址代码为 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.23.23.23.2 HardwareHardwareHardwareHardware BreakpointBreakpointBreakpointBreakpoint DetectionDetectionDetectionDetection
另一种断点称之为硬件断点,硬件断点是通过设置名为 Dr0到 Dr7的调试寄存器 12来实
现的。Dr0-Dr3包含至多 4个断点的地址,Dr6是个标志,它指示哪个断点被触发了,Dr7
包含了控制 4个硬件断点诸如启用/禁用或者中断于读/写的标志。
由于调试寄存器无法在 Ring3下访问,硬件断点的检测需要执行一小段代码。壳利用了
含有调试寄存器值的 CONTEXT 结构,CONTEXT 结构可以通过传递给异常处理例程的
ContextRecord参数来访问。
示例
这是一段查询调试寄存器的示例代码:
更多精彩资源请关注鱼 CCCC作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
更多精彩资源请关注鱼 CCCC工作室:www.fishc.comwww.fishc.comwww.fishc.comwww.fishc.com
; 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+0