抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

来总结一下学过的反调试技术。

进程状态

IsDebuggerPresent()

相信学习反调试都是从这个函数入手的,这个函数会查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。

实际上调用IsDebuggerPresent()会检查PEB结构体中偏移为0x2的BeingDebugge标志位,用来表示进程是否处于被调试状态。

1
2
3
4
BOOL CheckDebug()
{
return IsDebuggerPresent();
}

NtQueryInformationProcess()

我们只需要关注这个函数的第二个参数即可

1
2
3
4
5
6
7
8
9
BOOL CheckDebug()
{
int debugPort = 0;
HMODULE hModule = LoadLibrary("Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
//如果进程正在被调试,则返回调试端口,否则返回0
NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort, sizeof(debugPort), NULL);
return debugPort != 0;
}

这里再提一嘴CheckRemoteDebuggerPresen这个API,它再往下调用也会用到NtQueryInformationProcess,所以两者绕过可以是一样的。

NtGlobalFlags

由于调试器中启动进程与正常模式下启动进程有些不同,所以它们创建内存堆的方式也不同。系统使用PEB结构偏移量0x68处的NtGlobalFlags,来决定如何创建堆结构。如果这个位置的值为0x70,我们就知道进程正运行在调试器中。

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CheckDebug()
{
int result = 0;
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 68h]
and eax, 0x70
mov result, eax
}
return result != 0;
}

TEB结构体中还有很多指向堆的标志位可以来判断是否存在调试器环境下,有兴趣可以看这篇文章26种对付反调试的方法 -腾讯云开发者社区-腾讯云 (tencent.com)

STARTUPINFO

双击启动程序时,实际是通过CreateProcess函数创建启动的

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);

