C/C++ 程式反除錯的方法

lyshark發表於2020-08-18

C/C++ 要實現程式反除錯有多種方法,BeingDebugged,NtGlobalFlag,ProcessHeap,CheckRemoteDebuggerPresent,STARTUPINFO,IsDebuggerPresent,父程式檢測,TLS 執行緒區域性儲存,RDTSC時鐘檢測反除錯,MapFileAndCheckSum,等都可實現反除錯,這裡我分別編寫了一些案例,基本上一鍋端了。

使用WinDBG隨便載入一個二進位制檔案,並載入除錯符號連結檔案.

0:000> .sympath srv*c:\symbols*http://msdl.microsoft.com/download/symbols
0:000> .reload

0:000> srv*c:\\symbols\*http://www.blib.cn/symbols
0:000> .reload

在建立程式時,作業系統會為每個執行緒分配TEB(執行緒環境塊),而且環境塊FS段暫存器總是被設定為fs:[0]的位置上,也就是預設指向當前執行緒的TEB資料,首先我們可以通過萬用字元找到TEB結構的具體名稱.

0:000> dt ntdll!*teb*
          ntdll!_TEB (執行緒環境塊)
          ntdll!_TEB32
          ntdll!_TEB64
          ntdll!_TEB_ACTIVE_FRAME_CONTEXT
          ntdll!_TEB_ACTIVE_FRAME
          ntdll!_GDI_TEB_BATCH64
          ntdll!_GDI_TEB_BATCH32
          ntdll!_GDI_TEB_BATCH
          ntdll!_TEB_ACTIVE_FRAME_CONTEXT

接著可通過dt命令,查詢下ntdll!_TEB結構,如下我們可以看到偏移為+0x018的位置就是TEB結構頭指標,在該地址基礎上向下偏移0x30就可以得到PEB(程式環境塊)的基地址.

0:000> dt -rv ntdll!_TEB

struct _TEB, 66 elements, 0xfb8 bytes
  +0x000 NtTib            : struct _NT_TIB, 8 elements, 0x1c bytes                  # NT_TIB結構
  +0x018 Self             : Ptr32 to struct _NT_TIB, 8 elements, 0x1c bytes         # NT_TIB結構(TEB自身)
     +0x000 ExceptionList    : Ptr32 to struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes
     +0x004 StackBase        : Ptr32 to Void
     +0x008 StackLimit       : Ptr32 to Void
     +0x00c SubSystemTib     : Ptr32 to Void
     +0x010 FiberData        : Ptr32 to Void
     +0x010 Version          : Uint4B
     +0x014 ArbitraryUserPointer : Ptr32 to Void
     +0x018 Self             : Ptr32 to struct _NT_TIB, 8 elements, 0x1c bytes

   +0x020 ClientId         : struct _CLIENT_ID, 2 elements, 0x8 bytes                # 程式與執行緒ID
      +0x000 UniqueProcess    : Ptr32 to Void   # 程式的PID
      +0x004 UniqueThread     : Ptr32 to Void   # 程式的PPID

   +0x02c ThreadLocalStoragePointer : Ptr32 to Void
   +0x030 ProcessEnvironmentBlock : Ptr32 to struct _PEB, 111 elements, 0x480 bytes  # 指向了PEB結構體
      +0x000 InheritedAddressSpace : UChar
      +0x001 ReadImageFileExecOptions : UChar
      +0x002 BeingDebugged    : UChar
      +0x003 BitField         : UChar

接著再來驗證一下,首先偏移地址0x18是TEB結構基地址,也就是指向自身偏移fs:[0x18]的位置,而!teb地址加0x30正是PEB的位置,在teb的基礎上加上0x30就可以得到PEB的基地址,拿到PEB基地址就可以幹很多事了.

0:000> r $teb            # 使用系統符號解析
$teb=0081e000

