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()漏洞,程序調用OutputDebugString()會產生一個特殊標志的軟件異常,如果程序正在被調試那么調試器線程的WaitForDebugEvent函數會將此異常捕獲並轉化為OUTPUT_DUBGU_STRING_EVENT調試事件,OD在捕獲此調試事件后會接着調用Sprintf()將OutPutDebugString中的字符串打印出來。而Springf函數並不會對參數進行檢查,如果OutputDebugStringA(TEXT("%s%s%s")),OD中是Springf(目標緩沖區,"調試字符串"),因為現在調試字符串是“%s%s%s”,那么Springf(目標緩沖區,“%s%s%s”,X1,X2,X3),而此X1,X2,X3就會隨機從棧中取出數據作為字符換的首地址,所以很容易取到的數據是一個無效指針會產生緩沖區異常。但是目前多數版本的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;
}

實時調試器檢測

當程序崩潰時其一般會彈出彈框來詢問是否用事先設置的JIT及時調試器附加程序。一旦設置了JIT調試器就會在注冊表的對應位置留下對應調試器的路徑。我們可以通過檢測注冊表對應位置的鍵值來達到反調試的目的

int main(int argc, char* argv[])
{
	HKEY hKey;														//注冊表鍵的句柄
	BOOL bExe64  = FALSE;
	IsWow64Process(GetCurrentProcess(), &bExe64);										//判斷系統是32位還是64位,32位和64位其對應需要檢測注冊表的位置不同
	TCHAR * szRegedit = bExe64 ? 
		TEXT("SOFTWARE\\WOW6432Node\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug")
		: TEXT("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug");
	

	TCHAR  szValue[256];
	DWORD  dwLen = 256;
	//打開注冊表鍵
	RegCreateKey(HKEY_LOCAL_MACHINE, szRegedit, &hKey);
	//查詢對應項的值
	RegQueryValueEx(hKey, TEXT("Debugger"), NULL, NULL, (LPBYTE)szValue, &dwLen);
	//關閉注冊表鍵
	RegCloseKey(hKey);

	if(_tcsstr(szValue, TEXT("吾愛破解.exe")))										//檢測是否為調試器名稱
	{
		MessageBox(NULL, TEXT("已檢測到調試器!"),NULL, MB_OK);
		ExitProcess(NULL);
	}

	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);
	} 
}

調試模式檢測

在進行雙機內核調試時,虛擬機會處於調試狀態。我們通過檢測虛擬機的狀態來判斷是否正在進行內核調試


typedef NTSTATUS (NTAPI *pfnNtQuerySystemInformation)(
    IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
    OUT PVOID SystemInformation,
    IN ULONG SystemInformationLength,
    OUT PULONG ReturnLength OPTIONAL
);

struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION	
{
	BOOLEAN DebuggerEnabled;
	BOOLEAN DebuggerNotPresent;
}DebuggerInfo = {0};

int main(int argc,char* argv[])
{
	pfnNtQuerySystemInformation NtQuerySystemInformation;
	NtQuerySystemInformation = (pfnNtQuerySystemInformation)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQuerySystemInformation"));

	NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)0x23, &DebuggerInfo, sizeof(DebuggerInfo), NULL);
	if(DebuggerInfo.DebuggerEnabled != 0)
	{
		MessageBox(NULL, TEXT("已檢測到調試器!"), NULL, MB_OK);
		return 0;
	}
	MessageBox(NULL, TEXT("正常運行!"), NULL, MB_OK);
	return 0;
}

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設置異常處理程序。然后在異常處理程序中進行反調試。

SetUnhandledExceptionFilter()

利用SEH的頂級異常處理程序過濾函數UnhandledExceptionFilter()會檢測調試器是否存在,如果不存在就執行SetUnhandledExceptionFilter()設置的頂級異常處理過濾干擾函數。如果存在就直接掠過SetUnhandledExceptionFilter()設置的頂級異常處理過濾干擾函數。那么我們就可以SetUnhandledExceptionFilter()設置的頂級異常處理過濾干擾函數,主動產生異常然后將程序一部分流程放到此函數中。如果被調試的話此函數中正常的程序流程將不會執行。

INT 0x2D

OD並不會把int 0x2d認定為是一個異常,也就是其並不能夠被傳遞給異常處理程序(忽略異常也沒用),如果在OD中使用F8/F9,程序會直接運行知道遇到斷點后暫停。

int main(int argc, char* argv[])
{
	BOOL bReturn;
	bReturn = AntiDebug();

	if(bReturn == 1)
	{
		MessageBox(NULL, TEXT("已檢測到調試器!"), NULL, MB_OK);
		exit(0);
	}
	else if(bReturn == 0)
		MessageBox(NULL, TEXT("程序正常運行!"), NULL, MB_OK);
	
	return 0;
}

BOOL AntiDebug()
{
	BOOL bReturn = 0;
	__asm
	{
		push offset handler
		push dword ptr fs:[0]
		mov  dword ptr fs:[0],esp


		int 0x2D
		nop
		mov bReturn,1;
		jmp end


handler:
		mov eax, dword ptr ss : [esp + 0xc]
		mov dword ptr ds : [eax + 0xb8], offset end					//修改CONTEXT的eip

		xor eax,eax
		retn
		
end:	
		pop dword ptr fs:[0]
		add esp,4

	}
	return bReturn;
}

句柄追蹤機制

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);
	}
}

雙進程自調試自修改

利用在進程中創建進程,或者通過自調試創建兩個進程。其中一個為父進程,另一個為子進程。兩個進程都是同一個可執行文件只不過執行流程不一樣。然后通過在父進程中修復子進程,子進程修改自身的自修改手段達到反調試的目的。

Hook DbgUiRemoteBreakin防附加

為了使運行中進程能夠即時中斷到調試器中,操作系統提供了一個函數DbgUiRemoteBreakin,其內部通過調用DbgBreakPoint產生一個中斷異常從而被調試器捕獲,為了實現及時中斷我們需要在運行中的進程中創建遠程線程,線程回調函數就是DbgUiRemoteBreakin函數。實際在我們附加進程時,調試器就時這么做的。所以我們通過hookDbgUiRemoteBreakin函數可以達到反附加的目的。

我們下面代碼通過將DbgUiRemoteBreakin函數的入口點代碼改為jmp ExitProcess函數的入口點,這樣一旦有調試器附加進程就會調用ExitProcess結束運行。

int main(int argc, char* argv[])
{
	BYTE	bBuffer[0x10] = {0};
	DWORD	dwBreakAddress;					//DbgUiRemoteBreakin函數的地址
	DWORD	dwOldProtect;					
	DWORD	dwNum;

	dwBreakAddress  = (DWORD)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("DbgUiRemoteBreakin"));
	bBuffer[0] = 0xE9;									//jmp指令字節碼
	*((DWORD *)(bBuffer + 1)) = (DWORD)ExitProcess - dwBreakAddress;			//ExitProcess函數偏移地址

	VirtualProtect((LPVOID)dwBreakAddress, 0x10, PAGE_EXECUTE_READWRITE, &dwOldProtect);
	WriteProcessMemory(GetCurrentProcess(), (LPVOID)dwBreakAddress, bBuffer, 5, &dwNum);
	VirtualProtect((LPVOID)dwBreakAddress, 0x10, dwOldProtect, &dwOldProtect);
	
	//此死循環是為了檢測
	while(1)
	{


	}
	return 0;
}

參考資料: 看雪學院《加密解密》
張銀奎《軟件調試》
https://www.apriorit.com/dev-blog/367-anti-reverse-engineering-protection-techniques-to-use-before-releasing-software


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM