详解反调试技术

反调试技术,恶意代码用它识别是否被调试,或者让调试器失效。恶意代码编写者意识到分析人员经常使用调试器来观察恶意代码的操作,因此他们使用反调试技术尽可能地延长恶意代码的分析时间。为了阻止调试器的分析,当恶意代码意识到自己被调试时,它们可能改变正常的执行路径或者修改自身程序让自己崩溃,从而增加调试时间和复杂度。很多种反调试技术可以达到反调试效果。这里介绍当前常用的几种反调试技术,同时也会介绍一些逃避反调试的技巧。
一.探测Windows调试器
恶意代码会使用多种技术探测调试器调试它的痕迹,其中包括使用Windows API、手动检测调试器人工痕迹的内存结构,查询调试器遗留在系统中的痕迹等。调试器探测是恶意代码最常用的反调试技术。
1.使用Windows API
使用Windows API函数检测调试器是否存在是最简单的反调试技术。Windows操作系统中提供了这样一些API,应用程序可以通过调用这些API,来检测自己是否正在被调试。这些API中有些是专门用来检测调试器的存在的,而另外一些API是出于其他目的而设计的,但也可以被改造用来探测调试器的存在。其中很小部分API函数没有在微软官方文档显示。通常,防止恶意代码使用API进行反调试的最简单的办法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的API函数,或者修改这些API函数的返回值,确保恶意代码执行合适的路径。与这些方法相比,较复杂的做法是挂钩这些函数,如使用rootkit技术。
1.1IsDebuggerPresent
IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。

BOOL CheckDebug()
{
	return IsDebuggerPresent();
}

1.2CheckRemoteDebuggerPresent
CheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试。

BOOL CheckDebug()
{
	BOOL ret;
	CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);
	return ret;
}

1.3NtQueryInformationProcess
这个函数是Ntdll.dll中一个原生态API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。

BOOL CheckDebug()
{
	int debugPort = 0;
	HMODULE hModule = LoadLibrary("Ntdll.dll");
	NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
	NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort, sizeof(debugPort), NULL);
	return debugPort != 0;
}

BOOL CheckDebug()
{
	HANDLE hdebugObject = NULL;
	HMODULE hModule = LoadLibrary("Ntdll.dll");
	NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
	NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &hdebugObject, sizeof(hdebugObject), NULL);
	return hdebugObject != NULL;
}

BOOL CheckDebug()
{
	BOOL bdebugFlag = TRUE;
	HMODULE hModule = LoadLibrary("Ntdll.dll");
	NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
	NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &bdebugFlag, sizeof(bdebugFlag), NULL);
	return bdebugFlag != TRUE;
}

1.4GetLastError
编写应用程序时,经常需要涉及到错误处理问题。许多函数调用只用TRUE和FALSE来表明函数的运行结果。一旦出现错误,MSDN中往往会指出请用GetLastError()函数来获得错误原因。恶意代码可以使用异常来破坏或者探测调试器。调试器捕获异常后,并不会立即将处理权返回被调试进程处理,大多数利用异常的反调试技术往往据此来检测调试器。多数调试器默认的设置是捕获异常后不将异常传递给应用程序。如果调试器不能将异常结果正确返回到被调试进程,那么这种异常失效可以被进程内部的异常处理机制探测。
对于OutputDebugString函数,它的作用是在调试器中显示一个字符串,同时它也可以用来探测调试器的存在。使用SetLastError函数,将当前的错误码设置为一个任意值。如果进程没有被调试器附加,调用OutputDebugString函数会失败,错误码会重新设置,因此GetLastError获取的错误码应该不是我们设置的任意值。但如果进程被调试器附加,调用OutputDebugString函数会成功,这时GetLastError获取的错误码应该没改变。