0:000> dd $teb+0x18      # 手動驗證地址
0081e018  0081e000 00000000 0000139c 0000194c
0081e028  00000000 0081e02c 0081b000 00000000

0:000> dd $teb + 0x30    # 在teb基礎上+30 得到PEB基地址
0081e030  0081b000 00000000 00000000 00000000

0:000> !teb
TEB at 0081e000
    ExceptionList:        00b3f8e0
    StackBase:            00b40000
    StackLimit:           00b3d000
    ClientId:             0000139c . 0000194c
    Tls Storage:          0081e02c
    PEB Address:          0081b000          # 此處地址一致

獲取程式/執行緒PID: 首先我們需要fs:[0x18]定位到TEB(執行緒環境塊)然後在此基礎上加上0x20得到ClientId.

0:000> dd fs:[0x18]                                  # 找到TEB基地址
0053:00000018  0081e000 00000000 0000139c 0000194c
0053:00000028  00000000 0081e02c 0081b000 00000000

0:000> dt _teb 0081e000 
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : (null) 
   +0x020 ClientId         : _CLIENT_ID              # 將TEB+0x20定位到這裡(程式與執行緒資訊)
   +0x028 ActiveRpcHandle  : (null) 

0:004> dt _CLIENT_ID 0081e000                   # 檢視程式詳細結構
ntdll!_CLIENT_ID
   +0x000 UniqueProcess    : 0x00b3f774 Void    # 獲取程式PID
   +0x004 UniqueThread     : 0x00b40000 Void    # 獲取執行緒PID

知道了流程,接著我們通過以下公式計算得出本程式的程式與執行緒ID.

#include <stdio.h>
#include <Windows.h>

DWORD GetSelfPid()
{
	DWORD Pid = 0;
	__asm
	{
		mov eax, fs:[0x18]   // 獲取到PEB基地址
		add eax,0x20         // 加上20得到 _CLIENT_ID
		add eax,0x0          // 加上偏移0得到 UniqueProcess
		mov eax, [eax]       // 取出記憶體地址中的值
		mov Pid,eax
	}
	return Pid;
}

DWORD GetSelfTid()
{
	DWORD Pid = 0;
	__asm
	{
		mov eax, fs:[0x18]   // 獲取到PEB基地址
		add eax, 0x20        // 加上20得到 _CLIENT_ID
		add eax, 0x04        // 加上偏移04得到 UniqueThread
		mov eax, [eax]       // 取出記憶體地址中的值
		mov Pid, eax
	}
	return Pid;
}

int main(int argc,char* argv[])
{
	printf("程式 Pid = %d \n", GetSelfPid());
	printf("執行緒 Tid = %d \n", GetSelfTid());

	system("pause");
	return 0;
}

BeingDebugged 反除錯: 程式執行時,位置FS:[30h]指向PEB的基地址,為了實現反除錯,惡意程式碼通過這個位置來檢查BeingDebugged標誌位是否為1,如果為1則說明程式被除錯,則刪除自身等.

首先我們可以使用dt _teb命令解析一下TEB的結構,如下TEB結構的起始偏移為0x0,而0x30的位置指向的是ProcessEnvironmentBlock 也就是指向了程式環境塊PEB,

0:000> dt _teb
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB       // PEB 程式環境塊

只需要在程式環境塊的基礎上+0x2就能定位到執行緒環境塊TEB中BeingDebugged的標誌,此處的標誌位如果為1則說明程式正在被除錯,為0則說明沒有被除錯.

0:000> dt _peb
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   +0x003 IsProtectedProcess : Pos 1, 1 Bit

我們手動來驗證一下,首先執行緒環境塊地址是007f1000,在此基礎上加0x30即可得到程式環境快的基地址,位置FS:[0x30]指向PEB的基地址,007ee000繼續加0x2即可得到BeingDebugged的狀態ffff0401此處我們只需要看byte位是否為1即可.

0:000> r $teb
$teb=007f1000

