动态反调试技术
动态反调试技术较于静态反调试技术破解难度更大。
基于SEH的反调试技术
Windows用户态异常处理流程:

基于SEH反调试的基本原理:

基本例子
现在通过一个基本例子来让大家感受一下SEH反调试技术:
#include "Windows.h"
bool g_isDebugged = false;
void CheckDebugger()
{
__try
{
DebugBreak();
g_isDebugged = true;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
g_isDebugged = false;
}
}
int main(int argc, char const *argv[])
{
CheckDebugger();
if (g_isDebugged == true)
{
::MessageBoxW(NULL, L"Debugged :(", L"INFO", MB_OK);
}
else
{
::MessageBoxW(NULL, L"Good Job!!!", L"INFO", MB_OK);
}
return 0;
}
基本原理
在 64 位 Windows 中,当异常发生时,事件分发的优先级顺序是:
- 内核调试器(如果有,如 KD/WinDbg 内核调试)
- 用户态调试器(通过
DebugActiveProcess附加,如 x64dbg/x64dbg) - VEH(向量化异常处理程序)
- SEH(结构化异常处理,即
__try/__except)
调试器会首先收到 EXCEPTION_DEBUG_EVENT 事件,并决定:
DBG_CONTINUE:将异常传递给程序(程序自己的 SEH 会处理)。DBG_EXCEPTION_NOT_HANDLED:也表示传递给程序。- 或者,调试器可以修改线程上下文(例如改变
RIP跳过故障指令)然后继续执行。
反调试的核心就是检测调试器是否在这个链条上,以及它的行为是否符合预期。
具体技术实现
技术1:异常流劫持检测(基础但有效)
这是最直接的方法,检测调试器是否“吞掉”了异常。
BOOL IsDebuggedBySEH() {
__try {
// 触发一个硬件异常
RaiseException(0x12345678, 0, 0, NULL);
// 如果执行到这里,说明异常被调试器处理了(调试器修改了RIP跳过RaiseException)
return TRUE;
}
__except(EXCEPTION_EXECUTE_HANDLER) {
// 正常流程:异常被本程序的 SEH 捕获
return FALSE;
}
}
变种:使用硬件异常(更难以被完美模拟)
__try {
// 触发特权指令异常(#UD)
__asm { ud2 } // 0F 0B,触发无效操作码异常
return TRUE;
}
__except(EXCEPTION_EXECUTE_HANDLER) {
return FALSE;
}
变种:访问违例 + 内存属性检测
__try {
// 尝试写入只读内存
DWORD oldProtect;
VirtualProtect((LPVOID)&IsDebuggedBySEH, 4, PAGE_READONLY, &oldProtect);
*(DWORD*)&IsDebuggedBySEH = 0xDEADBEEF; // 触发访问违例
VirtualProtect((LPVOID)&IsDebuggedBySEH, 4, oldProtect, &oldProtect);
return TRUE; // 如果执行到这里,说明调试器跳过了写入指令
}
__except(EXCEPTION_EXECUTE_HANDLER) {
return FALSE;
}
技术2:VEH 优先级检测(高级)
利用 VEH 在 SEH 之前被调用的特性,检测调试器是否破坏了调用链。
// 全局计数器
volatile LONG g_VehCallCount = 0;
LONG NTAPI MyVectoredHandler(PEXCEPTION_POINTERS pExc) {
InterlockedIncrement(&g_VehCallCount);
return EXCEPTION_CONTINUE_SEARCH; // 继续传递给 SEH
}
BOOL IsDebuggedByVEH() {
PVOID hVeh = AddVectoredExceptionHandler(1, MyVectoredHandler);
g_VehCallCount = 0;
__try {
RaiseException(0x55555555, 0, 0, NULL);
}
__except(EXCEPTION_EXECUTE_HANDLER) {
// 正常应进入这里
}
RemoveVectoredExceptionHandler(hVeh);
// 关键检测:VEH 必须被调用过一次
return (g_VehCallCount == 0); // 如果为0,说明调试器截胡了异常,VEH没被调用
}
技术3:基于 Unwind 信息的隐藏陷阱(非常隐蔽)
64 位 SEH 严重依赖 .pdata 节中的 UNWIND_INFO。可以在 UNWIND_CODE 中设置陷阱。
原理:在 UNWIND_CODE 中插入罕见的、自定义的展开操作码(虽然标准操作码有限,但可以通过不常见的组合),或者依赖展开过程中的特定副作用。
// 方法:创建一个“复杂”的栈帧,然后故意破坏它
__declspec(noinline) void DeceptiveFunction() {
// 分配不常见的栈大小,使用特定的寄存器保存模式
__asm {
push r12
push r13
push r14
sub rsp, 0x1234 // 不常见的分配大小
}
__try {
__asm { int 3 } // 触发断点异常
}
__except(EXCEPTION_EXECUTE_HANDLER) {
// 正常的展开应该能正确处理 r12-r14 的恢复和 rsp 调整
}
__asm {
add rsp, 0x1234
pop r14
pop r13
pop r12
}
}
// 检测:在 except 块中检查寄存器是否被正确恢复
// 如果调试器单步执行或修改了上下文,可能导致栈不平衡或寄存器错误
技术4:SEH 作为代码混淆/控制流混淆
将关键逻辑隐藏在 __except 块中,使静态分析者难以发现真正的控制流。
// 正常代码看起来是“死胡同”
BOOL CheckLicense() {
BOOL isValid = FALSE;
__try {
// 故意触发异常
*(int*)0 = 1;
// 逆向者看到这里可能会认为前面的代码不会执行
// 但实际上异常会跳转到 except 块
isValid = SomeComplexLicenseCheck(); // 这行永远不会在无调试时执行
}
__except(EXCEPTION_EXECUTE_HANDLER) {
// 真正的验证逻辑在这里
isValid = RealLicenseCheck();
// 可以进一步嵌套异常
__try {
if (isValid) {
*(int*)0x1 = 1; // 再次触发异常,跳到内层处理
}
}
__except(EXCEPTION_EXECUTE_HANDLER) {
// 第二层真实逻辑
DecryptAndExecute();
}
}
return isValid;
}
技术5:检测异常处理的时间差异(时序攻击)
调试器处理异常会有显著的时间延迟。
LARGE_INTEGER freq, start, end;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);
__try {
__asm { int 3 } // 断点异常
}
__except(EXCEPTION_EXECUTE_HANDLER) {
QueryPerformanceCounter(&end);
// 计算耗时(以微秒计)
LONGLONG elapsed = (end.QuadPart - start.QuadPart) * 1000000 / freq.QuadPart;
// 正常异常处理应在几微秒内,调试器介入需要几十到几百微秒
if (elapsed > 50) { // 阈值根据实际情况调整
return TRUE; // 检测到调试器
}
}
return FALSE;
技术6:破坏 .pdata 完整性检测(自校验)
64 位 SEH 依赖 .pdata 的完整性。可以检测 .pdata 是否被调试器修改。
BOOL CheckPdataIntegrity() {
// 1. 获取 .pdata 节的内存范围
HMODULE hMod = GetModuleHandle(NULL);
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hMod + ((PIMAGE_DOS_HEADER)hMod)->e_lfanew);
// 遍历节表找到 .pdata
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
if (memcmp(pSection->Name, ".pdata", 6) == 0) {
// 2. 计算 CRC32 或哈希
DWORD hash = CalculateCRC32((BYTE*)hMod + pSection->VirtualAddress, pSection->Misc.VirtualSize);
// 3. 与预计算的正确值比较
if (hash != EXPECTED_PDATA_HASH) {
return TRUE; // 被修改,可能被调试器打补丁
}
break;
}
pSection++;
}
return FALSE;
}
在检测到进程被调试之后,我们可以将代码跳转到垃圾代码来干扰逆向分析人员的跟踪。
:基于SEH的反调试技术&spm=1001.2101.3001.5002&articleId=159587886&d=1&t=3&u=936e9851b08948f4aa26739b39613f3f)
3024

被折叠的 条评论
为什么被折叠?



