这个例子在异常处理例程中设置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中可以通过 选项-> 调试选项 -> 异常 -> 忽略下列异常 选项卡中钩选\中断\和\单步中断\复选框来完成设置。
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字符串\比较,然后检查
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值来防止壳枚举进程。