0x01 反调试方法
软件及硬件断点
断点(breakpoint)是调试器提供的主要工具。我们可以使用断点在特定位置中断程序执行流程。断点有两种类型:
1、软件断点
2、硬件断点
如果没有断点支持,我们很难逆向分析目标软件。常用的反逆向分析技巧都会检测断点是否存在,因此我们也有对应的反调试方法。
软件断点
在IA-32架构中,有一条特殊的指令:带有0xCC
操作码(opcode)的int 3h
,这条指令可以用来调用调试句柄。当CPU执行这条指令时,就会产生中断,将控制权交给调试器。为了获得控制权,调试器需要将int 3h
指令注入代码中。为了检测断点是否存在,我们可以计算函数的校验和。示例代码如下:
DWORD CalcFuncCrc(PUCHAR funcBegin, PUCHAR funcEnd)
{
DWORD crc = 0;
for (; funcBegin < funcEnd; ++funcBegin)
{
crc += *funcBegin;
}
return crc;
}
#pragma auto_inline(off)
VOID DebuggeeFunction()
{
int calc = 0;
calc += 2;
calc <<= 8;
calc -= 3;
}
VOID DebuggeeFunctionEnd()
{
};
#pragma auto_inline(on)
DWORD g_origCrc = 0x2bd0;
int main()
{
DWORD crc = CalcFuncCrc((PUCHAR)DebuggeeFunction, (PUCHAR)DebuggeeFunctionEnd);
if (g_origCrc != crc)
{
std::cout << "Stop debugging program!" << std::endl;
exit(-1);
}
return 0;
}
需要注意的是,以上代码只有在设置/INCREMENTAL:NO
链接器选项时才能生效,否则当获取函数地址来计算校验和时,我们会得到相对跳转地址:
DebuggeeFunction:
013C16DB jmp DebuggeeFunction (013C4950h)
g_origCrc
全局变量中包含CalcFuncCrc
函数已计算出的crc
。为了检测函数尾部,我们使用了stub函数(桩函数)技巧。由于函数代码顺序存放,DebuggeeFunction
函数的尾部就是DebuggeeFunctionEnd
函数的头部。我们还使用了#pragma auto_inline(off)
指令来阻止编译器在中间嵌入函数。
如何绕过
绕过软件断点检测并没有通用的方法。如果想完成该任务,我们应当找到计算校验和的代码,将返回值替换为其他常量值,也要修改存储函数校验和的所有变量的值。
硬件断点
在x86架构中,开发者在检查和调试代码时会用到一些调试寄存器。这些寄存器可以让我们中断程序执行流,在读写内存时将控制权交给调试器。调试寄存器属于特权资源,程序只有在实模式(real mode)或者安全模式(safe mode)下且特权级CPL=0
时才能使用这些寄存器。调试寄存器总共有8个,分别为DR0
–DR7
:
-
DR0
–DR3
:断点寄存器 -
DR4
及DR5
:保留 -
DR6
:调试状态 -
DR7
:调试控制
DR0
–DR3
包含断点的线性地址。系统会在物理地址转换之前比较这些地址。每个断点都在DR7
寄存器中单独描述。DR6
寄存器用来表示哪个断点处于激活状态。DR7
通过访问模式来定义断点激活模式,分别为:读取(read)、写入(write)或者执行(execute)。基于硬件断点的检查示例如下所示:
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (GetThreadContext(GetCurrentThread(), &ctx))
{
if (ctx.Dr0 != 0 || ctx.Dr1 != 0 || ctx.Dr2 != 0 || ctx.Dr3 != 0)
{
std::cout << "Stop debugging program!" << std::endl;
exit(-1);
}
}
我们也可以通过SetThreadContext
函数来重设硬件断点。重设硬件断点的代码如下所示:
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(GetCurrentThread(), &ctx);
这样所有DRx寄存器都会被清零。
如何绕过
如果我们观察GetThreadContext
函数,可以看到其中会调用NtGetContextThread
函数。
0:000> u KERNELBASE!GetThreadContext L6
KERNELBASE!GetThreadContext:
7538d580 8bff mov edi,edi
7538d582 55 push ebp
7538d583 8bec mov ebp,esp
7538d585 ff750c push dword ptr [ebp+0Ch]
7538d588 ff7508 push dword ptr [ebp+8]
7538d58b ff1504683975 call dword ptr [KERNELBASE!_imp__NtGetContextThread (75396804)]
为了让代码读取Dr0
–Dr7
寄存器的值为0,我们需要重设CONTEXT
结构中ContextFlags
字段的CONTEXT_DEBUG_REGISTERS
标志,然后在NtGetContextThread
函数调用后恢复该值。至于GetThreadContext
函数,该函数调用的是NtSetContextThread
。绕过硬件断点检查并重置相关字段的代码如下所示:
typedef NTSTATUS(NTAPI *pfnNtGetContextThread)(
_In_ HANDLE ThreadHandle,
_Out_ PCONTEXT pContext
);
typedef NTSTATUS(NTAPI *pfnNtSetContextThread)(
_In_ HANDLE ThreadHandle,
_In_ PCONTEXT pContext
);
pfnNtGetContextThread g_origNtGetContextThread = NULL;
pfnNtSetContextThread g_origNtSetContextThread = NULL;
NTSTATUS NTAPI HookNtGetContextThread(
_In_ HANDLE ThreadHandle,
_Out_ PCONTEXT pContext)
{
DWORD backupContextFlags = pContext->ContextFlags;
pContext->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;
NTSTATUS status = g_origNtGetContextThread(ThreadHandle, pContext);
pContext->ContextFlags = backupContextFlags;
return status;
}
NTSTATUS NTAPI HookNtSetContextThread(
_In_ HANDLE ThreadHandle,
_In_ PCONTEXT pContext)
{
DWORD backupContextFlags = pContext->ContextFlags;
pContext->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;
NTSTATUS status = g_origNtSetContextThread(ThreadHandle, pContext);
pContext->ContextFlags = backupContextFlags;
return status;
}
void HookThreadContext()
{
HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
g_origNtGetContextThread = (pfnNtGetContextThread)GetProcAddress(hNtDll, "NtGetContextThread");
g_origNtSetContextThread = (pfnNtSetContextThread)GetProcAddress(hNtDll, "NtSetContextThread");
Mhook_SetHook((PVOID*)&g_origNtGetContextThread, HookNtGetContextThread);
Mhook_SetHook((PVOID*)&g_origNtSetContextThread, HookNtSetContextThread);
}
SEH
SEH(Structured Exception Handling)是操作系统向应用程序提供的一种机制,使应用程序可以接受关于异常情况的通知(如除以0、引用不存在的指针或者执行受限指令)。这种机制可以让我们在应用内部处理异常,无需操作系统介入。如果异常没有被处理,就会导致程序异常终止。开发者通常会在栈中找到指向SEH的指针,也就是SEH帧(SEH frame)。当前SEH帧地址位于FS
选择器(x64系统上是GS
选择器)相对地址offset 0处,该地址指向的是ntdll!_EXCEPTION_REGISTRATION_RECORD
结构:
0:000> dt ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION
当出现异常时,控制权将交给当前的SEH处理函数(handler)。根据所处的具体情况,这个SEH处理函数应当返回如下某个_EXCEPTION_DISPOSITION
枚举值:
typedef enum _EXCEPTION_DISPOSITION {
ExceptionContinueExecution,
ExceptionContinueSearch,
ExceptionNestedException,
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;
如果处理函数返回的是ExceptionContinueSearch
,系统会从触发异常的指令处继续执行。如果处理函数不知道如何处理异常,就会返回ExceptionContinueSearch
,然后系统会移到处理链中的下一个处理函数。我们可以在WinDbg调试器中使用!exchain
命令浏览当前的异常处理链:
0:000> !exchain
00a5f3bc: AntiDebug!_except_handler4+0 (008b7530)
CRT scope 0, filter: AntiDebug!SehInternals+67 (00883d67)
func: AntiDebug!SehInternals+6d (00883d6d)
00a5f814: AntiDebug!__scrt_stub_for_is_c_termination_complete+164b (008bc16b)
00a5f87c: AntiDebug!_except_handler4+0 (008b7530)
CRT scope 0, filter: AntiDebug!__scrt_common_main_seh+1b0 (008b7c60)
func: AntiDebug!__scrt_common_main_seh+1cb (008b7c7b)
00a5f8e8: ntdll!_except_handler4+0 (775674a0)
CRT scope 0, filter: ntdll!__RtlUserThreadStart+54386 (7757f076)
func: ntdll!__RtlUserThreadStart+543cd (7757f0bd)
00a5f900: ntdll!FinalExceptionHandlerPad4+0 (77510213)
链中最后一个处理程序是系统分配的默默人处理程序。如果之前的所有处理程序都无法处理异常,那么系统处理程序就会访问注册表,获取如下键值:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AeDebug
根据AeDebug
表项的值,应用程序会被终止,或者控制权会交给调试器。调试器路径位于Debugger
表项中(REG_SZ
)。
当创建新进程时,系统会将主SEH帧(primary SEH frame)加入其中。主SEH帧的处理程序同样由系统定义。主SEH帧大多数情况下位于分配给进程的内存栈的开头处。SEH处理程序函数原型如下所示:
typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) (
__in struct _EXCEPTION_RECORD *ExceptionRecord,
__in PVOID EstablisherFrame,
__inout struct _CONTEXT *ContextRecord,
__inout PVOID DispatcherContext
);
如果应用程序正在被调试,那么在生成int 3h
中断后,调试器将会拦截控制权。否则,控制权就会交给SEH处理函数。基于SEH帧的反调试代码如下所示:
BOOL g_isDebuggerPresent = TRUE;
EXCEPTION_DISPOSITION ExceptionRoutine(
PEXCEPTION_RECORD ExceptionRecord,
PVOID EstablisherFrame,
PCONTEXT ContextRecord,
PVOID DispatcherContext)
{
g_isDebuggerPresent = FALSE;
ContextRecord->Eip += 1;
return ExceptionContinueExecution;
}
int main()
{
__asm
{
// set SEH handler
push ExceptionRoutine
push dword ptr fs:[0]
mov dword ptr fs:[0], esp
// generate interrupt
int 3h
// return original SEH handler
mov eax, [esp]
mov dword ptr fs:[0], eax
add esp, 8
}
if (g_isDebuggerPresent)
{
std::cout << "Stop debugging program!" << std::endl;
exit(-1);
}
return 0
}
如上代码中设置了SEH处理函数,指向该处理函数的指针位于处理链的开头,然后生成int 3h
中断。如果应用程序没有被调试,控制权会转给SEH处理程序,g_isDebuggerPresent
的值会被设置为FALSE
。ContextRecord->Eip += 1
这一行代码会修改执行流中的下一条指令地址,这样就会执行在int 3h
后的下一条指令。然后代码返回原始的SEH处理函数,清空栈,并且检查是否存在调试器。
如何绕过
虽然绕过SEH检查并没有通用的方法,但逆向分析人员还是可以使用某些技术来减轻工作量。我们来观察下关于SEH处理程序的调用栈:
0:000> kn
# ChildEBP RetAddr
00 0059f06c 775100b1 AntiDebug!ExceptionRoutine
01 0059f090 77510083 ntdll!ExecuteHandler2+0x26
02 0059f158 775107ff ntdll!ExecuteHandler+0x24
03 0059f158 003b11a5 ntdll!KiUserExceptionDispatcher+0xf
04 0059fa90 003d7f4e AntiDebug!main+0xb5
05 0059faa4 003d7d9a AntiDebug!invoke_main+0x1e
06 0059fafc 003d7c2d AntiDebug!__scrt_common_main_seh+0x15a
07 0059fb04 003d7f68 AntiDebug!__scrt_common_main+0xd
08 0059fb0c 753e7c04 AntiDebug!mainCRTStartup+0x8
09 0059fb20 7752ad1f KERNEL32!BaseThreadInitThunk+0x24
0a 0059fb68 7752acea ntdll!__RtlUserThreadStart+0x2f
0b 0059fb78 00000000 ntdll!_RtlUserThreadStart+0x1b
可以看到该调用来自于ntdll!ExecuteHandler2
。这个函数是任何SEH处理函数的调用起点。我们可以在调用指令处设置断点:
0:000> u ntdll!ExecuteHandler2+24 L3
ntdll!ExecuteHandler2+0x24:
775100af ffd1 call ecx
775100b1 648b2500000000 mov esp,dword ptr fs:[0]
775100b8 648f0500000000 pop dword ptr fs:[0]
0:000> bp 775100af
设置断点后,我们应该分析被调用的每个SEH处理函数的代码。如果反调试技术涉及到对SEH处理函数的多次调用,那么逆向人员应该花精力绕过这些函数。
VEH
VEH(Vectored Exception Handler)是从Windows XP引入的一种机制,也是SEH的变种。VEH和SEH并不相互依赖,可以同时工作。当添加了新的VEH处理函数时,SEH链并不会受到影响,因为VEH处理函数存在于未导出的ntdll!LdrpVectorHandlerList
变量中。VEH和SEH机制非常相似,唯一的区别在于系统使用已公开的函数来设置并删除VEH处理函数。添加并删除VEH处理函数的函数原型以及VEH处理函数本身的原型如下所示:
PVOID WINAPI AddVectoredExceptionHandler(
ULONG FirstHandler,
PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
ULONG WINAPI RemoveVectoredExceptionHandler(
PVOID Handler
);
LONG CALLBACK VectoredHandler(
PEXCEPTION_POINTERS ExceptionInfo
);
The _EXCEPTION_POINTERS structure looks like this:
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
在处理函数中收到控制权后,系统会收集当前进程上下文并通过ContextRecord
参数进行传递。使用VEH的反调试代码如下所示:
LONG CALLBACK ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
PCONTEXT ctx = ExceptionInfo->ContextRecord;
if (ctx->Dr0 != 0 || ctx->Dr1 != 0 || ctx->Dr2 != 0 || ctx->Dr3 != 0)
{
std::cout << "Stop debugging program!" << std::endl;
exit(-1);
}
ctx->Eip += 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
int main()
{
AddVectoredExceptionHandler(0, ExceptionHandler);
__asm int 1h;
return 0;
}
这里我们设置了一个VEH处理函数并生成中断(int 1h
不是必需操作)。当产生中断时,会出现异常,控制权会转给VEH处理函数。如果设置了硬件断点,系统就会终止程序执行。如果没有硬件断点,EIP
寄存器的值就会加2,以便在int 1h
指令后继续执行。
如何绕过
来观察下涉及到VEH处理函数的调用栈:
0:000> kn
# ChildEBP RetAddr
00 001cf21c 774d6822 AntiDebug!ExceptionHandler
01 001cf26c 7753d151 ntdll!RtlpCallVectoredHandlers+0xba
02 001cf304 775107ff ntdll!RtlDispatchException+0x72
03 001cf304 00bf4a69 ntdll!KiUserExceptionDispatcher+0xf
04 001cfc1c 00c2680e AntiDebug!main+0x59
05 001cfc30 00c2665a AntiDebug!invoke_main+0x1e
06 001cfc88 00c264ed AntiDebug!__scrt_common_main_seh+0x15a
07 001cfc90 00c26828 AntiDebug!__scrt_common_main+0xd
08 001cfc98 753e7c04 AntiDebug!mainCRTStartup+0x8
09 001cfcac 7752ad1f KERNEL32!BaseThreadInitThunk+0x24
0a 001cfcf4 7752acea ntdll!__RtlUserThreadStart+0x2f
0b 001cfd04 00000000 ntdll!_RtlUserThreadStart+0x1b
如上所示,控制权会从main+0x59
转移到ntdll!KiUserExceptionDispatcher
。来看下main+0x59
中负责该操作的具体指令:
0:000> u main+59 L1
AntiDebug!main+0x59
00bf4a69 cd02 int 1
生成中断的指令如上所示。KiUserExceptionDispatcher
函数是系统从内核模式到用户模式的一个回调函数,函数原型如下所示:
VOID NTAPI KiUserExceptionDispatcher(
PEXCEPTION_RECORD pExcptRec,
PCONTEXT ContextFrame
);
我们可以通过KiUserExceptionDispatcher
函数hook来绕过硬件断点检测,如下所示:
typedef VOID (NTAPI *pfnKiUserExceptionDispatcher)(
PEXCEPTION_RECORD pExcptRec,
PCONTEXT ContextFrame
);
pfnKiUserExceptionDispatcher g_origKiUserExceptionDispatcher = NULL;
VOID NTAPI HandleKiUserExceptionDispatcher(PEXCEPTION_RECORD pExcptRec, PCONTEXT ContextFrame)
{
if (ContextFrame && (CONTEXT_DEBUG_REGISTERS & ContextFrame->ContextFlags))
{
ContextFrame->Dr0 = 0;
ContextFrame->Dr1 = 0;
ContextFrame->Dr2 = 0;
ContextFrame->Dr3 = 0;
ContextFrame->Dr6 = 0;
ContextFrame->Dr7 = 0;
ContextFrame->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;
}
}
__declspec(naked) VOID NTAPI HookKiUserExceptionDispatcher()
// Params: PEXCEPTION_RECORD pExcptRec, PCONTEXT ContextFrame
{
__asm
{
mov eax, [esp + 4]
mov ecx, [esp]
push eax
push ecx
call HandleKiUserExceptionDispatcher
jmp g_origKiUserExceptionDispatcher
}
}
int main()
{
HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
g_origKiUserExceptionDispatcher = (pfnKiUserExceptionDispatcher)GetProcAddress(hNtDll, "KiUserExceptionDispatcher");
Mhook_SetHook((PVOID*)&g_origKiUserExceptionDispatcher, HookKiUserExceptionDispatcher);
return 0;
}
在上述代码中,DRx寄存器的值会在HookKiUserExceptionDispatcher
函数中重置,也就是说,会在调用VEH处理函数前重置。
NtSetInformationThread:从调试器中隐藏线程
在Windows 2000中,出现了传递给NtSetInformationThread
函数的一个新的线程信息类:ThreadHideFromDebugger
。这是微软在研究如何防御逆向工程时在Windows中引入的第一个反调试技术,并且这种技术也非常强大。如果某个线程设置了该标志,那么该线程就会停止发送关于调试事件的通知。这些事件包括断点信息以及关于程序完成的通知信息。该标志的值存放于_ETHREAD
结构的HideFromDebugger
字段中。
1: kd> dt _ETHREAD HideFromDebugger 86bfada8
ntdll!_ETHREAD
+0x248 HideFromDebugger : 0y1
设置ThreadHideFromDebugger
的代码如下所示:
typedef NTSTATUS (NTAPI *pfnNtSetInformationThread)(
_In_ HANDLE ThreadHandle,
_In_ ULONG ThreadInformationClass,
_In_ PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength
);
const ULONG ThreadHideFromDebugger = 0x11;
void HideFromDebugger()
{
HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
pfnNtSetInformationThread NtSetInformationThread = (pfnNtSetInformationThread)
GetProcAddress(hNtDll, "NtSetInformationThread");
NTSTATUS status = NtSetInformationThread(GetCurrentThread(),
ThreadHideFromDebugger, NULL, 0);
}
如何绕过
为了阻止应用程序向调试器隐藏线程信息,我们需要hook NtSetInformationThread
函数调用。hook代码如下所示:
pfnNtSetInformationThread g_origNtSetInformationThread = NULL;
NTSTATUS NTAPI HookNtSetInformationThread(
_In_ HANDLE ThreadHandle,
_In_ ULONG ThreadInformationClass,
_In_ PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength
)
{
if (ThreadInformationClass == ThreadHideFromDebugger &&
ThreadInformation == 0 && ThreadInformationLength == 0)
{
return STATUS_SUCCESS;
}
return g_origNtSetInformationThread(ThreadHandle,
ThreadInformationClass, ThreadInformation, ThreadInformationLength
}
void SetHook()
{
HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
if (NULL != hNtDll)
{
g_origNtSetInformationThread = (pfnNtSetInformationThread)GetProcAddress(hNtDll, "NtSetInformationThread");
if (NULL != g_origNtSetInformationThread)
{
Mhook_SetHook((PVOID*)&g_origNtSetInformationThread, HookNtSetInformationThread);
}
}
}
在被hook的函数中,如果正确调用的话就会返回STATUS_SUCCESS
,并且不会将控制权交给原始的NtSetInformationThread
函数。
NtCreateThreadEx
Windows从Vista开始引入了NtCreateThreadEx
函数,函数原型如下所示:
NTSTATUS NTAPI NtCreateThreadEx (
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_opt_ ULONG_PTR ZeroBits,
_In_opt_ SIZE_T StackSize,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ PVOID AttributeList
);
其中最有趣的参数是CreateFlags
,该参数可以使用如下标志:
#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001
#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
#define THREAD_CREATE_FLAGS_HAS_SECURITY_DESCRIPTOR 0x00000010
#define THREAD_CREATE_FLAGS_ACCESS_CHECK_IN_TARGET 0x00000020
#define THREAD_CREATE_FLAGS_INITIAL_THREAD 0x00000080
如果新线程设置了THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
标志,那么在创建时就可以向调试器隐藏该线程信息,这与NtSetInformationThread
函数设置的ThreadHideFromDebugger
相同。负责安全任务的代码可以在设置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
标志的线程中执行。
如何绕过
我们可以hook NtCreateThreadEx
函数来绕过这种技术,在该函数中重置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
。
句柄跟踪
从Windows XP开始,Windows系统就具备跟踪内核对象句柄的机制。当跟踪模式启动后,与句柄所有操作都会被保存到循环缓冲区中,并且当使用不存在的句柄时(比如使用CloseHandle
函数关闭该句柄),那么就会出现EXCEPTION_INVALID_HANDLE
异常。如果进程没有通过调试器启动,那么CloseHandle
函数会返回FALSE
。基于CloseHandle
的反调试技术代码如下所示:
EXCEPTION_DISPOSITION ExceptionRoutine(
PEXCEPTION_RECORD ExceptionRecord,
PVOID EstablisherFrame,
PCONTEXT ContextRecord,
PVOID DispatcherContext)
{
if (EXCEPTION_INVALID_HANDLE == ExceptionRecord->ExceptionCode)
{
std::cout << "Stop debugging program!" << std::endl;
exit(-1);
}
return ExceptionContinueExecution;
}
int main()
{
__asm
{
// set SEH handler
push ExceptionRoutine
push dword ptr fs : [0]
mov dword ptr fs : [0], esp
}
CloseHandle((HANDLE)0xBAAD);
__asm
{
// return original SEH handler
mov eax, [esp]
mov dword ptr fs : [0], eax
add esp, 8
}
return 0
}
篡改堆栈段
当修改ss
堆栈段(stack segment register)寄存器时,调试器会跳过指令跟踪。在如下示例中,调试器会立即移到xor edx, edx
指令,而上一条指令仍会被执行。
这里大家可以拓展阅读“How to Reverse Engineer Software (Windows) in a Right Way”这篇文章。
调试信息
从Windows 10开始,Windows修改了OutputDebugString
函数的实现,改成带有特定参数的RaiseException
调用。因此,现在调试输出异常必须由调试器来处理。
我们可以使用两种异常类型来检测是否存在调试器,分别为DBG_PRINTEXCEPTION_C
(0x40010006
)以及DBG_PRINTEXCEPTION_W
(0x4001000A
)。
#define DBG_PRINTEXCEPTION_WIDE_C 0x4001000A
WCHAR * outputString = L"Any text";
ULONG_PTR args[4] = {0};
args[0] = (ULONG_PTR)wcslen(outputString) + 1;
args[1] = (ULONG_PTR)outputString;
__try
{
RaiseException(DBG_PRINTEXCEPTION_WIDE_C, 0, 4, args);
printf("Debugger detected");
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
printf("Debugger NOT detected");
}
因此,如果异常没有被处理,就意味着没有附加调试器。
DBG_PRINTEXCEPTION_W
用于宽字符输出,DBG_PRINTEXCEPTION_C
用于ansi
字符。这意味着在使用DBG_PRINTEXCEPTION_C
的情况下,arg[0]
会保存strlen()
的结果,而args[1]
在指向ansi
字符串(char *
)。
0x02 总结
本文描述了一系列反逆向工程技术,特别是反调试方法。我们从最简单的技术开始,也介绍了相应的绕过方法。本文并没有覆盖所有技术,还有一些技术大家可以自己进一步研究,比如:
- 自调试进程
- 使用
FindWindow
函数的调试器检测技术 - 基于时间计算的检测技术(参考这篇文章)
NtQueryObject
BlockInput
NtSetDebugFilterState
- 自修改代码
虽然我们主要关注的是反调试保护方法,还有其他一些反逆向分析方法,包括反转储(anti-dumping)以及混淆技术。
这里我们要再次强调一下,即使最优秀的反逆向分析技术也无法完全避免软件被逆向分析。反调试技术的主要任务是使逆向分析人员操作起来更为复杂,尽可能提高反逆向分析的难度。
0x03 参考资料
https://msdn.microsoft.com/library
http://www.infosecinstitute.com/
发表评论
您还未登录,请先登录。
登录