TOC

参考链接

反调试技术总结
白话windows之四 异常处理机制(VEH、SEH、TopLevelEH…)
动态反调试技术
静态反调试技术
2020 空指针 5月RE公开赛

静态反调试

查看PEB.BeingDebugged的值

IsDebuggerPresent()的实现方式,如果该值为1,则表示当前进程被调试。 PEB访问方法:

  1. 直接获取PEB地址
MOV EAX, DWORD PTR FS:[30] ; FS[30] = address of PEB
  1. 先获取TEB地址,在通过ProcessEnvironmentBlock成员(+30偏移)获取PEB地址
MOV EAX, DWORD PTR FS:[18] ; FS[18] = address of TEB
MOV EAX, DWORD PTR DS:[EAX+30] ; DS[EAX+30] = address of PEB

PEB.Ldr(PEB+0xC)

(仅限在WindowsXP系统,之后的系统无法使用,且以附加形式将无法在堆内存中出现上述标识) 调试进程时,其堆内存会出现一些奇怪的标志,表示它正处于被调试状态。最醒目的是,未使用的堆内存区域全部填充着0xEEFEEEFE,这证明该进程正在被调试。 PEB.Ldr指向_PEB_LDR_DATA结构体的指针,而_PEB_LDR_DATA结构体结构体又恰好在堆内存中创建,所以扫描该区域是否存在0xEEFEEEFE区域即可判断。

  • 破解方法:将相关0xEEFEEEFE区域全部覆盖为NULL即可。

PEB.Process Heap(PEB+0x18)

(仅限在WindowsXP系统,且以附加形式将无法在堆内存中出现上述特征)

PEB.Process Heap成员既可以直接从PEB结构体获取,也可以从GetProcessHeap()API获取。

HEAP(PEB+0x18)地址(黄色方框)为0x00790000 我们访问0x00790000 进程正常运行(非调试运行)时,Heap.Flags成员(+0xC)的值为0x2,Heap.ForceFlags成员(+0x10)的值为0x0. 进程处于被调试状态时,这些值也会随之改变。比较这些值即可判断。

  • 破解方法:将Heap.Flags与Heap.ForceFlags的值分别重新设置为2与0即可。

NtGlobalFalg(PEB+0x68)

(将运行中的进程附加到调试器的时候,NtGlobalFalg值不变) 调试进程时,PEB.NtGlobalFalg成员(+0x68)的值会被设置为0x70.所以,检测该成员的值即可判断进程是否处于被调试阶段. NtGlobalFalg 0x70是由下列的flags值进行位或(bit OR)运算的结果

FLG_HEAP_ENABLE_TAIL_CHECK(0X10)
FLG_HEAP_ENABLE_FREE_CHECK(0X20)
FLG_HEAP_VALIDATE_PARAMETERS(0X40)

  • 破解方法:设置PEB.NtGlobalFalg为0即可。

NtQueryInformationProcess()系列

函数原型

__kernel_entry NTSTATUS NtQueryInformationProcess(
IN HANDLE           ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID           ProcessInformation,
IN ULONG            ProcessInformationLength,
OUT PULONG          ReturnLength
);

ProcessInformationClass为enum类型。其中,与调试器探测有关的成员为

  • ProcessDebugPort(0x7)
  • ProcessDebugObjectHandle(0x1E)
  • ProcessDebugFlags(0x1F)

ProcessDebugPort(0x7)

进程处于非调试阶段时,dwDebugPort的值为0,处于调试阶段,则为0xFFFFFFFF 例如

DWORD dwDebugPort=0;
pNtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &dwDebugPort, sizeof(dwDebugPort), NULL);
if(dwDebugPort != 0)
      printf("being debuged\n");

即CheckRemoteDebuggerPresent()函数的实现方法,该函数还可以检测其他进程是否处于被调试状态。

ProcessDebugObjectHandle(0x1E)

如果进程处于被调试状态,则调试对象句柄存在,反之,该句柄值为NULL 例如

HANDLE hDebugObject = NULL;
pNtQueryInformationProcess(GetCurrentProcess(),(PROCESSINFOCLASS)30 , &hDebugObject, sizeof(hDebugObject), NULL);
if(hDebugObject != null)
      printf("being debuged\n");

API文档头文件中的ProcessInformationClass成员中找不到此值,故直接写入30作为参数。当然,也可以自己声明好PROCESSINFOCLASS。

ProcessDebugFlags(0x1F)

函数的第二个参数设置为ProcessDebugFlags(0x1F)后,调用函数后,通过第三个参数即可获取调试标志的值。如果为0,则进程处于被调试状态;若为1,则进程处于非调试状态。

BOOL bDebugFlag = TRUE;
  pNtQueryInformationProcess(GetCurrentProcess(),
                             ProcessDebugFlags,
                             &bDebugFlag,
                             sizeof(bDebugFlag),
                             NULL);
  printf("NtQueryInformationProcess(ProcessDebugFlags) = 0x%X\n", bDebugFlag);
  if( bDebugFlag == 0x0  )  printf("  => Debugging!!!\n\n");
  else                      printf("  => Not debugging...\n\n");

注意,此处BOOL为大写,即与int等价(如果用了bool,会导致结果错误)

NtQuerySystemInformation()

API文档 函数原型

__kernel_entry NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID                   SystemInformation,
IN ULONG                    SystemInformationLength,
OUT PULONG                  ReturnLength
);

  • SystemInformationClass是枚举类型,其成员SystemKernelDebuggerInformation的值为0x23
  • 向函数第一个参数传入0x23,通过获取的第二个参数SystemInformation结构体的DebuggerEnabled和DebuggerInfo的值即可判断。
  • 注:当前(2018.12.26)的API文档中没有了SystemInformation结构体的定义,官方说明NtQuerySystemInformation可能将在未来的Windows版本中发生改变。
  • 破解方法:windows7中在命令行窗口(管理员模式)执行bcdedit /debug off即可。

NtQueryObject()

系统中的调试器调试进程的时候,会创建一个调试对象类型的内核对象。检测该对象是否存在即可判断是否有进程正在被调试。

ZwSetInformationThread()

  • 通过设置当前线程的信息来将自身从调试器中分离出来。
  • 原理:隐藏当前线程,使调试器无法再收到该线程的调试事件。

DebugActiveProcessStop()

用于分离调试器和被调试进程。

动态反调试

异常

  1. SEH
  2. SetUnhandledExceptionFilter()

进程中发生异常,若SEH未处理或者注册的SEH不存在,此时会调用执行系统的kernel32!UnhandledExceptionFilter()API.该函数内部会运行系统的最后一个异常处理器(名为Top Level Exception Filter或Last Exception Filter).系统最后的异常处理器通常会弹出错误消息框,然后终止进程运行。 kernel32!UnhandledExceptionFilter()API内部调用了ntdll!NtQueryInformationProcess(ProcessDebugPort)这个API(静态反调试),来判断是否正在调试进程。若进程正常运行(非调试状态),则运行系统最后的异常处理器,否则将异常派送给调试器。通过kernel32!SetUnhandledExceptionFilter可以修改系统最后的异常处理器。 基于异常的反调试技术中,通常先故意触发异常,然后在新注册的Last Exception Filter内部判断进程是正常运行还是调试运行,并根据判断结果修改EIP。

Timing Check

在调试器中逐行跟踪代码比程序正常运行耗费的时间要长很多,Timing Check技术通过计算运行的时间差异来判断进程是否处于被调试状态。

  1. 时间间隔测量

测量时间间隔的方法有很多,例如 基于计数器

RDTSC (汇编指令)
kernel32!QueryPerformanceCounter() / ntdll!NtQueryPerformanceCounter()
kernel32!GetTickCount()

基于时间

timeGetTime()
__ftime()

计数器的精准程度从高到低: RDTSC>NtQueryPerformanceCounter()>GetTickCount()

  1. RDTSC

x64CPU中存在一个名为TSC(Time Stamp Counter时间戳计数器)的64位寄存器。RDTSC这个汇编指令将TSC值读入EDX:EAX寄存器(高32位被保存到EDX,低32位被保存到EAX)

陷阱标志