我们关注倒数第二个参数就可以,A表示Ascii码。explorer启动程序时,会把倒数第2个参数STARTUPINFO结构体中的值设置为0,但调试器启动程序的时候不会,所以我们可以通过判断该结构体中的某些值是否为0来判断是否被调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _STARTUPINFOA {
DWORD cb;
LPSTR lpReserved;
LPSTR lpDesktop;
LPSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;

我们选择几个属性来进行反调试检查

1
2
3
4
5
6
7
8
9
10
bool CheckDebug STARTUPINFO(){
STARTUPINFO si ={};
GetStartupInfol(&si);
if (si.dw||si.dwy||si.dwXSize||si.dwYSize)
{
printf("%x %x %x %x n",si.dwy,si.dwy,si.dwrsize,si.dwysize):
return true;.
return false;
}
}

可以选择修改函数返回值来绕过。

调试环境

这里就是查找调试器的特征值,如果找到就退出

窗口检测

查找当前系统中运行的程序窗口名称是否包含敏感程序来进行反调试。常用的函数有FindWindow、EnumWindows。FindWindow可以查找符合指定类名或窗口名的窗口句柄。

1
2
3
4
5
6
7
8
9
10
11
BOOL CheckDebug()
{
if (FindWindowA("OLLYDBG", NULL)!=NULL || FindWindowA("WinDbgFrameClass", NULL)!=NULL || FindWindowA("QWidget", NULL)!=NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}

绕过可以可以使用插件来隐藏窗口,或者为调试器改名。

进程检测

遍历系统所有进程,如果存在调试器就退出。

实现方法很简单,获取系统中所有进程的快照,接着一一对比就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL CheckDebug()
{
DWORD ID;
DWORD ret = 0;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32);
while(bMore)
{
if (stricmp(pe32.szExeFile, "OllyDBG.EXE")==0 || stricmp(pe32.szExeFile, "OllyICE.exe")==0 || stricmp(pe32.szExeFile, "x64_dbg.exe")==0 || stricmp(pe32.szExeFile, "windbg.exe")==0 || stricmp(pe32.szExeFile, "ImmunityDebugger.exe")==0)
{
return TRUE;
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
return FALSE;
}

调试器行为

正常运行程序时不会有下断点这个操作,会被当成异常去处理。只有使用调试器时才会有下断点的操作,本质上是修改运行的进程中的代码,在调试器中运行和直接运行在细节上会有一些不同,因此可以检测出调试器的存在。

软件断点

这里检查软件断点有两种思路。一种是检查自己的缓冲区是否出现了0xcc这个机器码,另外一种就是crc检验,查看自己的代码是否被修改了。

下面贴出两种思路的实现方式和绕过

检查字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
BOOL CheckDebug()
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS32 pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +
(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
DWORD dwCodeSize = pSectionHeader->SizeOfRawData;
BOOL Found = FALSE;
__asm
{
cld
mov edi,dwAddr
mov ecx,dwCodeSize
mov al,0CCH
repne scasb
jnz NotFound
mov Found,1
NotFound:
}
return Found;
}

检查crc校验码(需要提前计算好)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
}

对于这两种技术来说,都不是很好绕过。对于通用的方法,我觉得可以在程序中的汇编指令的判断语句中打补丁更改跳转条件去绕过。对于crc校验码,可以修改计算cec的返回值,或者修改全局变量与返回值相等。

硬件断点

在Windows x86架构中,开发人员在检查和调试代码时使用了一组调试寄存器。这些寄存器允许在访问内存读取或写入时中断程序执行并将控制传输到调试器。

  1. DR0-DR3 -断点寄存器
  2. DR4,DR5 -储藏
  3. DR6 -调试状态
  4. DR7 – 调试控制

所以同时最多只能设置4个硬件断点,如果没有硬件断点,那么DR0、DR1、DR2、DR3这4个寄存器的值都为0。

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CheckDebug()
{
CONTEXT context;
HANDLE hThread = GetCurrentThread();
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &context);
if (context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3!=0)
{
return TRUE;
}
return FALSE;
}

当然这也只有设置硬件断点才会被检查出来。

时钟检测

当程序自己运行没有附件调试器时,肯定做什么都直接快速,而不像调试器一样跑一条动一条。时钟检测技术就是通过计算关键内容运行时间差异来判断进程是否处于被调试状态。同时程序在虚拟机中的运行速度也比正常速度慢,所以时钟检测技术一般也用于反虚拟机/反模拟器技术。计算运行时差的方式一般有两种:读取CPU时钟计数器、时间计数相关API。

CPU计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool CheckDebug_RDTSC(){
int64t t1=0,t2=0;
int lo=0,hi=0;
__asm{
rdtsc
mov [lo],eax
mov [hi],edx
}

t1=((int64_t)1o)|((int64_t)hi<32);
__asm{
rdtsc
mov[lo],eax
mov [hi],edx
}

t2 = ((int64_t)1o)|((int64_t)hi <32);
printf("t2-t1=%x\n",t2 -t1):
//不同的CPU该差值不同,还有可能发生线程切换使差值大于一般情况,:
//所以谨慎使用这种反调试方法
return t2 - t1 > 0x100;
}

只要保证两次rdtsc运行结束后结果相同即可。

时间API

这些API也是一种反调试手段,这些API有:QueryPerformanceCounter、GetTickCount、GetSystemTime、GetLocalTime等。这里介绍一种QueryPerformanceCounter的使用

1
2
3
4
5
6
7
8
9
bool CheckDebug_QueryPerformanceCounter(){
LARGE_INTEGER startTime endTime;
QueryPerformanceCounter(&startTime):
printf("检测");
QueryPerformanceCounter(&endTime):
printf("%x\n",endTime.QuadPart - startTime.QuadPart);
return endTime.QuadPart - startTime.QuadPart > 0x500;
}

我们可以改变函数调用结果,使两次调用时间相同。(注意:一般这些时间API也会用于其他用途,所以除非明确知道所有的该API调用都是用来反调试,否则不要随便HOOK。)

异常处理

在正常运行的进程中发生异常时,操作系统会接受异常,调用程序中注册的SEH处理。而在调试器进程中,那么调试器就会先于SEH接受异常消息,让调试器去处理异常信息。利用该特征可判断进程是正常运行还是调试运行,然后根据不同的结果执行不同的操作,这就是利用异常处理机制不同的反调试原理。

常见的异常有:https://msdn.microsoft.com/zh-tw/library/aa915076.aspx

如果一个异常发生而没有注册异常处理程序(或者已经注册但没有处理这样的异常),kernel32!UnhandledExceptionFilter()函数将被调用。可以使用kernel32!SetUnhandledExceptionFilter()来注册一个自定义的未处理异常过滤器。但是如果程序在调试器下运行,自定义的过滤器将不会被调用,异常将被传递给调试器。因此,如果未处理的异常过滤器被注册,并且控制被传递给它,那么这个进程就不是在调试器下运行。

处理方法可以看一下这篇文章:https://www.52pojie.cn/thread-933123-1-1.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LONG UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo)
{
PCONTEXT ctx = pExceptionInfo->ContextRecord;
ctx->Eip += 3; // Skip \xCC\xEB\
return EXCEPTION_CONTINUE_EXECUTION;
}

bool Check()
{
bool bDebugged = true;
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)UnhandledExceptionFilter);
__asm
{
int 3
jmp near being_debugged
}
bDebugged = false;

