参考链接
反调试技术总结
白话windows之四 异常处理机制(VEH、SEH、TopLevelEH...)
动态反调试技术
静态反调试技术
2020 空指针 5月RE公开赛
静态反调试
查看PEB.BeingDebugged的值
即IsDebuggerPresent()
的实现方式,如果该值为1,则表示当前进程被调试。
PEB访问方法:
- 直接获取PEB地址
MOV EAX, DWORD PTR FS:[30] ; FS[30] = address of PEB
- 先获取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()
用于分离调试器和被调试进程。
动态反调试
异常
- SEH
- 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技术通过计算运行的时间差异来判断进程是否处于被调试状态。
- 时间间隔测量
测量时间间隔的方法有很多,例如
基于计数器
RDTSC (汇编指令)
kernel32!QueryPerformanceCounter() / ntdll!NtQueryPerformanceCounter()
kernel32!GetTickCount()
基于时间
timeGetTime()
__ftime()
计数器的精准程度从高到低:
RDTSC>NtQueryPerformanceCounter()>GetTickCount()
- 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
异常处理器处理顺序流程:
- 交给调试器(进程必须被调试)
- 执行VEH
- 执行SEH
- TopLevelEH(进程被调试时不会被执行)
- 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
- 调用异常端口通知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公开赛这道题用了很多反调试的技术。
- 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。
- 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;
}
- GetTickCount
获取时间间隔
int sub_402AB0()
{
__int16 v0; // STE8_2
v0 = GetTickCount();
return (((unsigned __int16)GetTickCount() - v0) & 0xFF00) >> 8;
}
- 校验和
这里不仅是代码不能修改,也不能下断点,因为断点的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;
}
- VEH
AddVectoredExceptionHandler(0, Handler);
- 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);
}
网友评论