陷阱标志指EFLAGS寄存器的第九个(Index 8)比特位, Trap Flag,(在x64dbg中表示为TF) TF设置为1后,CPU将进入单步执行模式。单步执行模式中,CPU执行1条指令后即触发EXCEPTION_SINGLE_STEP异常,然后TF会自动清零(0)。 INT 2D 在调试模式下执行完INT 2D后,下一条指令的第一个字节将被调试器忽略。(od的bug?)(若设置TF为1后再执行INT 2D,则不会忽略)

0xCC探测

API断点0xCC检测

比较校验和

异常处理

SEH: 结构化异常处理 VEH: 向量化异常处理 TopLevelEH:顶层异常处理

EXCEPTION_EXECUTE_HANDLER :该异常被处理。从异常处下一条指令继续执行
EXCEPTION_CONTINUE_SEARCH:不能处理该异常,让别人处理它吧
EXCEPTION_CONTINUE_EXECUTION:该异常被忽略。从异常处处继续执行

//调试器返回值:
DBG_CONTINUE : 等同于EXCEPTION_CONTINUE_EXECUTION
DBG_EXCEPTION_NOT_HANDLED :等同于EXCEPTION_CONTINUE_SEARCH

异常处理器处理顺序流程:

  1. 交给调试器(进程必须被调试)
  2. 执行VEH
  3. 执行SEH
  4. TopLevelEH(进程被调试时不会被执行)
  5. 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
  6. 调用异常端口通知csrss.exe
// exception.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <stdio.h>
#include <windows.h>
#include "atlstr.h" 

//先介绍一下返回值的意思
//EXCEPTION_EXECUTE_HANDLER			//该指令异常被处理。从下一条指令继续执行
//EXCEPTION_CONTINUE_SEARCH			//不能处理该异常,让别人处理它吧
//EXCEPTION_CONTINUE_EXECUTION		//该指令被忽略。从该指令处继续执行


void ShowExceptionInfo(PEXCEPTION_POINTERS pExcepInfo)
{

}
LONG ShowSelectMessageBox(TCHAR* pTitle)
{
	int nRet = MessageBox(0, _T("--------\n我要“认领”该异常?\n--------\nYES:认领该异常。\n\nNO: 交给别人处理(return EXCEPTION_CONTINUE_SEARCH)"), pTitle, MB_YESNO);
	if (nRet != IDYES)
	{//让别人处理
		_tprintf(_T("[EH.Exe] [SELE] Select EXCEPTION_CONTINUE_SEARCH\n"));
		return EXCEPTION_CONTINUE_SEARCH;
	}
	nRet = MessageBox(0, _T("--------\n是“忽略”还是“处理”该异常?\n--------\nYES:忽略,从该指令处继续执行(return EXCEPTION_CONTINUE_EXECUTION)。\n\nNO: 处理,从下一条指令继续执行(return EXCEPTION_EXECUTE_HANDLER)"), pTitle, MB_YESNO);
	if (nRet != IDYES)
	{//处理
		_tprintf(_T("[EH.Exe] [SELE] Select EXCEPTION_EXECUTE_HANDLER\n"));
		return EXCEPTION_EXECUTE_HANDLER;
	}

	//忽略
	_tprintf(_T("[EH.Exe] [SELE] Select EXCEPTION_CONTINUE_EXECUTION\n"));
	return EXCEPTION_CONTINUE_EXECUTION;
}
LONG NTAPI FirstVectExcepHandler(PEXCEPTION_POINTERS pExcepInfo)
{
	TCHAR* pTitle = _T("*首个* VEH异常处理器");
	_tprintf(_T("[EH.Exe] [VEH][1] in \n"));
	LONG nRet = ShowSelectMessageBox(pTitle);
	if (nRet == EXCEPTION_EXECUTE_HANDLER)
	{
		_tprintf(_T("[EH.Exe] [VEH][1] EXCEPTION_EXECUTE_HANDLER 标志在VEH中无效!,所以第二个VEH被调用。\n"));
	}
	if (nRet == EXCEPTION_CONTINUE_EXECUTION)
	{
		if (MessageBox(0, _T("jmp int3?(跳过INT3指令,否则还会被断下)"), _T("是否修正到下一条指令执行"), MB_YESNO) == IDYES)
		{
			pExcepInfo->ContextRecord->Eip += 1;//跳过int3
			_tprintf(_T("[EH.Exe] [VEH][1] 该异常被处理。 且:jmp int3\n"));
		}
		else
		{
			_tprintf(_T("[EH.Exe] [VEH][1] 该异常被处理\n"));
		}
	}
	_tprintf(_T("[EH.Exe] [VEH][1] out\n"));
	return nRet;
}