BOOL CheckDebug()
{
	DWORD errorValue = 12345;
	SetLastError(errorValue);
	OutputDebugString("Test for debugger!");
	if (GetLastError() == errorValue)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

对于DeleteFiber函数,如果给它传递一个无效的参数的话会抛出ERROR_INVALID_PARAMETER异常。如果进程正在被调试的话,异常会被调试器捕获。所以,同样可以通过验证LastError值来检测调试器的存在。如代码所示,0x57就是指ERROR_INVALID_PARAMETER。

BOOL CheckDebug()
{
	char fib[1024] = {0};
	DeleteFiber(fib);
	return (GetLastError() != 0x57);
}

同样还可以使用CloseHandle、CloseWindow产生异常,使得错误码改变。

BOOL CheckDebug()
{
	DWORD ret = CloseHandle((HANDLE)0x1234);
	if (ret != 0 || GetLastError() != ERROR_INVALID_HANDLE)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

BOOL CheckDebug()
{
	DWORD ret = CloseWindow((HWND)0x1234);
	if (ret != 0 || GetLastError() != ERROR_INVALID_WINDOW_HANDLE)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

1.5ZwSetInformationThread
ZwSetInformationThread拥有两个参数,第一个参数用来接收当前线程的句柄,第二个参数表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11),使用语句ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);调用该函数后,调试进程就会被分离出来。该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程序,因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终停止调试。还有一个函数DebugActiveProcessStop用来分离调试器和被调试进程,从而停止调试。两个API容易混淆,需要牢记它们的区别。
2.手动检测数据结构
虽然使用Windows API是探测调试器存在的最简单办法,但手动检查数据结构是恶意代码编写者最常使用的办法。这是因为很多时候通过Windows API实现的反调试技术无效,例如这些API函数被rootkit挂钩,并返回错误信息。因此,恶意代码编写者经常手动执行与这些API功能相同的操作。在手动检测中,PEB结构中的一些标志暴露了调试器存在的信息。这里,我们关注检测调试器存在常用的一些标志。
2.1检测BeingDebugged属性
Windows操作系统维护着每个正在运行的进程的PEB结构,它包含与这个进程相关的所有用户态参数。这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址,以及调试器状态。


进程运行时,位置fs:[30h]指向PEB的基地址。为了实现反调试技术,恶意代码通过这个位置检查BeingDebugged标志,这个标志标识进程是否正在被调试。

BOOL CheckDebug()
{
	int result = 0;
	__asm
	{
		mov eax, fs:[30h]
		mov al, BYTE PTR [eax + 2] 
		mov result, al
	}
	return result != 0;
}

这种检查有多种形式,最终,条件跳转决定代码的路径。避免这种问题最简单的方法是在执行跳转指令前,手动修改零标志,强制执行跳转(或者不跳转)。
可以或者手动修改BeingDebugged属性值为0。在OllyDbg中安装命令行插件,为了启动该插件,用OllyDbg加载恶意代码,选择Plugins->Command Line->Command Line选项,在命令行窗口输入下面的命令。


如图所示,这条命令会将BeingDebugged属性转储到转储面板窗口。右键单击BeingDebugged属性,选择Binary->Fill With 00's,这时属性被设置为0。
OllyDbg的一些插件可以帮助我们修改BeingDebugged标志。其中最流行的有HideDebugger、Hidedebug和PhantOm。以PhantOm为例,同样将dll文件拷贝到OllyDbg的安装目录下就会自动安装。选择Plugins->PhantOm->Options选项,勾选hide from PEB即可。


2.2检测ProcessHeap属性
Reserved数组中一个未公开的位置叫作ProcessHeap,它被设置为加载器为进程分配的第一个堆的位置。ProcessHeap位于PEB结构的0x18处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建。这些属性叫作ForceFlags和Flags。在Windows XP系统中,ForceFlags属性位于堆头部偏移量0x10处;在Windows 7系统中,对于32位的应用程序来说ForceFlags属性位于堆头部偏移量0x44处。
BOOL CheckDebug()
{
	int result = 0;
	DWORD dwVersion = GetVersion();
	DWORD dwWindowsMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
	//for xp
	if (dwWindowsMajorVersion == 5)
	{
		__asm
		{
			mov eax, fs:[30h]
			mov eax, [eax + 18h]
			mov eax, [eax + 10h]
			mov result, eax
		}
	}
	else
	{
		__asm
		{
			mov eax, fs:[30h]
			mov eax, [eax + 18h]
			mov eax, [eax + 44h]
			mov result, eax
		}
	}
	return result != 0;
}

同样,恶意代码也可以检查Windows XP系统中偏移量0x0C处,或者Windows 7系统中偏移量0x40处的Flags属性。这个属性总与ForceFlags属性大致相同,但通常情况下Flags与值2进行比较。

BOOL CheckDebug()
{
	int result = 0;
	DWORD dwVersion = GetVersion();
	DWORD dwWindowsMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
	//for xp
	if (dwWindowsMajorVersion == 5)
	{
		__asm
		{
			mov eax, fs:[30h]
			mov eax, [eax + 18h]
			mov eax, [eax + 0ch]
			mov result, eax
		}
	}
	else
	{
		__asm
		{
			mov eax, fs:[30h]
			mov eax, [eax + 18h]
			mov eax, [eax + 40h]
			mov result, eax
		}
	}
	return result != 2;
}
避免这种问题方法和前面的差不多。如果用OllyDbg的命令行插件修改,输入的命令为dump ds:[fs:[30]+0x18]+0x10。如果用PhantOm插件,它会禁用调试堆创建功能而不需要手动设置。
2.3检测NTGlobalFlag
由于调试器中启动进程与正常模式下启动进程有些不同,所以它们创建内存堆的方式也不同。系统使用PEB结构偏移量0x68处的一个未公开位置,来决定如何创建堆结构。如果这个位置的值为0x70,我们就知道进程正运行在调试器中。
BOOL CheckDebug()
{
	int result = 0;
	__asm
	{
		mov eax, fs:[30h]
		mov eax, [eax + 68h]
		and eax, 0x70
		mov result, eax
	}
	return result != 0;
}
操作系统创建堆时,值0x70是下列标志的一个组合。如果进程从调试器启动,那么进程的这些标志将被设置。
(FLG_HEAP_ENABLE_TAIL_CHECK|FLG_HEAP_ENABLE_FREE_CHECK|FLG_HEAP_VALIDATE_PARAMETERS)
避免这种问题方法和前面的差不多。如果用OllyDbg的命令行插件修改,输入的命令为dump fs:[30]+0x68。如果用PhantOm插件,它会逃避使用NTGlobalFlag的反调试技术而不需要手动设置。
3.系统痕迹检测
通常,我们使用调试工具来分析恶意代码,但这些工具会在系统中驻留一些痕迹。恶意代码通过搜索这种系统痕迹,来确定你是否试图分析它。
3.1查找调试器引用的注册表项
下面是调试器在注册表中的一个常用位置。
SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系统)
SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系统)
该注册表项指定当应用程序发生错误时,触发哪一个调试器。默认情况下,它被设置为Dr.Watson。如果该这册表的键值被修改为OllyDbg,则恶意代码就可能确定它正在被调试。
BOOL CheckDebug()
{
	BOOL is_64;
	IsWow64Process(GetCurrentProcess(), &is_64);
	HKEY hkey = NULL;
    char key[] = "Debugger";
	char reg_dir_32bit[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug";
	char reg_dir_64bit[] = "SOFTWARE\\Wow6432Node\\Microsoft\\WindowsNT\\CurrentVersion\\AeDebug";
	DWORD ret = 0;
	if (is_64)
	{
		ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_64bit, &hkey);
	}
	else
	{
		ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_32bit, &hkey);
	}
	if (ret != ERROR_SUCCESS)
	{
		return FALSE;
	}
	char tmp[256];
    DWORD len = 256;
    DWORD type;
    ret = RegQueryValueExA(hkey, key, NULL, &type, (LPBYTE)tmp, &len);
	if (strstr(tmp, "OllyIce")!=NULL || strstr(tmp, "OllyDBG")!=NULL || strstr(tmp, "WinDbg")!=NULL || strstr(tmp, "x64dbg")!=NULL || strstr(tmp, "Immunity")!=NULL)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

3.2查找窗体信息
FindWindow函数检索处理顶级窗口的类名和窗口名称匹配指定的字符串。