0:000> dd 007f1000 + 0x30
007f1030  007ee000 00000000 00000000 00000000
007f1040  00000000 00000000 00000000 00000000

0:000> r $peb
$peb=007ee000

0:000> dd 007ee000 + 0x2
007ee002  ffff0401 0000ffff 0c400112 19f0775f
007ee012  0000001b 00000000 09e0001b 0000775f

梳理一下知識點我們可以寫出一下反除錯程式碼,本程式碼單獨執行程式不會出問題,一旦被偵錯程式附加則會提示正在被除錯,當然除了自己使用匯編程式碼來實現反除錯以外,還可以使用IsDebuggerPresent()這個API函式來完成,其兩者原理完全相同.

#include <stdio.h>
#include <Windows.h>

int IsDebugA()
{
	BYTE Debug = 0;
	__asm
	{
		mov eax, dword ptr fs:[0x30]
		mov bl, byte ptr[eax + 0x2]
		mov Debug,bl
	}
	return Debug;
}

int IsDebugB()
{
	BYTE Debug = 0;
	__asm
	{
		push dword ptr fs : [0x30]
		pop edx
		mov al, [edx + 2]
		mov Debug,al
	}
	return Debug;
}

int IsDebugC()
{
	DWORD Debug = 0;

	__asm
	{
		mov eax, fs:[0x18]       // TEB Self指標
		mov eax, [eax+0x30]      // PEB
		movzx eax, [eax+2]       // PEB->BeingDebugged
		mov Debug,eax
	}
	return Debug;
}

int main(int argc,char* argv[])
{
	if (IsDebugC())
		printf("正在被除錯");
	else
		printf("沒有被除錯");

	system("pause");
	return 0;
}

如果惡意程式碼中使用該種技術阻礙我們正常除錯,我們只需要在X64DBG的命令列中執行dump fs:[30]+2來定位到BeingDebugged()的位置,並將其數值改為0然後執行程式,會發現反除錯已經被繞過了.

NtGlobalFlag 反除錯: 首先定位dt -rv ntdll!_TEB找到TEB結構並通過TEB找到PEB結構,然後找到+0x068 NtGlobalFlag,這個位置的NtGlobalFlag類似於BeingDebugged,如果是除錯狀態NtGlobalFlag的值會是0x70,所以我們可以判斷這個標誌是否為0x70來判斷程式是否被除錯了,首先我們來使用匯編程式碼解決.

#include <stdio.h>
#include <windows.h>

DWORD IsDebug()
{
	DWORD Debug = 0;
	__asm
	{
		mov eax, fs:[0x18]       // TEB基地址
		mov eax, [eax + 0x30]    // 找到PEB
		mov eax, [eax + 0x68]    // 找打 NtGlobalFlag
		mov Debug,eax            // 取出值
	}

	if (Debug == 112)
		printf("程式正在被調戲 \n");
	else
		printf("程式正常 \n");
	
	return Debug;
}

int main(int argc, char * argv[])
{
	printf("返回狀態: %d \n", IsDebugA());

	system("pause");
	return 0;
}

除了使用匯編實現反除錯外,我們也可以使用Native API中的ZwQueryInformationProcess()這個函式來讀取到程式中的PET資料,然後判斷PebBase+0x68是否等於70,由於這個函式並沒有公開,所以在使用時應該自行宣告一下結構型別.

#include <stdio.h>
#include <windows.h>
#include <winternl.h>

typedef NTSTATUS(NTAPI *typedef_ZwQueryInformationProcess)(
	IN HANDLE ProcessHandle,
	IN PROCESSINFOCLASS ProcessInformationClass,
	OUT PVOID ProcessInformation,
	IN ULONG ProcessInformationLength,
	OUT PULONG ReturnLength OPTIONAL
	);