LONG NTAPI LastVectExcepHandler(PEXCEPTION_POINTERS pExcepInfo)
{
	TCHAR* pTitle = _T("*最后* VEH异常处理器");
	_tprintf(_T("[EH.Exe] [VEH][2] in \n"));
	LONG nRet = ShowSelectMessageBox(pTitle);
	if (nRet == EXCEPTION_EXECUTE_HANDLER)
	{
		_tprintf(_T("[EH.Exe] [VEH][2] EXCEPTION_EXECUTE_HANDLER 标志在VEH中无效!,所以SEH被调用。\n"));
	}
	if (nRet == EXCEPTION_CONTINUE_EXECUTION)
	{
		if (MessageBox(0, _T("jmp int3?(跳过INT3指令,否则还会被断下)"), _T("是否修正到下一条指令执行"), MB_YESNO) == IDYES)
		{
			pExcepInfo->ContextRecord->Eip += 1;//跳过int3
			_tprintf(_T("[EH.Exe] [VEH][2] 该异常被处理。 且:jmp int3\n"));
		}
		else
		{
			_tprintf(_T("[EH.Exe] [VEH][2] 该异常被处理\n"));
		}
	}
	_tprintf(_T("[EH.Exe] [VEH][2] out \n"));
	return nRet;
}

LONG NTAPI TopLevelExcepFilter(PEXCEPTION_POINTERS pExcepInfo)
{
	TCHAR* pTitle = _T("*顶级* 异常处理器");
	_tprintf(_T("[EH.Exe] [TOP] in \n"));
	LONG nRet = ShowSelectMessageBox(pTitle);
	_tprintf(_T("[EH.Exe] [TOP] out \n"));;
	return nRet;
}

LONG FirstSEHer(PEXCEPTION_POINTERS pExcepInfo)
{
	TCHAR* pTitle = _T("第一个SEH处理器");
	_tprintf(_T("[EH.Exe] [SEH][1] in \n"));
	LONG nRet = ShowSelectMessageBox(pTitle);
	_tprintf(_T("[EH.Exe] [SEH][1] out \n"));
	return nRet;
}
LONG SecondSEHer(PEXCEPTION_POINTERS pExcepInfo)
{
	TCHAR* pTitle = _T("第二个SEH处理器");
	_tprintf(_T("[EH.Exe] [SEH][2] in \n"));
	LONG nRet = ShowSelectMessageBox(pTitle);
	_tprintf(_T("[EH.Exe] [SEH][2] out \n"));;
	return nRet;
}

LONG ThirdSEHer(PEXCEPTION_POINTERS pExcepInfo)
{
	TCHAR* pTitle = _T("第三个SEH处理器");
	_tprintf(_T("[EH.Exe] [SEH][3] in \n"));
	LONG nRet = ShowSelectMessageBox(pTitle);
	_tprintf(_T("[EH.Exe] [SEH][3] out \n"));;
	return nRet;
}

void ExcepFunction()
{
	__try
	{
		__try
		{
			_tprintf(_T("[EH.Exe] *[CALL] int 3\n"));
			__asm int 3;
		}
		__except (FirstSEHer(GetExceptionInformation()))
		{
			_tprintf(_T("[EH.Exe] [SEH][1] 被俺处理了~(只有返回EXCEPTION_EXECUTE_HANDLER才会走到这里)\n"));
		}
	}
	__except (SecondSEHer(GetExceptionInformation()))
	{
		_tprintf(_T("[EH.Exe] [SEH][2] 被俺处理了(只有返回EXCEPTION_EXECUTE_HANDLER才会走到这里)\n"));
	}
}