BOOL CheckDebug()
{
	if (FindWindowA("OLLYDBG", NULL)!=NULL || FindWindowA("WinDbgFrameClass", NULL)!=NULL || FindWindowA("QWidget", NULL)!=NULL)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

EnumWindows函数枚举所有屏幕上的顶层窗口,并将窗口句柄传送给应用程序定义的回调函数。

BOOL CALLBACK EnumWndProc(HWND hwnd, LPARAM lParam)  
{  
	char cur_window[1024];
    GetWindowTextA(hwnd, cur_window, 1023);
	if (strstr(cur_window, "WinDbg")!=NULL || strstr(cur_window, "x64_dbg")!=NULL || strstr(cur_window, "OllyICE")!=NULL || strstr(cur_window, "OllyDBG")!=NULL || strstr(cur_window, "Immunity")!=NULL)
	{
		*((BOOL*)lParam) = TRUE;
	}
	return TRUE;
} 

BOOL CheckDebug()
{
	BOOL ret = FALSE;
	EnumWindows(EnumWndProc, (LPARAM)&ret); 
	return ret;
}

GetForegroundWindow获取一个前台窗口的句柄。

 
BOOL CheckDebug()
{
	char fore_window[1024];
	GetWindowTextA(GetForegroundWindow(), fore_window, 1023);
	if (strstr(fore_window, "WinDbg")!=NULL || strstr(fore_window, "x64_dbg")!=NULL || strstr(fore_window, "OllyICE")!=NULL || strstr(fore_window, "OllyDBG")!=NULL || strstr(fore_window, "Immunity")!=NULL)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

为了防范这种技术,在OllyDbg的PhantOm插件中勾选hide OllyDbg windows。
3.3查找进程信息

BOOL CheckDebug()
{
	DWORD ID;
	DWORD ret = 0;
	PROCESSENTRY32 pe32;
	pe32.dwSize = sizeof(pe32); 
	HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 
	if(hProcessSnap == INVALID_HANDLE_VALUE) 
	{ 
		return FALSE; 
	}
	BOOL bMore = Process32First(hProcessSnap, &pe32); 
	while(bMore)
	{
		if (stricmp(pe32.szExeFile, "OllyDBG.EXE")==0 || stricmp(pe32.szExeFile, "OllyICE.exe")==0 || stricmp(pe32.szExeFile, "x64_dbg.exe")==0 || stricmp(pe32.szExeFile, "windbg.exe")==0 || stricmp(pe32.szExeFile, "ImmunityDebugger.exe")==0)
		{
			return TRUE;
		}
		bMore = Process32Next(hProcessSnap, &pe32); 
	}
	CloseHandle(hProcessSnap); 
	return FALSE;
}
二.识别调试器行为
在逆向工程中,为了帮助恶意代码分析人员进行分析,可以使用调试器设置一个断点,或是单步执行一个进程。然而,在调试器中执行这些操作时,它们会修改进程中的代码。因此,恶意代码常使用几种反调试技术探测软件/硬件断点、完整性校验、时钟检测等几种类型的调试器行为。直接运行恶意代码与在调试器中运行恶意代码也会在一些细节上不同,如父进程信息、STARTUPINFO信息、SeDebugPrivilege权限等。
1.软件断点检查
调试器设置断点的基本机制是用软件中断指令INT 3临时替换运行程序中的一条指令,然后当程序运行到这条指令时,调用调试异常处理例程。INT 3指令的机器码是0xCC,因此无论何时,使用调试器设置一个断点,它都会插入一个0xCC来修改代码。恶意代码常用的一种反调试技术是在它的代码中查找机器码0xCC,来扫描调试器对它代码的INT 3修改。repne scasb指令用于在一段数据缓冲区中搜索一个字节。EDI需指向缓冲区地址,AL则包含要找的字节,ECX设为缓冲区的长度。当ECX=0或找到该字节时,比较停止。
BOOL CheckDebug()
{
	PIMAGE_DOS_HEADER pDosHeader;
	PIMAGE_NT_HEADERS32 pNtHeaders;
	PIMAGE_SECTION_HEADER pSectionHeader;
	DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL); 
	pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
	pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
	pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) + 
					 (WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
	DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage; 
	DWORD dwCodeSize = pSectionHeader->SizeOfRawData;    
	BOOL Found = FALSE;
	__asm
	{
		cld             
		mov     edi,dwAddr
		mov     ecx,dwCodeSize
		mov     al,0CCH 
		repne   scasb
		jnz     NotFound
		mov Found,1
NotFound:         
	}
	return Found;
}

2.硬件断点检查


在OllyDbg的寄存器窗口按下右键,点击View debug registers可以看到DR0、DR1、DR2、DR3、DR6和DR7这几个寄存器。DR0、Dr1、Dr2、Dr3用于设置硬件断点,由于只有4个硬件断点寄存器,所以同时最多只能设置4个硬件断点。DR4、DR5由系统保留。  DR6、DR7用于记录Dr0-Dr3中断点的相关属性。如果没有硬件断点,那么DR0、DR1、DR2、DR3这4个寄存器的值都为0。

BOOL CheckDebug()
{
	CONTEXT context;  
    HANDLE hThread = GetCurrentThread();  
    context.ContextFlags = CONTEXT_DEBUG_REGISTERS;  
    GetThreadContext(hThread, &context);  
    if (context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3!=0)   
    {  
        return TRUE;  
    }  
    return FALSE;  
}
3.执行代码校验和检查
恶意代码可以计算代码段的校验并实现与扫描中断相同的目的。与扫描0xCC不同,这种检查仅执行恶意代码中机器码CRC或者MD5校验和检查。
BOOL CheckDebug()
{
	PIMAGE_DOS_HEADER pDosHeader;
	PIMAGE_NT_HEADERS32 pNtHeaders;
	PIMAGE_SECTION_HEADER pSectionHeader;
	DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL); 
	pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
	pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
	pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +
		(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
	DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage; 
	DWORD dwCodeSize = pSectionHeader->SizeOfRawData;    
	DWORD checksum = 0;
	__asm
	{
		cld
		mov     esi, dwAddr
		mov     ecx, dwCodeSize
		xor eax, eax
	checksum_loop :
		movzx    ebx, byte ptr[esi]
		add        eax, ebx
		rol eax, 1
		inc esi
		loop       checksum_loop
		mov checksum, eax
	}
	if (checksum != 0x46ea24)
	{
		return FALSE;
	}
	else
	{
		return TRUE;
	}
}
4.时钟检测
被调试时,进程的运行速度大大降低,例如,单步调试大幅降低恶意代码的运行速度,所以时钟检测是恶意代码探测调试器存在的最常用方式之一。有如下两种用时钟检测来探测调试器存在的方法。
记录一段操作前后的时间戳,然后比较这两个时间戳,如果存在滞后,则可以认为存在调试器。
记录触发一个异常前后的时间戳。如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。默认情况下,调试器处理异常时需要人为干预,这导致大量延迟。虽然很多调试器允许我们忽略异常,将异常直接返回程序,但这样操作仍然存在不小的延迟。
4.1使用rdtsc指令
较常用的时钟检测方法是利用rdtsc指令(操作码0x0F31),它返回至系统重新启动以来的时钟数,并且将其作为一个64位的值存入EDX:EAX中。恶意代码运行两次rdtsc指令,然后比较两次读取之间的差值。
BOOL CheckDebug()
{
	DWORD time1, time2;
	__asm
	{
		rdtsc
		mov time1, eax
		rdtsc
		mov time2, eax
	}
	if (time2 - time1 < 0xff)
	{
		return FALSE;
	}
	else
	{
		return TRUE;
	}
}
4.2使用QueryPerformanceCounter和GetTickCount
同rdtsc指令一样,这两个Windows API函数也被用来执行一个反调试的时钟检测。使用这种方法的前提是处理器有高分辨率能力的计数器-寄存器,它能存储处理器活跃的时钟数。为了获取比较的时间差,调用两次QueryPerformanceCounter函数查询这个计数器。若两次调用之间花费的时间过于长,则可以认为正在使用调试器。GetTickCount函数返回最近系统重启时间与当前时间的相差毫秒数(由于时钟计数器的大小原因,计数器每49.7天就被重置一次)。
BOOL CheckDebug()
{
	DWORD time1 = GetTickCount();
	__asm
	{
		mov     ecx,10           
		mov     edx,6               
		mov     ecx,10
	}
	DWORD time2 = GetTickCount();
	if (time2-time1 > 0x1A)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

5.判断父进程是否是explorer.exe 
一般双击运行的进程的父进程都是explorer.exe,但是如果进程被调试父进程则是调试器进程。也就是说如果父进程不是explorer.exe则可以认为程序正在被调试。

BOOL CheckDebug()
{
	LONG                      status;  
    DWORD                     dwParentPID = 0;  
    HANDLE                    hProcess;  
    PROCESS_BASIC_INFORMATION pbi;  
	int pid = getpid();
    hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);  
    if(!hProcess)  
        return -1;  
	PNTQUERYINFORMATIONPROCESS  NtQueryInformationProcess = (PNTQUERYINFORMATIONPROCESS)GetProcAddress(GetModuleHandleA("ntdll"),"NtQueryInformationProcess");
	status = NtQueryInformationProcess(hProcess,SystemBasicInformation,(PVOID)&pbi,sizeof(PROCESS_BASIC_INFORMATION),NULL);
	PROCESSENTRY32 pe32;
	pe32.dwSize = sizeof(pe32); 
	HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 
	if(hProcessSnap == INVALID_HANDLE_VALUE) 
	{ 
		return FALSE; 
	}
	BOOL bMore = Process32First(hProcessSnap, &pe32); 
	while(bMore)
	{
		if (pbi.InheritedFromUniqueProcessId == pe32.th32ProcessID)
		{
			if (stricmp(pe32.szExeFile, "explorer.exe")==0)
			{
				CloseHandle(hProcessSnap);
				return FALSE;
			}
			else
			{
				CloseHandle(hProcessSnap);
				return TRUE;
			}
		}
		bMore = Process32Next(hProcessSnap, &pe32); 
	}
	CloseHandle(hProcessSnap);
}
6.判断STARTUPINFO信息
explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0。所以可以利用STARTUPINFO来判断程序是否在被调试。
BOOL CheckDebug()
{
	STARTUPINFO si;
	GetStartupInfo(&si);
	if (si.dwX!=0 || si.dwY!=0 || si.dwFillAttribute!=0 || si.dwXSize!=0 || si.dwYSize!=0 || si.dwXCountChars!=0 || si.dwYCountChars!=0)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

7.判断是否具有SeDebugPrivilege权限
默认情况下进程是没有SeDebugPrivilege权限的,但是当进程通过调试器启动时,由于调试器本身启动了SeDebugPrivilege权限,当调试进程被加载时SeDebugPrivilege也就被继承了。所以我们可以检测进程的SeDebugPrivilege权限来间接判断是否存在调试器,而对SeDebugPrivilege权限的判断可以用能否打开csrss.exe进程来判断。

BOOL CheckDebug()
{
	DWORD ID;
	DWORD ret = 0;
	PROCESSENTRY32 pe32;
	pe32.dwSize = sizeof(pe32); 
	HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 
	if(hProcessSnap == INVALID_HANDLE_VALUE) 
	{ 
		return FALSE; 
	}
	BOOL bMore = Process32First(hProcessSnap, &pe32); 
	while(bMore)
	{
		if (strcmp(pe32.szExeFile, "csrss.exe")==0)
		{
			ID = pe32.th32ProcessID;
			break;
		}
		bMore = Process32Next(hProcessSnap, &pe32); 
	}
	CloseHandle(hProcessSnap); 
	if (OpenProcess(PROCESS_QUERY_INFORMATION, NULL, ID) != NULL)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

三.干扰调试器的功能
恶意代码可以用一些技术来干扰调试器的正常运行。例如线程本地存储(TLS)回调、插入中断、异常等。这些技术当且仅当程序处于调试器控制之下时才试图扰乱程序的运行。
1.使用TLS回调
Thread Local Storage(TLS),即线程本地存储,是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。下面是一个简单的TLS回调的例子,TLS_CALLBACK1函数在main函数执行前调用IsDebuggerPresent函数检查它是否正在被调试。

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

void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD dwReason, PVOID Reserved);

#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
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif

PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK1,0};
#pragma data_seg ()
#pragma const_seg ()

#include <iostream>

void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
	if (IsDebuggerPresent())
	{
		printf("TLS_CALLBACK: Debugger Detected!\n");
	}
	else
	{
		printf("TLS_CALLBACK: No Debugger Present!\n");
	}
}

