Windows反调试技术(下)

OD的DBGHELP模块

检测DBGHELP模块,此模块是用来加载调试符号的,所以一般加载此模块的进程的进程就是调试器。绕过方法也很简单,将DBGHELP.DLL改名。

#include <Windows.h>
#include <TlHelp32.h>
int main(int argc, char * argv[])
{
	HANDLE hSnapProcess;
	HANDLE hSnapModule;
	PROCESSENTRY32	pe32;
	pe32.dwSize = sizeof(PROCESSENTRY32);

	MODULEENTRY32	md32;
	md32.dwSize	= sizeof(MODULEENTRY32);
	hSnapProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	
	if(hSnapProcess != INVALID_HANDLE_VALUE)
	{
		Process32First(hSnapProcess, &pe32);
		do{
			hSnapModule = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pe32.th32ProcessID);
			Module32First(hSnapModule, &md32);
			do{
				if(lstrcmp(md32.szModule, "DBGHELP.DLL") == 0)
				{
					MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
					ExitProcess(NULL);
				}
			}while(Module32Next(hSnapModule, &md32));
		}while(Process32Next(hSnapProcess, &pe32));

		MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
	}
	else
		CloseHandle(hSnapProcess);
	return 0;
}

查看窗口

通过GetWindowText( )获取窗口标题文本,绕过方法也很简单就是更改窗口标题名。我们下面是检测OD调试器的示例,类比可以用来检测其他调试器如X64dbg等。

#include <Windows.h>
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam );
int main(int argc, char* argv[])
{
	EnumWindows(EnumWindowsProc, NULL);
	MessageBox(NULL,TEXT("程序正常运行!"), NULL, MB_OK);
}


BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam )
{
	char szWindowText[256] = {0};
	GetWindowText(hwnd, szWindowText, 256);					//获取的是标题栏的文本

	if(lstrcmp(szWindowText, "OllyDbg") == 0)
	{
		MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
		ExitProcess(NULL);
		return FALSE;
	}
	return TRUE;
}

也可以通过FindWindow来查找窗口。

int main(int argc, char* argv[])
{
	if(NULL != FindWindow(TEXT("OLLYDBG"),TEXT("OllyDbg")))
	{
		MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
		ExitProcess(NULL);
	}
	MessageBox(NULL,TEXT("程序正常运行!"), NULL, MB_OK);
	return 0;

}

创建进程快照来检测是否存在调试器进程

这种方法和查看窗口类似,当然也很容易被绕过。直接将程序名称更改就可以轻松绕过检测。

判断进程是否有SeDebugPrivilege权限

对于一般进程而言,如果用OpenProcess()打开csrss.exe程序则会返回无权限访问。如果以管理员身份登录并且进程被调试器调试的话,调试器会赋予进程SeDebugPrivilege权限,有了此权限程序就可以打开csrss.exe程序了。当然如果采用非管理员身份登录则这种检测将失效,因为非管理员身份下不会赋予进程SeDebugPrivilege权限。

typedef DWORD (NTAPI *pfnCsrGetProcessId)();

int main(int argc, char* argv[])
{
	pfnCsrGetProcessId CsrGetProcessId;
	CsrGetProcessId = (pfnCsrGetProcessId)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("CsrGetProcessId"));
	DWORD a = CsrGetProcessId();
	if(NULL != OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, CsrGetProcessId()))
	{
		MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
		ExitProcess(NULL);
	}
	MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
	return 0;
}

利用OD漏洞攻击调试器

OutputDebugString()漏洞,OD调用完OutputDebugString()后会接着调用Sprintf(),而此函数并不会对参数进行检查。会产生缓冲区异常,轻则执行任意代码重则程序崩溃。但是目前多数版本的OD已经将此漏洞修复。

#include <Windows.h>
int main(int argc, char* argv[])
{
	MessageBox(NULL, TEXT("程序开始运行!"), NULL, MB_OK);
	OutputDebugStringA(TEXT("%s%s%s"));
	MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
}

判断父进程

通过判断当前进程父进程的PID是否等于explorer.exe或cmd.exe或services.exe的PID来判断其是否是调试器创建的进程。