DWORD __stdcall ExcepThread(LPVOID lpThreadParameter)
{
	_tprintf(_T("[EH.Exe] [ExcepThread] in\n"));
	__try
	{
		ExcepFunction();
	}
	__except (ThirdSEHer(GetExceptionInformation()))
	{
		_tprintf(_T("[EH.Exe] [SEH][3] 被俺处理了(只有返回EXCEPTION_EXECUTE_HANDLER才会走到这里)\n"));
	}
	_tprintf(_T("[EH.Exe] [ExcepThread] out\n"));
	return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
	_tprintf(_T("[EH.Exe] Add VEH.\n"));
	AddVectoredExceptionHandler(1, &FirstVectExcepHandler);
	_tprintf(_T("[EH.Exe] Add VEH.\n"));
	AddVectoredExceptionHandler(0, &LastVectExcepHandler);
	_tprintf(_T("[EH.Exe] Add Top LEF.\n"));
	SetUnhandledExceptionFilter(&TopLevelExcepFilter);

	HANDLE hThread = CreateThread(NULL, 0, &ExcepThread, NULL, 0, NULL);
	if (hThread)
	{
		WaitForSingleObject(hThread, INFINITE);
	}
	_tprintf(_T("[EH.Exe] 进程退出\n"));
	return 0;
}

例子

空指针六月re公开赛这道题用了很多反调试的技术。

  1. TLS回调函数中进行反调试检测

TLS回调函数是指,每当创建/终止进程的线程时,会自动调用执行的函数。创建或终止某线程时,TLS回调函数都会自动调用执行,前后共两次。

int __stdcall TlsCallback_0(int a1, int a2, int a3)
{
HMODULE v3; // eax
FARPROC v4; // eax
int result; // eax
int v6; // [esp+DCh] [ebp-8h]

v6 = 0;
v3 = GetModuleHandleW(L"ntdll.dll");
v4 = GetProcAddress(v3, "NtQueryInformationProcess");
result = ((int (__stdcall *)(signed int, signed int, int *, signed int, _DWORD))v4)(-1, 7, &v6, 4, 0);
if ( result )
  return result;
if ( v6 )
{
  result = TlsIndex;
  *(_DWORD *)(*(_DWORD *)(__readfsdword(0x2Cu) + 4 * TlsIndex) + 4) = 0xDEADBEEF;
}
else
{
  result = TlsIndex;
  *(_DWORD *)(*(_DWORD *)(__readfsdword(0x2Cu) + 4 * TlsIndex) + 4) = 0;
}
return result;
}

NtQueryInformationProcess,等于7检测ProcessDebugPort。
2. NtGlobalFalg(PEB+0x68)

调试进程时,PEB.NtGlobalFalg成员(+0x68)的值会被设置为0x70

__int64 sub_402740()
{
int v0; // edx
__int64 v1; // ST00_8
signed int i; // [esp+D0h] [ebp-6Ch]
int v4[24]; // [esp+DCh] [ebp-60h]

v4[22] = 0;
v4[22] = *(_DWORD *)(__readfsdword(0x30u) + 0x68) & 0x70;
v4[0] = 31;
v4[1] = 43;
v4[2] = 14;
v4[3] = 87;
v4[4] = 34;
v4[5] = 64;
v4[6] = 15;
v4[7] = 7;
v4[8] = 56;
v4[9] = 32;
v4[10] = 67;
v4[11] = 98;
v4[12] = 23;
v4[13] = 45;
v4[14] = 87;
v4[15] = 63;
v4[16] = 25;
v4[17] = 74;
v4[18] = 68;
v4[19] = 88;
if ( v4[22] != 23 )
{
  for ( i = 0; i < 20; ++i )
    byte_452F80[i] = LOBYTE(v4[i]) ^ (23 - LOBYTE(v4[22]));
}
AddVectoredExceptionHandler(0, Handler);
HIDWORD(v1) = v0;
LODWORD(v1) = 0;
return v1;
}
  1. GetTickCount

获取时间间隔