int main(int argc, char* argv[])
{
	printf("233\n");
	return 0;
}

要在程序中使用TLS,必须为TLS数据单独建一个数据段,用相关数据填充此段,并通知链接器为TLS数据在PE文件头中添加数据。_tls_callback[]数组中保存了所有的TLS回调函数指针。数组必须以NULL指针结束,且数组中的每一个回调函数在程序初始化时都会被调用,程序员可按需要添加。但程序员不应当假设操作系统已何种顺序调用回调函数。如此则要求在TLS回调函数中进行反调试操作需要一定的独立性。
正常运行这个程序会打印下面的内容。
TLS_CALLBACK: No Debugger Present!
233
如果把在OllyDbg中运行,在OllyDbg暂停之前会打印下面的内容。
TLS_CALLBACK: Debugger Detected!
使用PEview查看.tls段,可以发现TLS回调函数。通常情况下,正常程序不使用.tls段,如果在可执行程序中看到.tls段,应该立即怀疑它使用了反调试技术。


在OllyDbg中选择Options->Debugging Options->Events,然后设置System break-point作为第一个暂停的位置,这样就可以让OllyDbg在TLS回调执行前暂停。


在IDA Pro中按Ctrl+E快捷键看到二进制的入口点,该组合键的作用是显示应用程序所有的入口点,其中包括TLS回调。双击函数名可以浏览回调函数。