DWORD IsDebug()
{
	HANDLE hProcess = NULL;
	DWORD ProcessId = 0;
	PROCESS_BASIC_INFORMATION Pbi;
	typedef_ZwQueryInformationProcess pZwQueryInformationProcess = NULL;
	ProcessId = GetCurrentProcessId();
	hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,ProcessId);
	if (hProcess != NULL)
	{
		HMODULE hModule = LoadLibrary(L"ntdll.dll");
		pZwQueryInformationProcess = (typedef_ZwQueryInformationProcess)GetProcAddress(hModule, "ZwQueryInformationProcess");
		NTSTATUS Status = pZwQueryInformationProcess(hProcess,ProcessBasicInformation,&Pbi,
			sizeof(PROCESS_BASIC_INFORMATION),NULL);
		if (NT_SUCCESS(Status))
		{
			DWORD ByteRead = 0;
			WORD NtGlobalFlag = 0;
			ULONG PebBase = (ULONG)Pbi.PebBaseAddress;
			if (ReadProcessMemory(hProcess, (LPCVOID)(PebBase + 0x68), &NtGlobalFlag, 2, &ByteRead) && ByteRead == 2)
			{
				if (NtGlobalFlag == 0x70)
					return 1;
			}
		}
		CloseHandle(hProcess);
	}
	return 0;
}

int main(int argc, char * argv[])
{
	if (IsDebug() == 1)
	{
		printf("正在被調戲. \n");
	}
	system("pause");
	return 0;
}

ProcessHeap 反除錯: 該屬性是一個未公開的屬性,它被設定為載入器為程式分配的第一個堆的位置(程式堆標誌),ProcessHeap標誌位於PEB結構中偏移為0x18處,第一個堆頭部有一個屬性欄位,這個屬性叫做ForceFlags屬性偏移為0x44,該屬性為0說明程式沒有被除錯,非0說明被除錯,另外的Flags屬性不為2說明被除錯,不為2則說明沒有被除錯.

0:000> dt !_peb
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x018 ProcessHeap      : Ptr32 Void       // 找到Process偏移地址

0:000> !heap                                  // 找出堆區首地址
        Heap Address      NT/Segment Heap
             1270000              NT Heap

0:000> !heap -a 1270000                       // 查詢heep的記憶體
Index   Address  Name      Debugging options enabled
  1:   01270000 
    Segment at 01270000 to 0136f000 (00006000 bytes committed)
    Flags:                40000062
    ForceFlags:           40000060
    Granularity:          8 bytes
    Segment Reserve:      00100000
    Segment Commit:       00002000

0:000> dt _HEAP 1270000                       // 找到ForceFlags標誌的偏移地址
ntdll!_HEAP
   +0x000 Segment          : _HEAP_SEGMENT
   +0x000 Entry            : _HEAP_ENTRY
   +0x040 Flags            : 0x40000062
   +0x044 ForceFlags       : 0x40000060

這裡需要注意的是堆區在不同系統中偏移值是不同的,在WindowsXP系統中ForceFlags屬性位於堆頭部偏移量為0x10處,對於Windows10系統來說這個偏移量為0x44,而預設情況如果被除錯則ForceFlags屬性為0x40000060,而Flags標誌為0x40000062,下面通過彙編分別讀取出這兩個堆頭的引數.

#include <stdio.h>
#include <windows.h>

int IsDebugA()
{
	DWORD Debug = 0;
	__asm
	{
		mov eax, fs:[0x18]       // TED基地址
		mov eax, [eax + 0x30]    // PEB基地址
		mov eax, [eax + 0x18]    // 定位 ProcessHeap
		mov eax, [eax + 0x44]    // 定位到 ForceFlags
		mov Debug, eax
	}
	return Debug;
}

int IsDebugB()
{
	DWORD Debug = 0;
	__asm
	{
		mov eax, fs:[0x18]       // TED基地址
		mov eax, [eax + 0x30]    // PEB基地址
		mov eax, [eax + 0x18]    // 定位 ProcessHeap
		mov eax, [eax + 0x40]    // 定位到 Flags
		mov Debug, eax
	}
	return Debug;
}