int sub_402AB0()
{
__int16 v0; // STE8_2

v0 = GetTickCount();
return (((unsigned __int16)GetTickCount() - v0) & 0xFF00) >> 8;
}
  1. 校验和 这里不仅是代码不能修改,也不能下断点,因为断点的0xcc也会被检测
int __cdecl sub_4029E0(unsigned __int8 *a1, unsigned int a2)
{
int v3; // [esp+D0h] [ebp-8h]

v3 = 0;
while ( (unsigned int)a1 < a2 )
  v3 += *a1++;
return v3;
}
__int64 sub_402470()
{
signed int v0; // edx
__int64 v1; // ST00_8
signed int j; // [esp+D0h] [ebp-DCh]
signed int i; // [esp+DCh] [ebp-D0h]
int v5[24]; // [esp+E8h] [ebp-C4h]
int v6; // [esp+150h] [ebp-5Ch]
int v7; // [esp+15Ch] [ebp-50h]
int v8; // [esp+168h] [ebp-44h]
int v9; // [esp+174h] [ebp-38h]
int v10; // [esp+180h] [ebp-2Ch]
int v11; // [esp+18Ch] [ebp-20h]
int v12; // [esp+198h] [ebp-14h]
int v13; // [esp+1A4h] [ebp-8h]

v13 = sub_402A30();
v12 = sub_402AB0();
v10 = sub_4029E0((unsigned __int8 *)sub_402B50 + 80, (unsigned int)sub_402B50 + 188);
v9 = sub_4029E0((unsigned __int8 *)main + 16, (unsigned int)main + 206);
v8 = sub_4029E0((unsigned __int8 *)sub_402E90 + 50, (unsigned int)sub_402E90 + 139);
v7 = sub_4029E0((unsigned __int8 *)TopLevelExceptionFilter, (unsigned int)TopLevelExceptionFilter + 300);
v6 = sub_4029E0((unsigned __int8 *)sub_402DC0 + 123, (unsigned int)sub_402DC0 + 136);
v0 = (unsigned __int8)v8;
v11 = (unsigned __int8)v6
    + (unsigned __int8)v8
    + (unsigned __int8)v7
    + (unsigned __int8)v10
    + (unsigned __int8)v9
    + (*(_DWORD *)(*(_DWORD *)(__readfsdword(0x2Cu) + 4 * TlsIndex) + 4) & 0xFF)
    + v12
    + v13;
v5[0] = 31;
v5[1] = 43;
v5[2] = 14;
v5[3] = 87;
v5[4] = 34;
v5[5] = 64;
v5[6] = 15;
v5[7] = 7;
v5[8] = 56;
v5[9] = 32;
v5[10] = 67;
v5[11] = 98;
v5[12] = 23;
v5[13] = 45;
v5[14] = 87;
v5[15] = 63;
v5[16] = 25;
v5[17] = 74;
v5[18] = 68;
v5[19] = 88;
v5[20] = 90;
v5[21] = 113;
v5[22] = 57;
v5[23] = 97;
if ( v11 )
{
  for ( i = 0; i < 24; ++i )
  {
    v0 = i;
    byte_452F80[i] = LOBYTE(v5[i]) ^ v11;
  }
}
else
{
  for ( j = 0; j < 24; ++j )
  {
    LOBYTE(v0) = v5[j];
    byte_452F80[j] = v0;
  }
}
HIDWORD(v1) = v0;
LODWORD(v1) = 0;
return v1;
}
  1. VEH
AddVectoredExceptionHandler(0, Handler);
  1. SetUnhandledExceptionFilter
unsigned int sub_402DC0()
{
DWORD flOldProtect; // [esp+D0h] [ebp-2Ch]
LPVOID lpAddress; // [esp+DCh] [ebp-20h]
SIZE_T dwSize; // [esp+E8h] [ebp-14h]
HMODULE v4; // [esp+F4h] [ebp-8h]

v4 = GetModuleHandleW(0);
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)TopLevelExceptionFilter);
dwSize = 188;
lpAddress = sub_402B50;
dword_452FA0 = (int)sub_402B50;
VirtualProtect(sub_402B50, 0xBCu, 0x40u, &flOldProtect);
return sub_4032A0((int)lpAddress, dwSize, (int)&byte_452F90, 16);
}