#include <TlHelp32.h>
int main(int argc, char* argv[])
{
	DWORD	dwPid;
	DWORD	dwParentPid;
	DWORD	dwPidExplorer = 0;
	DWORD	dwPidCmd	  = 0;
	DWORD	dwPidServices = 0;
	HANDLE	hSnapProcess;
	DWORD	dwFlag = 0;

	dwPid			= GetCurrentProcessId();
	hSnapProcess	= CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	PROCESSENTRY32	pe32;
	pe32.dwSize = sizeof(PROCESSENTRY32);

	if(hSnapProcess != INVALID_HANDLE_VALUE)
	{
		Process32First(hSnapProcess, &pe32);
		do{
			if(pe32.th32ProcessID == dwPid)
				dwParentPid = pe32.th32ParentProcessID;
			if(lstrcmp(pe32.szExeFile, "explorer.exe") == 0)
				dwPidExplorer = pe32.th32ProcessID;
			if(lstrcmp(pe32.szExeFile, "cmd.exe") == 0)
				dwPidCmd = pe32.th32ProcessID;
			if(lstrcmp(pe32.szExeFile, "services.exe") == 0)
				dwPidServices = pe32.th32ProcessID;
		}while(Process32Next(hSnapProcess, &pe32));


		if(dwParentPid == dwPidExplorer)
			dwFlag = 1;
		else if(dwParentPid == dwPidCmd)
			dwFlag = 1;
		else if(dwParentPid == dwPidServices)
			dwFlag = 1;
		
	}
	else
	{
		CloseHandle(hSnapProcess);
		return 0;
	}

	if(dwFlag == 1)
		MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
	else if(dwFlag == 0)
		MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
	return 0;
}

时间差

一般对于直接运行的程序而言,连续的几条指令执行所需的时间是很少的,因此指令与指令之间的时间差是很小的。而对于调试中的程序而言,就算我们按着F8不放让程序执行,其两条指令执行后也是会有时间差的。RDTSC指令可以计算出CPU自启动以后的运行周期,那么我们就可以用两条RDTSC指令计算出这两条指令执行所用的时间差。RDTSC指令执行后会将CPU运行周期的高32位放到edx,低32位放到eax中。

int main(int argc, char* argv[])
{
	if(_AntiDebug() != 0)
		MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
	else
		MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
	return 0;
}

DWORD _AntiDebug()
{
	_asm{
		rdtsc
		mov ecx,eax
		mov ebx,edx
		//一些运算
		rdtsc
		cmp edx,ebx
		jne s
		sub eax,ecx
		cmp eax,0x200
		ja  s
		xor eax,eax
		jmp s1
s:		
		mov eax,1
s1:		
	}
}

TF位检测

因为一般调试器都会在TF为1时处理单步异常让eip指向下一条指令。我们可以利用此特点,主动将TF为置1让调试器误认为是单步运行从而eip指向下一条指令。我们可以在正常的程序流程中设置异常处理程序,在异常处理程序中我们做一些处理,这样如果被调试器就会忽略异常处理程序从而不能够执行正确的程序流程。