int main(int argc, char * argv[])
{
	int ret = IsDebugA();

	if (ret != 0)
		printf("程式正在被除錯: %x \n", ret);
	
	int ret2 = IsDebugB();
	if (ret2 != 2)
		printf("程式正在被除錯: %x \n", ret2);

	system("pause");
	return 0;
}

另一種通過C語言實現的反除錯版本,其反除錯原理與上方相同,只不過此處我們使用了系統的API來完成檢測標誌位的.

#include <stdio.h>
#include <windows.h>
#include <winternl.h>

typedef NTSTATUS(NTAPI *typedef_ZwQueryInformationProcess)(
	IN HANDLE ProcessHandle,
	IN PROCESSINFOCLASS ProcessInformationClass,
	OUT PVOID ProcessInformation,
	IN ULONG ProcessInformationLength,
	OUT PULONG ReturnLength OPTIONAL
	);

DWORD IsDebug()
{
	HANDLE hProcess = NULL;
	DWORD ProcessId = 0;
	PROCESS_BASIC_INFORMATION Pbi;
	typedef_ZwQueryInformationProcess pZwQueryInformationProcess = NULL;
	ProcessId = GetCurrentProcessId();
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
	if (hProcess != NULL)
	{
		HMODULE hModule = LoadLibrary(L"ntdll.dll");
		pZwQueryInformationProcess = (typedef_ZwQueryInformationProcess)GetProcAddress(hModule, "ZwQueryInformationProcess");
		NTSTATUS Status = pZwQueryInformationProcess(hProcess, ProcessBasicInformation, &Pbi,
			sizeof(PROCESS_BASIC_INFORMATION), NULL);
		if (NT_SUCCESS(Status))
		{
			DWORD ByteRead = 0;
			DWORD ProcessHeap = 0;
			ULONG PebBase = (ULONG)Pbi.PebBaseAddress;
			DWORD ForceFlagsValue = 1;

			ReadProcessMemory(hProcess, (LPCVOID)(PebBase + 0x18), &ProcessHeap, 2, &ByteRead);
			ReadProcessMemory(hProcess, (LPCVOID)(ProcessHeap + 0x40), &ForceFlagsValue, 4, &ByteRead);

			if (ForceFlagsValue != 0)
			{
				printf("正在被調戲. \n");
			}
		}
		CloseHandle(hProcess);
	}
	return 0;
}

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

	system("pause");
	return 0;
}

CheckRemoteDebuggerPresent 反除錯:除了使用匯編實現反除錯以外,也可以使用以下方法實現反除錯,這個反除錯很強大,我還沒有發現能夠繞過的方法.

#include <stdio.h>
#include <windows.h>

typedef BOOL(WINAPI *CHECK_REMOTE_DEBUG_PROCESS)(HANDLE, PBOOL);

BOOL CheckDebugger()
{
	BOOL bDebug = FALSE;
	CHECK_REMOTE_DEBUG_PROCESS CheckRemoteDebuggerPresent;

	HINSTANCE hModule = GetModuleHandle("kernel32");
	CheckRemoteDebuggerPresent = (CHECK_REMOTE_DEBUG_PROCESS)GetProcAddress(hModule, "CheckRemoteDebuggerPresent");

	HANDLE hProcess = GetCurrentProcess();

	CheckRemoteDebuggerPresent(hProcess, &bDebug);
	return bDebug;
}

int main(int argc,char *argv[])
{
	if (CheckDebugger() == 1)
		printf("正在被除錯 \n");

	system("pause");
	return 0;
}

STARTUPINFO 反除錯: 程式啟動時預設會通過explorer資源管理器,呼叫CreateProcess()函式建立的時候會把STARTUPINFO結構體中的值設定為0,但如果通過偵錯程式啟動程式時該值並不會發生變化,我們可以通過判斷結構體中的dwFlags引數來實現反除錯.