由于TLS回调已广为人知,因此同过去相比,恶意代码使用它的次数已经明显减少。为数不多的合法程序使用TLS回调,所以可执行程序中的.tls段特别突出。
2.利用中断
因为调试器使用INT 3来设置软件断点,所以一种反调试技术就是在合法代码段中插入0xCC(INT 3)欺骗调试器,使其认为这些0xCC机器码是自己设置的断点。

BOOL CheckDebug()
{
      __try
      {
            __asm int 3
      }
      __except(1)
      {
            return FALSE;
      }
      return TRUE;
}
除了使用_try和_except以外还可以直接使用汇编代码安装SEH。在下面的代码中如果进程没有处于调试中,则正常终止;如果进程处于调试中,则跳转到非法地址0xFFFFFFFF处,无法继续调试。
#include "stdio.h"
#include "windows.h"
#include "tchar.h"

void AD_BreakPoint()
{
    printf("SEH : BreakPoint\n");

    __asm {
        // install SEH
        push handler
        push DWORD ptr fs:[0]
        mov DWORD ptr fs:[0], esp
        
        // generating exception
        int 3

        // 1) debugging
        //    go to terminating code
        mov eax, 0xFFFFFFFF
        jmp eax                 // process terminating!!!

        // 2) not debugging
        //    go to normal code
handler:
        mov eax, dword ptr ss:[esp+0xc]
        mov ebx, normal_code
        mov dword ptr ds:[eax+0xb8], ebx
        xor eax, eax
        retn

normal_code:
        //   remove SEH
        pop dword ptr fs:[0]
        add esp, 4
    }

    printf("  => Not debugging...\n\n");
}

int _tmain(int argc, TCHAR* argv[])
{
    AD_BreakPoint();

    return 0;
}

双字节操作码0xCD03也可以产生INT 3中断,这是恶意代码干扰WinDbg调试器的有效方法。在调试器外,0xCD03指令产生一个STATUS_BREAKPOINT异常。然而在WinDbg调试器内,由于断点通常是单字节机器码0xCC,因此WinDbg会捕获这个断点然后将EIP加1字节。这可能导致程序在被正常运行的WinDbg调试时,执行不同的指令集(OllyDbg可以避免双字节INT 3的攻击)。

BOOL CheckDebug()
{
      __try
      {
            __asm
            {
                  __emit 0xCD
                  __emit 0x03
            }
      }
      __except(1)
      {
            return FALSE;
      }
      return TRUE; 
}

INT 2D原为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。但程序调试运行时不会触发异常,只是忽略。INT 2D指令在ollydbg中有两个有趣的特性。在调试模式中执行INT 2D指令,下一条指令的第一个字节将被忽略。使用StepInto(F7)或者StepOver(F8)命令跟踪INT 2D指令,程序不会停在下一条指令开始的地方,而是一直运行,就像RUN(F9)一样。在下面的代码中,程序调试运行时,执行INT 2D之后不会运行SEH,而是跳过NOP,把bDebugging标志设置为1,跳转到normal_code;程序正常运行时,执行INT 2D之后触发SEH,在异常处理器中设置EIP并把bDebugging标志设置为0。

BOOL CheckDebug()
{
    BOOL bDebugging = FALSE;

    __asm {
        // install SEH
        push handler
        push DWORD ptr fs:[0]
        mov DWORD ptr fs:[0], esp
        
        int 0x2d

        nop
        mov bDebugging, 1
        jmp normal_code

handler:
        mov eax, dword ptr ss:[esp+0xc]
        mov dword ptr ds:[eax+0xb8], offset normal_code
        mov bDebugging, 0
        xor eax, eax
        retn

normal_code:
        //   remove SEH
        pop dword ptr fs:[0]
        add esp, 4
    }

    printf("Trap Flag (INT 2D)\n");
    if( bDebugging )  return 1;
    else              return 0;
}

片内仿真器(ICE)断点指令ICEBP(操作码0xF1)是Intel未公开的指令之一。由于使用ICE难以在任意位置设置断点,因此ICEBP指令被设计用来降低使用ICE设置断点的难度。运行ICEBP指令将会产生一个单步异常,如果通过单步调试跟踪程序,调试器会认为这是单步调试产生的异常,从而不执行先前设置的异常处理例程。利用这一点,恶意代码使用异常处理例程作为它的正常执行流程。为了防止这种反调试技术,执行ICEBP指令时不要使用单步。

BOOL CheckDebug()
{
      __try
      {
            __asm __emit 0xF1
      }
      __except(1)
      {
            return FALSE;
      }
      return TRUE;
}

3.设置陷阱标志位
EFLAGS寄存器的第八个比特位是陷阱标志位。如果设置了,就会产生一个单步异常。

BOOL CheckDebug()
{
      __try
      {
            __asm
            {
                  pushfd     
                  or word ptr[esp], 0x100            
                  popfd
                  nop        
            }
      }
      __except(1)
      {
            return FALSE;
      }
      return TRUE;
}

4.使用异常
前面已经讨论了各种使用异常机制的反调试手段。
4.1RaiseException
RaiseException函数产生的若干不同类型的异常可以被调试器捕获。

BOOL TestExceptionCode(DWORD dwCode)
{
      __try
      {
            RaiseException(dwCode, 0, 0, 0);
      }
      __except(1)
      {
            return FALSE;
      }
      return TRUE;
}
 
BOOL CheckDebug()
{
      return TestExceptionCode(DBG_RIPEXCEPTION);    
}

