稍稍總結一下在Crack或Rervese中比較常見的一些反調試方法,實現起來也比較簡單,以後有寫的Demo源碼參考,沒有太大的難度。windows
①最簡單也是最基礎的,Windows提供的API接口:IsDebuggerPresent(),這API實際上就是訪問PEB的BeingDebugged標誌來判斷是否處於調試狀態。函數
if (IsDebuggerPresent()) //API接口 { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); }
②本身用匯編實現實現IsDebuggerPresent,如何得到PEB呢?在應用層,fs寄存器是指向當前線程的TEB結構的,而在內核層,fs寄存器指向PCR(Processor Control Region)的內存,其數據類型是KPCR。因此能夠經過當前線程的TEB得到PEB,再取出BeingDebugged的值。oop
mov eax, fs:18h // TEB Self指針 mov eax, [eax+30h] // PEB movzx eax, [eax+2] // PEB->BeingDebugged
_asm { push eax; //TEB mov eax, fs:[0x30]; // PEB movzx eax, byte ptr[eax + 2]; //BeingDebugged mov dword ptr[Value], eax; //取值 pop eax; } if (Value) //判斷 { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); }
講個題外話,也是關於FS寄存器的前兩天有人是問了我一個問題,說是怎麼讓dll只被特定的進程加載,瞭解fs寄存器以後就很簡單了,在dll加載的DllMain()函數中對加載的當前進程進行判斷,看是不是本身的目標進程,若是不是,就拒絕被加載。下面是一段測試代碼,能夠得到當前進程的進程名。學習
#include "windows.h" //這段測試代碼能夠得到當前進程名 #include "stdio.h" int main(void) { LPSTR name; __asm{ mov eax,fs:[0x18] //獲得Teb 當線程運行於用戶空間時段寄存器FS指向當前線程的TEB,FS:[0x18] 就是指在 //Self,其內容就是TEB的起點 mov eax,[eax+0x30] //Teb偏移0x30處指向Peb mov eax,[eax+0xc] //Peb偏移 0xc 處指向 進程加載的模塊的信息 _PEB_LDR_DATA結構 mov eax,[eax+0xc] mov eax,[eax+0x30] mov name,eax } wprintf(L"%s\n",name); return 0; }
③NtGlobalFlag 也是相似於BeingDebugged的一個標誌,在Peb中取出。若是是調試狀態下,測試
NtGlobalFlag的值會是0x70,正常狀況下不是。以前是利用fs寄存器得到的Peb基地址,這裏使用Native API 的方法。spa
HANDLE hProcess = NULL; DWORD ProcessId = 0; PROCESS_BASIC_INFORMATION Pbi; PFNZwQueryInformationProcess pfunc_ZwQueryInformationProcess = NULL; ProcessId = GetCurrentProcessId(); hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, ProcessId ); if (hProcess != NULL) { HMODULE hModule = LoadLibrary(L"ntdll.dll"); pfunc_ZwQueryInformationProcess = (PFNZwQueryInformationProcess)GetProcAddress(hModule, "ZwQueryInformationProcess"); NTSTATUS Status = pfunc_ZwQueryInformationProcess( hProcess, ProcessBasicInformation, &Pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL ); if (NT_SUCCESS(Status)) { DWORD ByteRead = 0; WORD NtGlobalFlag = 0; bool bIsDebug = false; ULONG PebBase = (ULONG)Pbi.PebBaseAddress; if (ReadProcessMemory(hProcess, (LPCVOID)(PebBase + 0x68), &NtGlobalFlag, 2, &ByteRead) && ByteRead == 2) { // PEB.NtGlobalFlag // +0x068 NtGlobalFlag : Uint4B if (NtGlobalFlag == 0x70) //判斷是否處於調試狀態 { bIsDebug = true; } } if (bIsDebug) { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); } } CloseHandle(hProcess);
④PEB.ProcessHeap.Flags 和ForceFlags線程
進程堆標誌,系統建立進程時會將Flags置爲0x02(HEAP_GROWABLE),將ForceFlags置爲0。可是進程被調試時,這兩個標誌一般被設置爲0x50000062h和0x40000060h。下面是檢測代碼指針
HANDLE hProcess = NULL; DWORD ProcessId = 0; PROCESS_BASIC_INFORMATION Pbi; PFNZwQueryInformationProcess pfunc_pZwQueryInformationProcess = NULL; ULONG Flags = 0; ULONG ForceFlags = 0; switch (g_WinVersion) { case WINDOWS_VERSION_XP: Flags = 0xC; ForceFlags = 0x10; break; case WINDOWS_VERSION_2K3_SP1_SP2: Flags = 0xC; ForceFlags = 0x10; break; case WINDOWS_VERSION_7: Flags = 0x40; ForceFlags = 0x44; break; } ProcessId = GetCurrentProcessId(); hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, ProcessId ); if (hProcess != NULL) { HMODULE hModule = LoadLibrary(L"ntdll.dll"); pfunc_pZwQueryInformationProcess = (PFNZwQueryInformationProcess)GetProcAddress(hModule, "ZwQueryInformationProcess"); NTSTATUS Status = pfunc_pZwQueryInformationProcess( hProcess, ProcessBasicInformation, &Pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL ); if (NT_SUCCESS(Status)) { DWORD ByteRead = 0; ULONG ProcessHeap = 0; DWORD HeapFlags = 0; DWORD ForceFlagsValue = 1; bool bIsDebug = false; ULONG PebBase = (ULONG)Pbi.PebBaseAddress; if (ReadProcessMemory(hProcess, (LPCVOID)(PebBase + 0x18), &ProcessHeap, 4, &ByteRead)) { if (ReadProcessMemory(hProcess, (LPCVOID)(ProcessHeap + Flags), &HeapFlags, 4, &ByteRead)) { // PEB.ProcessHeap.Flags // nt!_PEB // +0x018 ProcessHeap : Ptr32 Void _HEAP // nt!_HEAP // WIN7 ProcessHeap 還有一個 Heap // +0x018 Heap : Ptr32 _HEAP // WIN7 // +0x040 Flags : Uint4B // +0x044 ForceFlags : Uint4B // WIN2K3 // +0x00C Flags : Uint4B // +0x010 ForceFlags : Uint4B // WINXP // +0x00C Flags : Uint4B // +0x010 ForceFlags : Uint4B if (HeapFlags != 2) //系統建立進程時會將其設置爲HEAP_GROEABLE(0x2) { bIsDebug = true; } else { ReadProcessMemory(hProcess,(LPCVOID)(PebBase+ForceFlags),&ForceFlagsValue,4,&ByteRead); if (ForceFlagsValue != 0) { bIsDebug = true; } } } } if (bIsDebug) { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); } } CloseHandle(hProcess); }
⑤檢測父進程調試
通常雙擊運行的進程的父進程都是explorer.exe,可是若是進程被調試,父進程則是調試器進程,也就是說若是父進程不是explorer.exe,就能夠認爲有調試器存在。code
DWORD ExplorerId = 0; PROCESSENTRY32 pe32 = {0}; CString str; BOOL bIsDebug = FALSE; DWORD ProcessId = GetCurrentProcessId(); // 獲取 Explorer 進程ID ::GetWindowThreadProcessId(::FindWindow(L"Progman", NULL), &ExplorerId); HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); if (hProcessSnap != INVALID_HANDLE_VALUE) { pe32.dwSize = sizeof(PROCESSENTRY32); if (Process32First(hProcessSnap, &pe32)) { do { if (ProcessId == pe32.th32ProcessID) { str.Format(L"進程ID:%d 父進程ID:%d Explorer進程ID:%d", ProcessId, pe32.th32ParentProcessID, ExplorerId); if (pe32.th32ParentProcessID != ExplorerId) { bIsDebug = TRUE; break; } } } while (Process32Next(hProcessSnap, &pe32)); } AfxMessageBox(str); } if (bIsDebug) { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); } CloseHandle (hProcessSnap);
⑥SeDebugPrivilege權限判斷
默認狀況下進程是沒有SeDebugPrivilege權限的,可是當進程經過OD或者xdbg等調試器啓動時,因爲調試器自己啓動了SeDebugPrivilege權限,當調試進程被加載時SeDebugPrivilege也就被繼承了。因此咱們能夠檢測進程的SeDebugPrivilege權限來間接判斷是否存在調試器,而對SeDebugPrivilege權限的判斷能夠用可否打開csrss.exe進程來判斷
HANDLE hProcess = NULL; PROCESSENTRY32 Pe32 = {0}; HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hProcessSnap == INVALID_HANDLE_VALUE) { return; } Pe32.dwSize = sizeof(PROCESSENTRY32); if (Process32First(hProcessSnap, &Pe32)) { do { if (_wcsicmp(L"csrss.exe", Pe32.szExeFile) == 0) { HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, Pe32.th32ProcessID ); if (hProcess) { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); } CloseHandle(hProcess); } } while (Process32Next(hProcessSnap, &Pe32)); } CloseHandle(hProcessSnap);
⑦檢測硬件斷點,其實也不能算是完徹底全對調試器的檢測,只能說是檢測硬件斷點,經過設置SHE處理例程,而後本身手動觸發異常來檢測硬件斷點。
#pragma data_seg(".Hardware") BOOL Hardwarei = FALSE; DWORD addr = 0; #pragma data_seg() #pragma comment(linker, "/section:.Hardware,RWS") LONG WINAPI HardwareExceptionFilter( PEXCEPTION_POINTERS ExceptionInfo ) { if (ExceptionInfo->ContextRecord->Dr0 != 0 || ExceptionInfo->ContextRecord->Dr1 != 0 || ExceptionInfo->ContextRecord->Dr2 != 0 || ExceptionInfo->ContextRecord->Dr3 != 0) { Hardwarei = TRUE; ExceptionInfo->ContextRecord->Dr0 = 0; ExceptionInfo->ContextRecord->Dr1 = 0; ExceptionInfo->ContextRecord->Dr2 = 0; ExceptionInfo->ContextRecord->Dr3 = 0; } // 設置新的eip 讓程序調轉到safe執行 ExceptionInfo->ContextRecord->Eip = addr; return EXCEPTION_CONTINUE_EXECUTION; } BOOL IsHardware() { LPTOP_LEVEL_EXCEPTION_FILTER lpsetun; lpsetun = ::SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)HardwareExceptionFilter); _asm { mov addr, offset safe; int 3; } safe: ::SetUnhandledExceptionFilter(lpsetun); return Hardwarei; } void CAntiDebugDlg::OnBnClickedButton12() { char *p = "aaaaaaaaaaaaaaddddddddddddddbbbbbbbbbbbb"; if (IsHardware()) { AfxMessageBox(L"檢測到硬件執行斷點"); } else { AfxMessageBox(L"沒有檢測到硬件執行斷點"); } }
⑧上面是經過SHE來檢測硬件斷點,固然還能夠經過VEH來檢測,思路是同樣的,差異就是SHE與VEH,一個是針對線程的,一個是針對進程的。
/************************************************************************/ /* 硬件執行斷點,VEH */ /************************************************************************/ #pragma data_seg(".VehHardware") BOOL VehHardwarei = FALSE; DWORD Vehaddr; #pragma data_seg() #pragma comment(linker, "/section:.VehHardware,RWS") LONG NTAPI VehHardwareExceptionFilter( PEXCEPTION_POINTERS ExceptionInfo ) { if (ExceptionInfo->ContextRecord->Dr0 != 0 || ExceptionInfo->ContextRecord->Dr1 != 0 || ExceptionInfo->ContextRecord->Dr2 != 0 || ExceptionInfo->ContextRecord->Dr3 != 0) { VehHardwarei = TRUE; ExceptionInfo->ContextRecord->Dr0 = 0; ExceptionInfo->ContextRecord->Dr1 = 0; ExceptionInfo->ContextRecord->Dr2 = 0; ExceptionInfo->ContextRecord->Dr3 = 0; } // 設置新的eip 讓程序調轉到safe執行 ExceptionInfo->ContextRecord->Eip = Vehaddr; return EXCEPTION_CONTINUE_EXECUTION; } BOOL IsVehHardware() { PVOID VEHandle = ::AddVectoredExceptionHandler(1, VehHardwareExceptionFilter); _asm { mov Vehaddr, offset vehsafe; int 3; } vehsafe: if (VEHandle != NULL) { RemoveVectoredExceptionHandler(VEHandle); } return VehHardwarei; } void CAntiDebugDlg::OnBnClickedButton16() { // TODO: 在此添加控件通知處理程序代碼 char *p = "aaaaaaaaaaaaaaddddddddddddddbbbbbbbbbbbb"; if (IsVehHardware()) { AfxMessageBox(L"檢測到硬件執行斷點"); } else { AfxMessageBox(L"沒有檢測到硬件執行斷點"); } }
⑨利用異常處理 經過咱們本身安裝異常處理例程,而後手動觸發異常,若是存在OD則不會走異常處理
/************************************************************************/ /* 異常處理判斷是否存在OD */ /************************************************************************/ #pragma data_seg(".excep") BOOL Exceptioni = FALSE; #pragma data_seg() #pragma comment(linker, "/section:.excep,RWS") LONG WINAPI ExceptionFilter( PEXCEPTION_POINTERS ExceptionInfo ) { Exceptioni = TRUE; //進行異常處理,則不存在OD return EXCEPTION_CONTINUE_EXECUTION; } BOOL IsException() { ULONG OldProtect = 0; LPTOP_LEVEL_EXCEPTION_FILTER lpsetun; 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; //若是存在OD,會把這裏的call 當成中斷處理,不會進行異常處理 } ::SetUnhandledExceptionFilter(lpsetun); //將原來的異常處理恢復頂層 return Exceptioni; } void CAntiDebugDlg::OnBnClickedButton13() { // TODO: 在此添加控件通知處理程序代碼 if (IsException()) { AfxMessageBox(L"沒有檢測到調試器"); } else { AfxMessageBox(L"檢測到調試器"); } }
⑩單步異常檢測
單步異常是調試器用來單步調試時使用,咱們也能夠用來檢測調試器的存在,由咱們本身設置標誌寄存器的值,觸發單步異常。若是存在調試器,這裏的單步異常會由調試器進行處理,而不會走__except()異常處理,但這種檢測方法的侷限性就在於檢測代碼須要被調試器單步調試的時候纔有效。扯個題外話,這裏我還打算嘗試一下C++11新支持的異常處理try{} /catch(){},沒想到一弄直接就崩潰了,仍是老老實實用__try/__except吧!
/************************************************************************/ /* 單步異常檢測 */ /************************************************************************/ //這種方法須要下面的彙編代碼在單步跟中的時候纔有效 void CAntiDebugDlg::OnBnClickedButton14() { // TODO: 在此添加控件通知處理程序代碼 __try { // 觸發單步異常 _asm { pushfd; //標誌寄存器入棧 or dword ptr [esp], 100h; // 將 TF =1 單步 popfd; } AfxMessageBox(L"檢測到調試器"); //若是不觸發異常,說明有調試器 } //catch(...) 用C++ 11的catch()仍是沒有原生__except強大,用catch(...)這裏會崩潰 __except(EXCEPTION_EXECUTE_HANDLER) { AfxMessageBox(L"沒有檢測到調試器"); } }
11.into使調試器一直循環。使用溢出中斷,在沒有調試器的狀況下會走異常處理,而後正常的執行下去,若是存在OD則會本身處理中斷,致使一直在這裏循環。其實這裏用into的溢出中斷和int 3的中斷效果是同樣的,這裏採用的是平時使用較少的into溢出中斷。
#pragma data_seg(".VehInto") DWORD VehintoAddr = 0; #pragma data_seg() #pragma comment(linker, "/section:.VehInto,RWS") LONG NTAPI VehIntoExceptionFilter( PEXCEPTION_POINTERS ExceptionInfo ) { // 設置新的eip 讓程序調轉到safe執行 ExceptionInfo->ContextRecord->Eip = VehintoAddr; return EXCEPTION_CONTINUE_EXECUTION; } void IsVehIntoBreak() { PVOID VEHandle = ::AddVectoredExceptionHandler(1, VehIntoExceptionFilter); _asm { // 存放異常處理以後的偏移地址 mov VehintoAddr, offset Intosafe; mov ecx, 1; } here: _asm { /* 若是存在OD調試器,這裏會一直循環下去。 */ rol ecx, 1; //不停的循環左移,直至產生溢出,OF位爲1 into; //溢出中斷 ,會調用異常處理 jmp here; } Intosafe: if (VEHandle != NULL) { RemoveVectoredExceptionHandler(VEHandle); } } void CAntiDebugDlg::OnBnClickedButton15() { // TODO: 在此添加控件通知處理程序代碼 IsVehIntoBreak(); }
12 . int 2d
/************************************************************************/ /* INT 2D */ /************************************************************************/ BOOL IsInt2d() { __try { __asm { int 2dh; inc eax;//any opcode of singlebyte.若是開啓VT會崩潰 //;or u can put some junkcode,"0xc8"..."0xc2"..."0xe8"..."0xe9" } return TRUE; } __except(EXCEPTION_EXECUTE_HANDLER) { return FALSE; } } void CAntiDebugDlg::OnBnClickedButton21() { // TODO: 在此添加控件通知處理程序代碼 if (IsInt2d()) { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); } }
13. 檢驗內存校驗和
調試器(OD)在設置斷點會改變內存屬性,改寫爲0xCC,咱們須要檢驗自身的某一個段內存的校驗和就能夠判斷是否有調試器的存在
/************************************************************************/ /* CheckSum */ /************************************************************************/ BOOL CheckSum() { BOOL bFoundOD; bFoundOD = FALSE; DWORD CHECK_SUM = 5555; //正確校驗值 DWORD dwAddr; dwAddr = (DWORD)CheckSum; __asm { ; 檢測代碼開始 mov esi, dwAddr; mov ecx, 100; xor eax, eax; checksum_loop: movzx ebx, byte ptr [esi]; add eax, ebx; rol eax, 1; inc esi; loop checksum_loop; cmp eax, CHECK_SUM; jz ODNotFound; mov bFoundOD, 1; ODNotFound: } return bFoundOD; } void CAntiDebugDlg::OnBnClickedButton27() { // TODO: 在此添加控件通知處理程序代碼 if (CheckSum()) { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); } }
14. 保護頁異常
當執行一個屬性爲PAGE_GUARD的頁面時,觸發EXCEPTION_GUARD_PAGE的異常,若是存在調試器通常都是由調試器處理了。
/************************************************************************/ /* 「保護頁異常」 */ /************************************************************************/ //當應用程序嘗試執行保護頁內的代碼時,將會產生一個EXCEPTION_GUARD_PAGE(0x80000001)異常 BOOL IsGuardPages() { SYSTEM_INFO sSysInfo; DWORD dwPageSize = 0; DWORD OldProtect = 0; GetSystemInfo(&sSysInfo); dwPageSize = sSysInfo.dwPageSize; LPVOID lpvBase = VirtualAlloc(NULL, dwPageSize, MEM_COMMIT, PAGE_READWRITE); if (lpvBase == NULL) { return FALSE; } PBYTE lptmpB = (PBYTE)lpvBase; *lptmpB = 0xc3; //retn VirtualProtect(lpvBase, dwPageSize, PAGE_EXECUTE_READ | PAGE_GUARD, &OldProtect); __try { __asm call dword ptr[lpvBase]; VirtualFree(lpvBase, 0, MEM_RELEASE); return TRUE; } __except(EXCEPTION_EXECUTE_HANDLER) { VirtualFree(lpvBase, 0, MEM_RELEASE); return FALSE; } } void CAntiDebugDlg::OnBnClickedButton25() { // TODO: 在此添加控件通知處理程序代碼 if (IsGuardPages()) { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); } }
14. 還有就是關於調試對象的檢測,這裏就牽扯到內核層的問題,一些基礎內容就不進行贅述了。若是往深了研究能夠學習系統的內核調試引擎,折騰TX的遊戲TP反雙機調試。這裏只給出一些在應用層利用的比較簡單的檢測方法。
檢測調試對象DebugObject
/************************************************************************/ /* ZwQueryObject */ /************************************************************************/ #ifndef STATUS_INFO_LENGTH_MISMATCH #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) #endif void CAntiDebugDlg::OnBnClickedButton24() { POBJECT_TYPES_INFORMATION pTypesInfo = NULL; ULONG dwSize = 0; PFNZwQueryObject pfunc_ZwQueryObject; HMODULE hModule = LoadLibrary(L"ntdll.dll"); pfunc_ZwQueryObject = (PFNZwQueryObject)GetProcAddress(hModule, "ZwQueryObject"); if (pfunc_ZwQueryObject) { NTSTATUS Status = pfunc_ZwQueryObject( NULL, ObjectAllTypesInformation, NULL, 0, &dwSize ); if (Status == STATUS_INFO_LENGTH_MISMATCH) { pTypesInfo = (POBJECT_TYPES_INFORMATION)VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (pTypesInfo == NULL) return; Status = pfunc_ZwQueryObject( NULL, ObjectAllTypesInformation, pTypesInfo, dwSize, &dwSize ); if (!NT_SUCCESS(Status)) { AfxMessageBox(L"查詢錯誤"); VirtualFree (pTypesInfo, 0, MEM_RELEASE); return; } for (UINT i = 0; i < pTypesInfo->NumberOfTypes; i++) { if (pTypesInfo->TypeInformation[i].TypeName.Buffer != NULL) { if (_wcsicmp(pTypesInfo->TypeInformation[i].TypeName.Buffer, L"DebugObject") == 0) { AfxMessageBox(L"發現OD"); VirtualFree (pTypesInfo, 0, MEM_RELEASE); return; } } } AfxMessageBox(L"沒有OD!"); VirtualFree (pTypesInfo, 0, MEM_RELEASE); } } }
DebugObjectHandle
/************************************************************************/ /* ProcessDebugObjectHandle */ /************************************************************************/ void CAntiDebugDlg::OnBnClickedButton22() { // TODO: 在此添加控件通知處理程序代碼 HANDLE hProcess = NULL; DWORD dwResult; PFNZwQueryInformationProcess pfunc_ZwQueryInformationProcess; HMODULE hModule = LoadLibrary(L"ntdll.dll"); pfunc_ZwQueryInformationProcess = (PFNZwQueryInformationProcess)GetProcAddress(hModule, "ZwQueryInformationProcess"); pfunc_ZwQueryInformationProcess( GetCurrentProcess(), ProcessDebugObjectHandle, &dwResult, 4, NULL ); if (dwResult != 0) { AfxMessageBox(L"檢測到調試器"); } else { AfxMessageBox(L"沒有檢測到調試器"); } }
一篇關於Anti不錯的文章,不過是英文的,能夠參考:anti-unpackers.pdf
http://www.openrce.org/reference_library/anti_reversing_view/34/INT%202D%20Debugger%20Detection/