#include <Windows.h>
#include <stdio.h>

int IsDebug()
{
	STARTUPINFO si = {0};
	GetStartupInfo(&si);

	if (si.dwFlags != 1)
		return 1;
	return 0;
}

int main(int argc, char * argv[])
{
	int ret = IsDebug();
	printf("%d \n", ret);


	system("pause");
	return 0;
}

IsDebuggerPresent 函式反除錯: 這個函式同樣可以實現判斷是否被除錯,不過由於這個函式的實現過於簡單,很容易就能夠被分析者突破,因此現在也沒有軟體再使用該函式來進行反除錯了.

#include <stdio.h>
#include <Windows.h>

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
	while (TRUE)
	{
		//檢測用 ActiveDebugProcess()來建立除錯關係
		if (IsDebuggerPresent() == TRUE)
		{
			printf("當前程式正在被除錯 \r\n");
			DebugBreak();    // 產生int3異常
			break;
		}
		Sleep(1000);
	}
	return 0;
}

int main(int argc, char * argv[])
{
	HANDLE hThread = CreateThread(0, 0, ThreadProc, 0, 0, 0);
	if (hThread == NULL)
		return -1;

	WaitForSingleObject(hThread, INFINITE);
	CloseHandle(hThread);

	system("pause");
	return 0;
}

父程式檢測實現反除錯: 該反除錯的原理非常簡單,我們的系統在執行程式的時候,都是由Explorer.exe這個程式派生出來,也就是說如果沒有被除錯得到的父程式就是Explorer.exe的程式ID,如果被除錯則該程式的父程式ID就會變成偵錯程式的PID,並直接直接使用TerminateProcess(hProcess, 0);直接將偵錯程式的父程式幹掉.

#include <Windows.h>
#include <stdio.h>
#include <tlhelp32.h>

int IsDebug()
{
	DWORD ExplorerId = 0;
	PROCESSENTRY32 pe32 = { 0 };
	DWORD ProcessId = GetCurrentProcessId();

	GetWindowThreadProcessId(FindWindow(L"Progman", NULL), &ExplorerId);

	HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
	if (hProcessSnap != INVALID_HANDLE_VALUE)
	{
		pe32.dwSize = sizeof(PROCESSENTRY32);
		Process32First(hProcessSnap, &pe32);
		do
		{	// 先判斷是不是我們自己程式的PID
			if (ProcessId == pe32.th32ProcessID)
			{	// 判斷父程式是否是 Explorer.exe
				if (pe32.th32ParentProcessID != ExplorerId)
				{	// 如果被偵錯程式附加了,我們直接強制幹調偵錯程式
					HANDLE h_process = OpenProcess(PROCESS_TERMINATE, FALSE, pe32.th32ParentProcessID);
					TerminateProcess(h_process, 0);
					return 1;
				}
			}
		} while (Process32Next(hProcessSnap, &pe32));
	}
	return 0;
}

int main(int argc, char * argv[])
{
	int ret = IsDebug();
	if (ret == 1)
	{
		printf("程式正在被除錯 \n");
	}
	system("pause");
	return 0;
}

異常處理實現反除錯: 通過安裝異常處理函式,然後手動觸發函式,如果被偵錯程式附加則會不走異常處理,此時IsDebug將會返回預設的False,並直接走_asm call pBuff;在偵錯程式不忽略int3中斷的情況下,除錯將會被終止.

#include <Windows.h>
#include <stdio.h>

BOOL Exceptioni = FALSE;

LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo)
{
	Exceptioni = TRUE;
	return EXCEPTION_CONTINUE_EXECUTION;
}