4.2SetUnhandledExceptionFilter
进程中发生异常时若SEH未处理或注册的SEH不存在,会调用UnhandledExceptionFilter,它会运行系统最后的异常处理器。UnhandledExceptionFilter内部调用了前面提到过的NtQueryInformationProcess以判断是否正在调试进程。若进程正常运行,则运行最后的异常处理器;若进程处于调试,则将异常派送给调试器。SetUnhandledExceptionFilter函数可以修改系统最后的异常处理器。下面的代码先触发异常,然后在新注册的最后的异常处理器内部判断进程正常运行还是调试运行。进程正常运行时pExcept->ContextRecord->Eip+=4;将发生异常的代码地址加4使得其能够继续运行;进程调试运行时产生无效的内存访问异常,从而无法继续调试。

#include "stdio.h"
#include "windows.h"
#include "tchar.h"

LPVOID g_pOrgFilter = 0;

LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS pExcept)
{
    SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)g_pOrgFilter);

    // 8900    MOV DWORD PTR DS:[EAX], EAX
    // FFE0    JMP EAX
    pExcept->ContextRecord->Eip += 4;

    return EXCEPTION_CONTINUE_EXECUTION;
}

void AD_SetUnhandledExceptionFilter()
{
    printf("SEH : SetUnhandledExceptionFilter()\n");

    g_pOrgFilter = (LPVOID)SetUnhandledExceptionFilter(
                                (LPTOP_LEVEL_EXCEPTION_FILTER)ExceptionFilter);

    __asm {
        xor eax, eax;
        mov dword ptr [eax], eax
        jmp eax                     
    }
    
    printf("  => Not debugging...\n\n");
}

int _tmain(int argc, TCHAR* argv[])
{
    AD_SetUnhandledExceptionFilter();

    return 0;
}

在OllyDbg中,选择Options->Debugging Options->Exceptions来设置把异常传递给应用程序。


四.调试器漏洞
与所有软件一样,调试器也存在漏洞,有时恶意代码编写者为了防止被调试,会攻击这些漏洞。这里我们展示几种OllyDbg调试器处理PE格式文件时的常见漏洞。
1.PE头漏洞
OllyDbg非常严格地遵循了微软对PE文件头部的规定。在PE文件的头部,通常存在一个叫作IMAGE_OPTIONAL_HEADER的结构。


需要特别注意这个结构中的最后几个元素。NumberOfRvaAndSizes属性标识后面DataDirectory数组中的元素个数。DataDirectory数组表示在这个可执行文件中的什么地方可找到其他导入可执行模块的位置,它位于可选头部结构的末尾,是一个比IMAGE_DATA_DIRECTORY略大一些的数组。数组中每个结构目录都指明了目录的相对虚拟地址和大小。DataDirectory数组的大小被设置为IMAGE_NUMBEROF_DIRECTORY_ENTRIES,它等于0x10。因为DataDirectory数组不足以容纳超过0x10个目录项,所以当NumberOfRvaAndSizes大于0x10时,Windows加载器将会忽略NumberOfRvaAndSizes。OllyDbg遵循了这个标准,并且无论NumberOfRvaAndSizes是什么值,OllyDbg都使用它。因此,设置NumberOfRvaAndSizes为一个超过0x10的值,会导致在程序退出前,OllyDbg对用户弹出一个窗口。如图所示,使用LordPE打开可执行文件,修改RVA数及大小并保存,再用OllyDbg打开,会提示错误Bad or unknown format of 32-bit executable file。



另一种PE头的欺骗与节头部有关。文件内容中包含的节包括代码节、数据节、资源节,以及一些其他信息节。每个节都拥有一个IMAGE_SECTION_HEADER结构的头部。


VirtualSize和SizeOfRawData是其中两个比较重要的属性。根据微软对PE的规定,VirtualSize应该包含载入到内存的节大小,SizeOfRawData应该包含节在硬盘中的大小。Windows加载器使用VirtualSize和SizeOfRawData中的最小值将节数据映射到内存。如果SizeOfRawData大于VirtualSize,则仅将VirtualSize大小的数据复制入内存,忽略其余数据。因为OllyDbg仅使用SizeOfRawData,所以设置SizeOfRawData为一个类似0x77777777的大数值时,会导致OllyDbg崩溃。如图所示,使用LordPE打开可执行文件,点击区段,在区段表上右击,点击编辑区段,修改物理大小并保存,再用OllyDbg打开,会提示同样的错误。


对抗这种反调试技术的最简单方法是用类似的编辑器手动修改PE头部。OllyDbg2.0和WinDbg不存在这种漏洞。
2.OutputDebugString漏洞
恶意代码常尝试利用OllyDbg1.1的格式化字符串漏洞,为OutputDebugString函数提供一个%s字符串的参数,让OllyDbg崩溃。因此,需要注意程序中可疑的OutputDebugString调用,例如OutputDebugString("%s%s%s%s%s%s%s%s%s")。如果执行了这个调用,OllyDbg将会崩溃。

总结