int main(int argc, char* argv[])
{
	BOOL isDebugged = TRUE;
	__try
	{
		__asm
		{
			pushfd
			or dword ptr[esp], 0x100 
			popfd                    
			nop
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		isDebugged = FALSE;
	}
	if (isDebugged)
	{
		MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
		ExitProcess(NULL);
	} 
}

双进程保护

利用在进程中创建新的进程,因为调试器只能调试一个进程。

TLS线程本地存储

利用TLS回调函数可以在到达main()前被调用非常的隐蔽,我们可以利用这一点在TLS回调函数中进行反调试的操作。

void NTAPI Tls_Call(PVOID DllHandle, DWORD Reason, PVOID Reserved);	  //声明TLS回调函数

#pragma comment(linker, "/INCLUDE:__tls_used")				  //告知连接器使用TLS

#pragma data_seg(".CRT$XLS")						  //在共享数据段中存储TLS回调函数的地址
PIMAGE_TLS_CALLBACK pTlsAddress = Tls_Call;
#pragma data_seg()

int main(int argc, char* argv[])
{
	MessageBox(NULL, TEXT("Main()"), NULL, MB_OK);
	return 0;
}
void NTAPI Tls_Call(PVOID DllHandle, DWORD dwReason, PVOID Reserved)
{
	switch (dwReason)
	{
	case DLL_THREAD_ATTACH:						  //Reason会有4种参数
		break;
	case DLL_PROCESS_ATTACH:				          //主线程在调用Main函数前调用TLS回调函数的原因就是DLL_PROCESS_ATTACH
				                                          //可以在此处进行反调试的操作(较隐蔽)
		if(IsDebuggerPresent())
		{
			MessageBox(NULL, TEXT("已检测到调试器!"),NULL, MB_OK);
			ExitProcess(NULL);
		}
		break;
	case DLL_THREAD_DETACH:
		break;
	case DLL_PROCESS_DETACH:
		break;
	}
}

IMAGE_LOAD_CONFIG_DIRECTORY的GlobalFlagsClear

通过检查磁盘或内存中的可执行文件中PIMAGE_LOAD_CONFIG_DIRECTORY结构(程序加载到内存的一些其他配置信息)的GlobalFlagsClear字段。
默认是文件中是没有次结构,可以手动添加。此结构不为0则表示存在调试器。
有问题:无法获得__load_config_used结构的值。

extern "C"
	IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {sizeof(IMAGE_LOAD_CONFIG_DIRECTORY)};

软件断点

一般调试器会利用0xCC也就时INT3指令实现软件断点功能,我们可以通过对特定的代码片段进行检验检测是否有指令被下断点,从而达到反调试的目的。

//可以让链接器生成的代码函数调用采用CALL [ ]的形式,否则器默认采用call,jmp dword的形式	
#pragma comment(linker, "/INCREMENTAL:NO")		
DWORD OldCrc = 0x2159;

#pragma auto_inline(off)				      //防止编译器嵌入函数(关)
void DebugFunc()
{
	DWORD dwNum = 0;
	dwNum++;
	dwNum >> 3;
	dwNum = dwNum - 3;
}
void DebugFuncEnd()
{
}
#pragma auto_inline(on)					      //防止编译器嵌入函数(开)

int main(int argc, char* argv[])
{
	DWORD dwCrc = 0;
	for(DWORD i = (DWORD)DebugFunc; i <= (DWORD)DebugFuncEnd; i++)
		dwCrc = *(BYTE*)i + dwCrc;

	if(dwCrc != OldCrc)
	{
		MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
		ExitProcess(NULL);
	}

	MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
	return 0;
}

硬件断点

通过检测调试寄存器的值来检测是否有硬件断点,达到反调试的目的。

int main(int argc, char* argv[])
{
	CONTEXT stContext;

	stContext.ContextFlags = CONTEXT_ALL;
	GetThreadContext(GetCurrentThread(),&stContext);
	if(stContext.Dr0 | stContext.Dr1 | stContext.Dr2 | stContext.Dr3)
	{
		MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
		ExitProcess(NULL);
	}
	MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
	return 0;
}

SEH和VEH

程序主动产生异常,然后利用SEH或VEH设置异常处理程序。然后在异常处理程序中进行反调试。

SetUnhandleExceptionFilter()

利用SEH的*异常处理程序过滤函数UnhandleExceptionFilter()会检测调试器是否存在,如果不存在就执行SetUnhandleExceptionFilter()设置的*异常处理过滤干扰函数。如果存在就直接掠过SetUnhandleExceptionFilter()设置的*异常处理过滤干扰函数。那么我们就可以SetUnhandleExceptionFilter()设置的*异常处理过滤干扰函数,主动产生异常然后将程序一部分流程放到此函数中。如果被调试的话此函数中正常的程序流程将不会执行。

句柄追踪机制

windows提供内核对象句柄跟踪机制,如果程序被调试则用CloseHandle关闭无效句柄时会产生异常。如果不是从调试器中启动进程,则该CloseHandle返回FALSE

EXCEPTION_DISPOSITION _ExceptionProc(
	PEXCEPTION_RECORD ExceptionRecord,
    PVOID             EstablisherFrame,
    PCONTEXT          ContextRecord,
    PVOID             DispatcherContext)
{
    if (EXCEPTION_INVALID_HANDLE == ExceptionRecord->ExceptionCode)
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        ExitProcess(NULL);
    }
    return ExceptionContinueExecution;
}
int main()
{
    __asm
    {     
        push _ExceptionProc
        push dword ptr fs : [0]
        mov  dword ptr fs : [0], esp
    }
    CloseHandle((HANDLE)0xBAAD);
    __asm
    {
        mov  eax, [esp]
        mov  dword ptr fs : [0], eax
        add  esp, 8
    }
    return 0;
}

调试输出异常

从win10开始,调试输出异常需要由调试器处理,以下两种异常需要可以检测调试器是否存在。
DBG_PRINTEXCEPTION_C(0x40010006)和DBG_PRINTEXCEPTION_W(0x4001000A)

int main(int argc, char* argv[])
{
	__try
	{
		RaiseException(0x4001000A, 0, 4, (ULONG_PTR *)"SDFSDF");
	}
	__except(EXCEPTION_EXECUTE_HANDLER)
	{
		MessageBox(NULL, TEXT("无调试器!"), NULL, MB_OK);
	}
}

参考资料: 看雪学院《加密解密》
张银奎《软件调试》
https://www.apriorit.com/dev-blog/367-anti-reverse-engineering-protection-techniques-to-use-before-releasing-software

Windows反调试技术(下)

上一篇:C# 中居然也有切片语法糖,太厉害了


下一篇:apache knox apache hdaoop 生态的rest api && 应用网关