BOOL IsDebug()
{
	ULONG OldProtect = 0;
	LPTOP_LEVEL_EXCEPTION_FILTER lpsetun;
	// 安裝自己實現的 ExceptionFilter 自定義異常處理函式
	lpsetun = SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)ExceptionFilter);
	LPVOID pBuff = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
	*((PWORD)pBuff) = 0xc3;
	VirtualProtect(pBuff, 0x1000, PAGE_EXECUTE_READ | PAGE_GUARD, &OldProtect);

	_asm call pBuff;                       // 如果被除錯,則執行中斷,不會進行異常處理
	SetUnhandledExceptionFilter(lpsetun);  // 恢復異常處理
	return Exceptioni;
}

int main(int argc, char * argv[])
{
	if (!IsDebug())
		printf("程式正在被除錯 \n");

	system("pause");
	return 0;
}

RDTSC時鐘檢測反除錯: 使用時鐘檢測方法是利用rdtsc這個彙編指令,它返回至系統重新啟動以來的時鐘數,並且將其作為一個64位的值存入EDX:EAX暫存器中,通過執行兩次rdstc指令,然後計算出他們的差值,來判斷是否被除錯了.

#include <Windows.h>
#include <stdio.h>

int IsDebug()
{
	int Debug = 0;
	__asm
	{
		rdtsc          // 呼叫時鐘
		xor ecx,ecx
		add ecx,eax    // 將eax與ecx相加
		rdtsc          // 再次呼叫時鐘
		sub eax,ecx    // 兩次結果做差值
		mov Debug,eax
	}
	//printf("列印差值: %d \n", Debug);
	if (Debug >= 21)
		return 1;
	return 0;
}

int main(int argc, char * argv[])
{
	int ret = IsDebug();
	if (ret == 1)
		printf("被除錯了 \n");

	system("pause");
	return 0;
}

TLS 執行緒區域性儲存反除錯: TLS是為了解決多執行緒變數同步問題,宣告為TLS變數後,當執行緒去訪問全域性變數時,會將這個變數拷貝到自己執行緒中的TLS空間中,以防止同一時刻內多次修改全域性變數導致變數不穩定的情況,先來看一段簡單的案例:

#include <Windows.h>
#include <stdio.h>

#pragma comment(linker, "/INCLUDE:__tls_used")

// TLS變數 
__declspec (thread) int g_nNum = 0x11111111;
__declspec (thread) char g_szStr[] = "TLS g_nNum = 0x%p ...\r\n";

// 當有執行緒訪問tls變數時,該執行緒會複製一份tls變數到自己tls空間
// 執行緒只能修改自己的空間tls變數,不會修改到全域性變數

// TLS回撥函式A
void NTAPI t_TlsCallBack_A(PVOID DllHandle, DWORD Reason, PVOID Red)
{
	if (DLL_THREAD_DETACH == Reason) // 如果執行緒退出則列印資訊   
		printf("t_TlsCallBack_A -> ThreadDetach!\r\n");
	return;
}

// TLS回撥函式B
void NTAPI t_TlsCallBack_B(PVOID DllHandle, DWORD Reason, PVOID Red)
{
	if (DLL_THREAD_DETACH == Reason) // 如果執行緒退出則列印資訊 
		printf("t_TlsCallBack_B -> ThreadDetach!\r\n");

	/* Reason 什麼事件觸發的
	DLL_PROCESS_ATTACH   1
	DLL_THREAD_ATTACH    2
	DLL_THREAD_DETACH    3
	DLL_PROCESS_DETACH   0        */
	return;
}

// 註冊TLS回撥函式,".CRT$XLB"
#pragma data_seg(".CRT$XLB") 
PIMAGE_TLS_CALLBACK p_thread_callback[] = { t_TlsCallBack_A, t_TlsCallBack_B, };
#pragma data_seg()

DWORD WINAPI t_ThreadFun(PVOID pParam)
{
	printf(g_szStr, g_nNum);
	g_nNum = 0x22222222;
	printf(g_szStr, g_nNum);
	return 0;
}