最后让我们总结一下提到的内容。腾讯2016游戏安全技术竞赛有一道题,大概意思就是给一个exe,要求编写一个Tencent2016D.dll,并导出多个接口函数CheckDebugX。X为1-100之间的数字,比如CheckDebug1,CheckDebug8,...,CheckDebug98。函数功能是检测自己是否处于被调试状态,是返回TRUE,否则返回FALSE。函数的原型都是typedef BOOL (WINAPI* Type_CheckDebug)();。编译好dll之后,放在Tencent2016D.exe的同目录,运行Tencent2016D.exe,点击检测按钮,正常运行时,函数接口输出为0,调试运行或者被附加运行时,接口输出1。我们把提到的知识综合一下完成这道题目。
解题的参考代码和题目相关信息:https://github.com/houjingyi233/test-debug/
参考资料:《恶意代码分析实战》第16章反调试技术、《逆向工程核心原理》第51章静态反调试技术&第52章动态反调试技术、freebuf技术分享:利用异常实现反调试天枢战队官方博客吾爱破解论坛、《windows anti-debugger reference》



  • 48
    点赞
  • 172
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
方案是为解决特定问题或达成特定目标而制定的一系列计划或步骤。它的作用是提供一种系统性的方法,以有效地应对挑战、优化流程或实现目标。以下是方案的主要作用: 问题解决: 方案的核心目标是解决问题。通过系统性的规划和执行,方案能够分析问题的根本原因,提供可行的解决方案,并引导实施过程,确保问题得到合理解决。 目标达成: 方案通常与明确的目标相关联,它提供了一种达成这些目标的计划。无论是企业战略、项目管理还是个人发展,方案的制定都有助于明确目标并提供达成目标的路径。 资源优化: 方案在设计时考虑了可用资源,以最大化其效用。通过明智的资源分配,方案可以在有限的资源条件下实现最大的效益,提高效率并减少浪费。 风险管理: 方案通常会对潜在的风险进行评估,并制定相应的风险管理策略。这有助于减轻潜在问题的影响,提高方案的可行性和可持续性。 决策支持: 方案提供了决策者所需的信息和数据,以便做出明智的决策。这种数据驱动的方法有助于减少不确定性,提高决策的准确性。 团队协作: 复杂的问题通常需要多个人的协同努力。方案提供了一个共同的框架,帮助团队成员理解各自的职责和任务,促进协作并确保整个团队朝着共同的目标努力。 监控与评估: 方案通常包括监控和评估的机制,以确保实施的有效性。通过定期的评估,可以及时调整方案,以适应变化的环境或新的挑战。 总体而言,方案的作用在于提供一种有序、有计划的方法,以解决问题、实现目标,并在实施过程中最大化资源利用和风险管理。
chm格式,目录如下。 第1章 PE文件格式深入研究 1.1 PE文件格式格式纵览 1.1.1 区块(Section) 1.1.2 相对虚拟地址(Relative Virtual Addresses) 1.1.3 数据目录 1.1.4 输入函数(Importing Functions) 1.2 PE文件结构 1.2.1 The MS-DOS头部 1.2.2 IMAGE_NT_HEADERS头部 1.2.3 区块表(The Section Table) 1.2.4 各种块(Sections)的描述 1.2.5 输出表 1.2.6 输出转向(Export Forwarding) 1.2.7 输入表 1.2.8 绑定输入(Bound import) 1.2.9 延迟装入数据(Delayload Data) 1.2.10 资源 1.2.11 基址重定位(Base Relocations) 1.2.12 调试目录(DebugDirectory) 1.2.13 NET头部 1.2.14 TLS初始化 1.2.15 程序异常数据 第2章 PE分析工具编写 2.1 文件格式检查 2.2 FileHeader和OptionalHeader内容的读取 2.3 得到数据目录(Data Dircetory)信息 2.4 得到块表(SectionTable)信息 2.5 得到输出表(ExportTable)信息 2.6 得到输入表(ImportTable)信息 第3章 Win32 调试API 3.1 Win32调试API原理 3.1.1 调试相关函数简要说明 3.1.2 调试事件 3.1.3 如何在调试时创建并跟踪一个进程 3.1.4 最主要的循环体 3.1.5 如何处理调试事件 3.1.6 线程环境详解 3.1.7 如何在另一个进程中注入代码 3.2 利用调试API编写脱壳机 3.2.1 tElock 0.98脱壳简介 3.2.2 脱壳机的编写 3.3 利用调试API制作内存补丁 3.3.1 跨进程内存存取机制 3.3.2 Debug API机制 第4章 Windows下的异常处理 4.1 基本概念 4.1.1 Windows下的软件异常 4.1.2 未公开的可靠吗 4.2 结构化异常处理(SEH) 4.2.1 异常处理的基本过程 4.2.2 SEH的分类 4.2.3 相关API 4.2.4 SEH相关数据结构 4.3 异常处理程序设计 4.3.1 顶层(top-level)异常处理 4.3.2 线程异常处理 4.3.3 异常处理的堆栈展开(Stack unwind) 4.3.4 异常处理程序设计中的几个注意事项: 4.4 SEH的简单应用 4.4.1 Win9x下利用SEH进ring0 4.4.2 利用SEH实现对自身的单步自跟踪 4.4.3 其它应用 4.5 系统背后的秘密 4.6 VC是如何封装系统提供的SEH机制的 4.6.1 扩展的EXCEPTION_REGISTRATION级相关结构 4.6.2 数据结构组织 4.7 Windows XP下的向量化异常处理(VEH) 第5章 软件加密技术 5.1 调试技术(Anti-Debug) 5.1.1 句柄检测 5.1.2 SoftICE后门指令 5.1.3 int68子类型 5.1.4 ICECream子类型 5.1.5 判断NTICE服务是否运行 5.1.6 INT 1 检测 5.1.7 利用UnhandledExceptionFilter检测 5.1.8 INT 41子类型 5.2 跟踪技术(Anti-Trace) 5.2.1 断点检测 5.2.2 利用SEH跟踪 5.2.3 SMC技术实现 5.3 加载技术(Anti-Loader) 5.3.1 利用TEB检测 5.3.2 利用IsDebuggerPresent函数检测 5.3.3 检查父进程 5.4 DUMP技术(Anti-Dump) 5.5 文件完整性检验 5.5.1 CRC校验实现 5.5.2 校验和(Checksum) 5.5.3 内存映像校验 5.6 监视技术(Anti-Monitor) 5.6.1 窗口方法检测 5.6.2 句柄检测 5.7 静态分析技术 5.7.1 扰乱汇编代码 5.7.2 花指令 5.7.3 信息隐藏 5.8 代码与数据结合技术 5.9 软件保护的若干忠告 第6章 加壳软件编写 6.1 外壳编写基础 6.1.1 判断文件是否是PE格式的EXE文件 6.1.2 文件基本数据的读入 6.1.3 额外数据保留 6.1.4 重定位数据的去除 6.1.5 文件的压缩 6.1.6 资源区块的处理 6.1.7 区块的融合 6.1.8 输入表的处理 6.1.9 外壳部分的编写 6.1.10 将外壳部分添加至原程序 6.1.10 小结 6.2 加壳程序综合运用的实例 6.2.1 程序简介 6.2.2 加壳子程序(WJQ_ShellBegin()) 6.2.3 PE外壳程序 6.2.4 加进Anti技术 6.2.5 通过外壳修改被加壳PE 6.2.6 VC++调用汇编子程序 第7章 如何让壳与程序融为一体 7.1 序 7.1.1 为何需要壳和程序一体化 7.1.2 为阅读此章节需要的知识 7.1.3 基于此章节用的的例子程序说明 7.2 欺骗检查壳的工具 7.2.1 fi是如何检查壳的 7.2.2 欺骗fi 7.3 判断自己是否给脱壳了 7.3.1 判断文件尺寸 7.3.2 检查标记 7.3.3 外部检测(使用dll) 7.3.4 hook 相关的api(防止loader和调试api) 7.4 使用sdk把程序和壳溶为一体 7.4.1 sdk的意义 7.4.2 做一个带sdk的壳 7.5 后记:关于壳和程序的思考 第8章 Visual Basic 6 逆向工程 8.1 简介 8.2 P-code传奇 8.3 VB编译奥秘 8.4 VB与COM 8.5 VB可执行程序结构研究 8.6 VB程序事件解读 8.7 VB程序图形界面(GUI)解读 8.8 VB程序执行代码研究 8.9 我们的工具 8.10 VB程序保护篇 附录A 在Visual C++中使用内联汇编 附录B 在Visual Basic中使用汇编
软件加密技术内幕 要花时间看 第1章 PE文件格式深入研究 1.1 PE文件格式格式纵览 1.1.1 区块(Section) 1.1.2 相对虚拟地址(Relative Virtual Addresses) 1.1.3 数据目录 1.1.4 输入函数(Importing Functions) 1.2 PE文件结构 1.2.1 The MS-DOS头部 1.2.2 IMAGE_NT_HEADERS头部 1.2.3 区块表(The Section Table) 1.2.4 各种块(Sections)的描述 1.2.5 输出表 1.2.6 输出转向(Export Forwarding) 1.2.7 输入表 1.2.8 绑定输入(Bound import) 1.2.9 延迟装入数据(Delayload Data) 1.2.10 资源 1.2.11 基址重定位(Base Relocations) 1.2.12 调试目录(DebugDirectory) 1.2.13 NET头部 1.2.14 TLS初始化 1.2.15 程序异常数据 第2章 PE分析工具编写 2.1 文件格式检查 2.2 FileHeader和OptionalHeader内容的读取 2.3 得到数据目录(Data Dircetory)信息 2.4 得到块表(SectionTable)信息 2.5 得到输出表(ExportTable)信息 2.6 得到输入表(ImportTable)信息 第3章 Win32 调试API 3.1 Win32调试API原理 3.1.1 调试相关函数简要说明 3.1.2 调试事件 3.1.3 如何在调试时创建并跟踪一个进程 3.1.4 最主要的循环体 3.1.5 如何处理调试事件 3.1.6 线程环境详解 3.1.7 如何在另一个进程中注入代码 3.2 利用调试API编写脱壳机 3.2.1 tElock 0.98脱壳简介 3.2.2 脱壳机的编写 3.3 利用调试API制作内存补丁 3.3.1 跨进程内存存取机制 3.3.2 Debug API机制 第4章 Windows下的异常处理 4.1 基本概念 4.1.1 Windows下的软件异常 4.1.2 未公开的可靠吗 4.2 结构化异常处理(SEH) 4.2.1 异常处理的基本过程 4.2.2 SEH的分类 4.2.3 相关API 4.2.4 SEH相关数据结构 4.3 异常处理程序设计 4.3.1 顶层(top-level)异常处理 4.3.2 线程异常处理 4.3.3 异常处理的堆栈展开(Stack unwind) 4.3.4 异常处理程序设计中的几个注意事项: 4.4 SEH的简单应用 4.4.1 Win9x下利用SEH进ring0 4.4.2 利用SEH实现对自身的单步自跟踪 4.4.3 其它应用 4.5 系统背后的秘密 4.6 VC是如何封装系统提供的SEH机制的 4.6.1 扩展的EXCEPTION_REGISTRATION级相关结构 4.6.2 数据结构组织 4.7 Windows XP下的向量化异常处理(VEH) 第5章 软件加密技术 5.1 调试技术(Anti-Debug) 5.1.1 句柄检测 5.1.2 SoftICE后门指令 5.1.3 int68子类型 5.1.4 ICECream子类型 5.1.5 判断NTICE服务是否运行 5.1.6 INT 1 检测 5.1.7 利用UnhandledExceptionFilter检测 5.1.8 INT 41子类型 5.2 跟踪技术(Anti-Trace) 5.2.1 断点检测 5.2.2 利用SEH跟踪 5.2.3 SMC技术实现 5.3 加载技术(Anti-Loader) 5.3.1 利用TEB检测 5.3.2 利用IsDebuggerPresent函数检测 5.3.3 检查父进程 5.4 DUMP技术(Anti
Visual Studio是一款强大而且流行的集成开发环境(IDE),它支持多种调试方式,帮助开发者查找和修复代码中的错误。下面详细介绍Visual Studio的调试方式。 1. 单步调试:单步调试是最常用的调试方式之一。它允许开发者逐行执行代码,观察每个步骤的变量值和程序状态。在程序运行时,通过设置断点(Breakpoint),程序会在断点处停止执行,然后逐步跟踪代码并调试。 2. 断点:断点是用来中断程序执行并暂停在指定行的调试工具。在Visual Studio中,开发者可以通过单击行号来设置断点。一旦程序执行到设定的断点位置,调试器就会暂停程序的执行,并提供查看变量值、调用堆栈以及执行步骤等信息的能力。 3. 条件断点:条件断点是一种特殊的断点,只有当满足特定条件时,才会触发断点。在设置断点时,开发者可以指定一个条件,例如,当某个变量的值等于某个特定值时,才会停止程序执行。 4. 数据断点:数据断点是在指定变量的值发生改变时中断程序的执行。通过设置数据断点,当监视的变量值发生改变时,调试器会自动触发断点。这对于追踪导致错误的代码行很有帮助。 5. 异常捕获:Visual Studio还支持异常捕获,可以设置当某个特定类型的异常出现时是否中断程序执行。这允许开发者在异常发生时快速定位和修复错误。 6. 内存调试:Visual Studio提供内存调试功能,可以对应用程序的内存使用情况进行监控和分析。开发者可以检查变量的内存地址、查看内存中的数据和操作内存。 综上所述,Visual Studio提供了多种强大的调试方式,包括单步调试、断点、条件断点、数据断点、异常捕获和内存调试等。这些调试工具使开发者能够更加高效地查找和解决代码中的错误,提高程序的质量和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值