being_debugged:
return bDebugged;
}

TLS回调函数

TLS 回调函数为开发者提供了一种灵活的机制,可以在程序加载时或者线程创建时执行特定的操作,用于初始化、资源管理、异常处理等方面在Windows操作系统中,TLS回调函数是通过TLS回调表来管理的。这些回调函数通常在DLL文件中实现,并通过动态链接库(DLL)的入口点DllMain函数进行注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include "stdafx.h"
#include <stdio.h>
#include <windows.h>

void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD dwReason, PVOID Reserved);

#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback")
#endif
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif

PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK1,0};
#pragma data_seg ()
#pragma const_seg ()

#include <iostream>

void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (IsDebuggerPresent())
{
printf("TLS_CALLBACK: Debugger Detected!\n");
}
else
{
printf("TLS_CALLBACK: No Debugger Present!\n");
}
}

int main(int argc, char* argv[])
{
printf("233\n");
return 0;
}

由操作系统或者运行时库(如 C 运行时库)所启动的一些线程,用于执行一些初始化或其他任务。在这些线程中,TLS 回调函数会先于main函数调用。

要在程序中使用TLS,必须为TLS数据单独建一个数据段,用相关数据填充此段,并通知链接器为TLS数据在PE文件头中添加数据。_tls_callback[]数组中保存了所有的TLS回调函数指针。数组必须以NULL指针结束,且数组中的每一个回调函数在程序初始化时都会被调用,程序员可按需要添加。但程序员不应当假设操作系统已何种顺序调用回调函数。如此则要求在TLS回调函数中进行反调试操作需要一定的独立性。

这个程序就会多出来一个.tls段

image-20240429214511965

双进程守护

一个进程只能被调试器附加一次。利用这一点可以自己先调试运行自己,防止被另一个调试器继续调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include "windows.h"

int DebugMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow);

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
if(!IsDebuggerPresent()) //区分调试进程与被调试进程,以执行不同的代码。
{
return DebugMain(hInstance,hPrevInstance,lpCmdLine,nCmdShow);
}

__asm("int $3");
MessageBox(0,"这是一个简单的例子","TraceMe",0);

return 0;
}

int DebugMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow) //调试进程主函数
{
char filename[MAX_PATH];
GetModuleFileName(0,filename,MAX_PATH); //获取自身文件名
STARTUPINFO si={0};
GetStartupInfo(&si);
PROCESS_INFORMATION pi={0};

if(!CreateProcess(filename,NULL,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi)) //创建被调试进程
{
return 0;
}

BOOL WhileDoFlag=TRUE;
DEBUG_EVENT DBEvent ;
DWORD dwState;

while (WhileDoFlag)
{
WaitForDebugEvent (&DBEvent, INFINITE);
dwState = DBG_EXCEPTION_NOT_HANDLED ;
switch (DBEvent.dwDebugEventCode)
{
case CREATE_PROCESS_DEBUG_EVENT:
dwState = DBG_CONTINUE ;
break;

case EXIT_PROCESS_DEBUG_EVENT :
WhileDoFlag=FALSE;
break ;

case EXCEPTION_DEBUG_EVENT:
switch (DBEvent.u.Exception.ExceptionRecord.ExceptionCode)
{
case EXCEPTION_BREAKPOINT:
{
dwState = DBG_CONTINUE ;
break;
}
}
break;
}
ContinueDebugEvent(pi.dwProcessId, pi.dwThreadId, dwState) ;
}

CloseHandle(pi.hProcess) ;
CloseHandle(pi.hThread) ;
return 0;
}

创建出两个一样的进程,只不过一个作为调试器,一个作为被调试进程,可以使用异常来进行控制程序执行。