int main(int argc, char * argv[])
{
	CreateThread(NULL, 0, t_ThreadFun, NULL, 0, 0);
	Sleep(100);
	CreateThread(NULL, 0, t_ThreadFun, NULL, 0, 0);

	system("pause");
	return 0;
}

前面的那幾種反除錯手段都是在程式執行後進行判斷的,這種判斷可以通過OD斷下後進行修改從而繞過反除錯,但TLS則是在程式執行前搶佔式執行TLS中斷,所以這種反除錯技術更加的安全,但也不絕對仍然能夠被繞過.

#include <Windows.h>
#include <stdio.h>

// linker spec 通知連結器PE檔案要建立TLS目錄
#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

void NTAPI __stdcall TLS_CALLBACK(PVOID DllHandle, DWORD dwReason, PVOID Reserved)
{
	if (IsDebuggerPresent())
	{
		MessageBox(NULL, L" TLS_CALLBACK: 請勿除錯本程式 !", L"TLS Callback", MB_ICONSTOP);
		ExitProcess(0);
	}
}

// 建立TLS段
EXTERN_C
#ifdef _M_X64
	#pragma const_seg (".CRT$XLB")
	PIMAGE_TLS_CALLBACK _tls_callback = TLS_CALLBACK;
#else
	#pragma data_seg (".CRT$XLB")
	PIMAGE_TLS_CALLBACK _tls_callback = TLS_CALLBACK;
#endif

int main(int argc ,char * argv [])
{
	return 0;
}

MapFileAndCheckSum反破解: 通過使用系統提供的API實現反破解,該函式主要通過檢測,PE可選頭IMAGE_OPTIONAL_HEADER中的Checksum欄位來實現的,一般的EXE預設為0而DLL中才會啟用,當然你可以自己開啟,讓其支援這種檢測.

// C/C++  -> 常規 -> 除錯資訊格式 --> 程式資料庫
// 聯結器 -> 常規 -> 啟用增量連結 -> 否
// 聯結器 -> 高階 -> 設定校驗和 -> 是
#include <stdio.h>
#include <windows.h>
#include <Imagehlp.h>
#pragma comment(lib,"imagehlp.lib")

int main(int argc,char *argv[])
{
	DWORD HeadChksum = 1, Chksum = 0;
	char text[512];

	GetModuleFileName(GetModuleHandle(NULL), text, 512);
	if (MapFileAndCheckSum(text, &HeadChksum, &Chksum) != CHECKSUM_SUCCESS)
		return 0;

	if (HeadChksum != Chksum)
		printf("檔案校驗和錯誤 \n");
	else
		printf("檔案正常 \n");

	system("pause");
	return 0;
}

利用In指令檢測虛擬機器: Vmware為真主機與虛擬機器之間提供了相互溝通的通訊機制,它使用IN指令來讀取特定埠的資料以進行兩機通訊,但由於IN指令屬於特權指令,在真機中執行將會觸發EXCEPTION_PRIV_INSTRUCTION異常,而在虛擬機器中並不會發生異常,我們可以利用這個特性判斷程式碼是否在虛擬機器中.

#include <windows.h>
#include <stdio.h>

bool IsInsideVM()
{
	bool VmWare = true;
	__try
	{
		__asm
		{
			mov    eax, 'VMXh'
			mov    ebx, 0
			mov    ecx, 10         // 指定功能號
			mov    edx, 'VX'
			in     eax, dx         // 從埠dx讀取VMware版本到eax
			cmp    ebx, 'VMXh'     // 判斷ebx中是否包含VMware版本VMXh
			setz[VmWare]           // 設定返回值 True/False
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		VmWare = false;            // 如果未處於虛擬機器中,將會產生異常
	}
	return VmWare;
}

int main()
{
	int ret = IsInsideVM();

	if (ret == 1)
		printf("當前程式碼在虛擬機器中 \n");
	else
		printf("宿主機 \n");

	system("pause");
	return 0;
}

相關文章