1. 程式人生 > 實用技巧 >Windows反除錯技術(下)

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