反调试与反反调试
参考文本
(190条消息) C++ 反反调试(NtQueryInformationProcess)_(-: LYSM :-)的博客-CSDN博客
Windows 平台反调试相关的技术方法总结—part 2 - 先知社区
C/C++ MinHook 库的使用技巧 - lyshark - 博客园 (cnblogs.com)
(177条消息) C++ 反反调试(NtQueryInformationProcess)_(-: LYSM :-)的博客-CSDN博客
https://www.cnblogs.com/BjblCracked/p/3470351.html
[原创]分析实战读书笔记12_常见反调试-软件逆向-看雪论坛-安全社区|安全招聘|bbs.pediy.com
https://www.cnblogs.com/Sna1lGo/p/15206766.html
CheckRemoteDebuggerPresent function (debugapi.h) - Win32 apps | Microsoft Docs
反调试技术常用API,用来对付检测od和自动退出程序_aijia1857的博客-CSDN博客^v81^insert_down38,201^v4^add_ask,239^v2^insert_chatgpt&utm_term=OD%E4%BF%AE%E6%94%B9forceflag&spm=1018.2226.3001.4187
Windows下反反调试技术汇总 - FreeBuf网络安全行业门户
(165条消息) 汇编语言 + Visual Studio 2019——Visual Studio 2019 中汇编语言环境解决方案_Starzkg的博客-CSDN博客
(165条消息) C/C++ 程序反调试的方法beingdebugged孤风洗剑的博客-CSDN博客
PEB和TEB资料整合 - -Vi - 博客园 (cnblogs.com)
(167条消息) 反调试 - PEB(BeingDebugged ,NtGlobalFlag)_「已注销」的博客-CSDN博客
(167条消息) 180306 逆向-反调试技术(1)BeingDebugged_奈沙夜影的博客-CSDN博客
(167条消息) 反调试技术总结(看雪)常见的反调试技术欧晨
26种对付反调试的方法_网易订阅
(167条消息) 反调试 - NtQueryInformationProcess(ProcessDebugPort,ProcessDebugFlags,ProcessDebugObjectHandle)_(-: LYSM :-)的博客-CSDN博客
TLS及反调试机制 - ylh666 - 博客园 (cnblogs.com)
C/C++ 程序反调试方法总结 - lyshark - 博客园 (cnblogs.com)
(167条消息) 由一道CTF对10种反调试的探究ctf 反调试0xDQ的博客-CSDN博客
(167条消息) 反调试技术总结_weixin_34007906的博客-CSDN博客
检测OD进程名 · 逆向工程笔记整理 · 看云
汇编运行环境配置
1、安装MASM32:Download The MASM32 SDK
2、配置Visusl Studio
-
右击项目,选择生成依赖项-生成自定义,选择masm自定义文件
-
右键项目->项目属性 添加依赖项将masm的bin目录添加到附加库目录中
-
tip:使用的是32位调试器
前置概念
TEB
TEB(Thread Environment Block,线程环境块)系统在此TEB中保存频繁使用的线程相关的数据。位于用户地址空间,在比 PEB 所在地址低的地方。进程中的每个线程都有自己的一个TEB。一个进程的所有TEB都以堆栈的方式,存放在从0x7FFDE000开始的线性内存中,每 4KB为一个完整的TEB,不过该内存区域是向下扩展的。在用户模式下,当前线程的TEB位于独立的4KB段,可通过CPU的FS寄存器来访问该段,一般存储在[FS:0]。
PEB 里其实本来有很多可以用来检测调试器的成员(虽然有的本意不一定是,但确实在被调试时会有固定变化),但是在 Win7 之后,能用的只剩下了两个
- typedef struct _PEB { // Size: 0x1D8
- /*000*/ UCHAR InheritedAddressSpace;
- /*001*/ UCHAR ReadImageFileExecOptions;
- /*002*/ UCHAR BeingDebugged; // 无调试器时 = 0,有调试器时 = 1
- /*003*/ UCHAR SpareBool;
- /*004*/ HANDLE Mutant;
- /*008*/ HINSTANCE ImageBaseAddress;
- /*00C*/ VOID *DllList;
- /*010*/ PPROCESS_PARAMETERS *ProcessParameters;
- /*014*/ ULONG SubSystemData;
- /*018*/ HANDLE ProcessHeap ;
- /*01C*/ KSPIN_LOCK FastPebLock;
- /*020*/ ULONG FastPebLockRoutine;
- /*024*/ ULONG FastPebUnlockRoutine;
- /*028*/ ULONG EnvironmentUpdateCount;
- /*02C*/ ULONG KernelCallbackTable;
- /*030*/ LARGE_INTEGER SystemReserved;
- /*038*/ ULONG FreeList;
- /*03C*/ ULONG TlsExpansionCounter;
- /*040*/ ULONG TlsBitmap;
- /*044*/ LARGE_INTEGER TlsBitmapBits;
- /*04C*/ ULONG ReadOnlySharedMemoryBase;
- /*050*/ ULONG ReadOnlySharedMemoryHeap;
- /*054*/ ULONG ReadOnlyStaticServerData;
- /*058*/ ULONG AnsiCodePageData;
- /*05C*/ ULONG OemCodePageData;
- /*060*/ ULONG UnicodeCaseTableData;
- /*064*/ ULONG NumberOfProcessors;
- /*068*/ LARGE_INTEGER NtGlobalFlag; // 有调试器时会被赋值为 70h = 112
- /*070*/ LARGE_INTEGER CriticalSectionTimeout;
- /*078*/ ULONG HeapSegmentReserve;
- /*07C*/ ULONG HeapSegmentCommit;
- /*080*/ ULONG HeapDeCommitTotalFreeThreshold;
- /*084*/ ULONG HeapDeCommitFreeBlockThreshold;
- /*088*/ ULONG NumberOfHeaps;
- /*08C*/ ULONG MaximumNumberOfHeaps;
- /*090*/ ULONG ProcessHeaps;
- /*094*/ ULONG GdiSharedHandleTable;
- /*098*/ ULONG ProcessStarterHelper;
- /*09C*/ ULONG GdiDCAttributeList;
- /*0A0*/ KSPIN_LOCK LoaderLock;
- /*0A4*/ ULONG OSMajorVersion;
- /*0A8*/ ULONG OSMinorVersion;
- /*0AC*/ USHORT OSBuildNumber;
- /*0AE*/ USHORT OSCSDVersion;
- /*0B0*/ ULONG OSPlatformId;
- /*0B4*/ ULONG ImageSubsystem;
- /*0B8*/ ULONG ImageSubsystemMajorVersion;
- /*0BC*/ ULONG ImageSubsystemMinorVersion;
- /*0C0*/ ULONG ImageProcessAffinityMask;
- /*0C4*/ ULONG GdiHandleBuffer[0x22];
- /*14C*/ ULONG PostProcessInitRoutine;
- /*150*/ ULONG TlsExpansionBitmap;
- /*154*/ UCHAR TlsExpansionBitmapBits[0x80];
- /*1D4*/ ULONG SessionId;
- } PEB, *PPEB;
tip:不同版本的操作系统,可能会有不同,可以使用windbg进行查看
-
使用WinDBG随便载入一个二进制文件,并加载调试符号链接文件.
-
.reload
-
dt ntdll!teb
-
dt -rv ntdll!_TEB
PEB
-
PEB(Process Environment Block,进程环境块)存放进程信息,每个进程都有自己的PEB信息。位于用户地址空间。在Win 2000下,进程环境块的地址对于每个进程来说是固定的,在0x7FFDF000处,这是用户地址空间,所以程序能够直接访问。准确的PEB地址应从系统 的EPROCESS结构的0x1b0偏移处获得,但由于EPROCESS在系统地址空间,访问这个结构需要有ring0的权限。还可以通过TEB结构的偏 移0x30处获得PEB的位置,FS段寄存器指向当前的TEB结构:
FS段寄存器
-
FS寄存器指向当前活动线程的TEB结构
PEB标识位识别
BeingDebug反调试
通过汇编进行定位BeingDebug位,当BeingDebug位为1 时代表函数处于调试状态
#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;
}
方案一:
手动dump fs:[30]+2找到BeingDebugged,把01修改为00,即可绕过BeingDebugged的反调试
在OllyDbg中安装命令行插件,在命令行窗口输入dump fs:[30]+2
这条命令会将BeingDebugged属性转储到转储面板窗口。右键单击BeingDebugged属性,将该位置设置为0
方案二:
OllyDbg的一些插件可以帮助我们修改BeingDebugged标志。其中最流行的有HideDebugger、Hidedebug和PhantOm,StrongOD。以strongOD为例,插件-》strongOD->options->hidePEB就可以进行绕过
-
究其根源都是由于BeingDebugged被设为True导致的,但是如果直接将BeingDebugged设置为false,则会导致u无法进行断点操作,因此如果找到一个时机,在NtGlobalFlag改变之前就设BeingDebugged为False即可绕过所有问题。
-
\1. 在第一次触发LOAD_DLL_DEBUG_EVENT的时候将BeingDebugged设为False,绕过NtGlobalFlag的改变
-
\2. 在第二次触发LOAD_DLL_DEBUG_EVENT的时候将BeingDebugged设为True,使系统中断正常触发
-
\3. 系统中断以后再将BeingDebugged清除掉
NtGlobalFlag 反调试
在PEB中的BeingDebugged被设为True后,还会有一些其他的变化产生。
加载时会将PEB中的NtGlobalFlag中的一个位改变,使其值为0x70,而正常状态下不是
在WRK中有一个宏也会随着NtGlobalFlag的改变而在RtlCreateHeap中用RtlDebugCreateHeap创建调试堆。这个调试堆中含有大量的标志(例如0xBAAD0F0D和0xFEEEFEEE等),而正常情况下这个地址中却没有有意义的数据。
-
在 32 位系统下,NtGlobalFlag 存在于 PEB 的 0x68 的位置。该值默认为 0,当调试器附加后,会设置以下标志。也就是说,我们可以通过判断NtBlobalFlag位的值,来判断程序是否处于调试状态
#include <stdio.h>
#include <windows.h>
DWORD IsDebug()
{
DWORD Debug = NULL;
__asm
{
mov eax, fs: [0x18] // TEB基地址
mov eax, [eax + 0x30] // 找到PEB
mov eax, [eax + 0x68] // 找打 NtGlobalFlag
mov Debug, eax // 取出值
}
printf("%d", Debug);
if (Debug == 112)
printf("程序正在被调戏 \n");
else
printf("程序正常 \n");
return Debug;
}
int main(int argc, char* argv[])
{
printf("返回状态: %d \n", IsDebug());
system("pause");
return 0;
}
-
以上两种的原理基本相同,就是作为检测的标识位不同
tip:该方法可以区分vs2022的程序调试器和OD的调试器,进行实验如下,在vs2022的32位调试器中显示程序正常,但是在没有插件的OD中却被检测出调试
绕过方案
方案一、手动dump fs:[30]+68找到BeingDebugged,把70修改为00,即可绕过NtGlobalFlag的反调试
方案二:开启strongOD 的HidePeb选项
ProcessHeap 反调试
该属性是一个未公开的属性,它被设置为加载器为进程分配的第一个堆的位置(进程堆标志),ProcessHeap标志位于PEB结构中偏移为0x18处,第一个堆头部有一个属性字段,这个属性叫做ForceFlags属性偏移为0x44,该属性为0说明程序没有被调试,非0说明被调试,另外的Flags属性不为2说明被调试,不为2则说明没有被调试.
这里需要注意的是堆区在不同系统中偏移值是不同的,在WindowsXP系统中ForceFlags属性位于堆头部偏移量为0x10处,对于Windows10系统来说这个偏移量为0x44,而默认情况如果被调试则ForceFlags属性为0x40000060,而Flags标志为0x40000062,下面通过汇编分别读取出这两个堆头的参数.
TIP:由于偏移地址可能不同,所以这里提供查找偏移地址的方法
-
使用windbg加载任意二进制文件
-
.reload
-
dt !_peb
-
!heap
-
!heap -a 1270000
-
dt _HEAP 1270000
#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);
}
else {
printf("ForceFlag反调试正常%d\n", ret);
}
int ret2 = IsDebugB();
if (ret2 != 2) {
printf("进程正在被调试: %x \n", ret2);
}
else {
printf("Flags反调试正常%d\n", ret);
}
system("pause");
return 0;
}
同样该程序,可以区分VS2022调试器和OD调试器
绕过方案
strongOD中勾选HidePEB
WinAPI反调试
IsDebuggerPresent 函数反调试
Win32API为程序提供了IsDebuggerPresent来判断自己是否处于调试状态,这个函数读取了当前进程PEB中的BeingDebugged标志当该标志位的值为1时,则证明函数处于调试状态,由于和之前BeingDebug的原理相同因此绕过方式也相同,就不再进行绕过演示
#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;
}
ZwQueryInformationProcess()
ZwQueryInformationProcess()这个函数来读取到程序中的PEB数据,然后判断PebBase+0x68是否等于70,本质上和汇编定位NtGlobalFlag是相同的,绕过方式也相同
#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
);
BOOL 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("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 == 112) {
printf("%d\n", NtGlobalFlag);
return true;
}
}
}
CloseHandle(hProcess);
}
return false;
}
int main(int argc, char* argv[])
{
if (IsDebug())
{
printf("正在被调戏. \n");
}
else {
printf("程序正常");
}
system("pause");
return 0;
}
该方法的原理和识别NtGlobalFalg基本相同,同样可以识别VS2022和OD基本可以使用同样的方法进行绕过
绕过方案
方案一:手动dump fs:[30]+68找到NtGlobalFlag,把0x70修改为00,即可绕过NtGlobalFlag的反调试,tip此时该位的显示一般为p
方案二:勾选插件中的HidePEB选项,和之前一样,这次不再进行演示
NtQueryInformationProcess
CheckRemoteDebuggerPresent function (debugapi.h) - Win32 apps | Microsoft Docs
这个函数是Ntdll.dll中一个API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。
__kernel_entry NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle, // 进程句柄
IN PROCESSINFOCLASS ProcessInformationClass, // 检索的进程信息类型
OUT PVOID ProcessInformation, // 接收进程信息的缓冲区指针
IN ULONG ProcessInformationLength, // 缓冲区指针大小
OUT PULONG ReturnLength // 实际接收的进程信息大小
);
进程处于被调试状态时,ProcessDebugPort = 0xffffffff
#include <iostream>
#include <windows.h>
#include <winternl.h>
#pragma region 依赖
typedef NTSTATUS(NTAPI* pfnNtQueryInformationProcess)(
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
#pragma endregion
int main(int argc, CHAR* argv[])
{
pfnNtQueryInformationProcess NtQueryInformationProcess = NULL; // 存放 ntdll 中 NtQueryInformationProcess 函数地址
NTSTATUS status; // NTSTATUS 错误代码,0:执行成功
DWORD isDebuggerPresent = -1; // 如果当前被调试,则 = ffffffff
HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll")); // ntdll 模块句柄
// ntdll 加载成功
if (hNtDll) {
// 取 NtQueryInformationProcess 函数地址
NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(hNtDll, "NtQueryInformationProcess");
// 取地址成功
if (NtQueryInformationProcess) {
// NtQueryInformationProcess 检测调试器
status = NtQueryInformationProcess(
GetCurrentProcess(), // 进程句柄
0x7, // 要检索的进程信息类型,ProcessDebugPort:调试器端口号
&isDebuggerPresent, // 接收进程信息的缓冲区指针
sizeof(DWORD), // 缓冲区大小
NULL // 实际返回进程信息的大小
);
// NtQueryInformationProcess 执行成功
if (status == 0 && isDebuggerPresent != 0) {
// 输出
std::cout << "status = " << status << std::endl;
std::cout << "isDebuggerPresent = " << std::hex << isDebuggerPresent << std::endl;
std::cout << "检测到调试器" << std::endl;
getchar();
return 0;
}
}
}
// 输出
std::cout << "status = " << status << std::endl;
std::cout << "isDebuggerPresent = " << std::hex << isDebuggerPresent << std::endl;
std::cout << "没有发现调试器" << std::endl;
getchar();
return 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;
}
*/
绕过方案
方案一:可以hook NtQueryInformationProcess的返回值,对其进行修改,以下是示例代码:
#include <Windows.h>
#include "mhook.h"
typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
const UINT ProcessDebugPort = 7;
pfnNtQueryInformationProcess g_origNtQueryInformationProcess = NULL;
NTSTATUS NTAPI HookNtQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
)
{
NTSTATUS status = g_origNtQueryInformationProcess(
ProcessHandle,
ProcessInformationClass,
ProcessInformation,
ProcessInformationLength,
ReturnLength);
if (status == 0x00000000 && ProcessInformationClass == ProcessDebugPort)
{
*((PDWORD_PTR)ProcessInformation) = 0;
}
return status;
}
DWORD SetupHook(PVOID pvContext)
{
HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
if (NULL != hNtDll)
{
g_origNtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(hNtDll, "NtQueryInformationProcess");
if (NULL != g_origNtQueryInformationProcess)
{
Mhook_SetHook((PVOID*)&g_origNtQueryInformationProcess, HookNtQueryInformationProcess);
}
}
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hInstDLL);
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)SetupHook, NULL, NULL, NULL);
Sleep(20);
case DLL_PROCESS_DETACH:
if (NULL != g_origNtQueryInformationProcess)
{
Mhook_Unhook((PVOID*)&g_origNtQueryInformationProcess);
}
break;
}
return TRUE;
}
方案二:暴力破解,定位该函数,对其二进制代码进行修改,进而跳过该函数的判断,该方法几乎对所有的反调试都适用,但是定位程序比较繁琐,因为反调试代码位置不固定,而且也不一定会有很明显的逻辑区分,比如程序强制关闭等。和软件破解的思路基本相同,根据软件破解的思路基本相同:尝试让绕过显示是否调试之前的每一个跳转
1、使用IDA pro打开测试程序,程序代码见之前,除了反调试功能,没有其他任何功能,可以看到一个明显的分支就是对调试状态的判断,我们目的就是修改其判定结果cmp [ebp+var_4],eax
2、重新使用OD打开该文件,定位到判断语句的位置,经过单步调试,可以找到002813B9,就是这个关键跳转,后续的操作就比较多样了,最简单的就是修改寄存器,运行到,双击右侧z寄存器的值,进行修改
然后就可以发现,反调试被绕过了
CheckRemoteDebuggerPresent 反调试
-
第一个参数是进程句柄,第二参数用于存放结果。返回值表示函数是否执行成功
-
CheckRemoteDebuggerPresent实际上调用了ntdll里面的NtQueryInformationProcess来检测。所以和之前的基本相同,绕过方法也相同
NTSTATUS NtQueryInformationProcess (
__in HANDLE ProcessHandle,
__in PROCESSINFOCLASS ProcessInformationClass,
__out_bcount(ProcessInformationLength) PVOID ProcessInformation,
__in ULONG ProcessInformationLength,
__out_opt PULONG ReturnLength
);
#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(L"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 反调试
在程序启动后,会有一个STARTUPINFO的结构体变量,来保存程序启动的信息,我们通过其中参数的改变来检测程序是正常运行还是在调试器中运行的。
1 typedef struct _STARTUPINFOW {
2 DWORD cb;
3 LPWSTR lpReserved;
4 LPWSTR lpDesktop;
5 LPWSTR lpTitle;
6 DWORD dwX;
7 DWORD dwY;
8 DWORD dwXSize;
9 DWORD dwYSize;
10 DWORD dwXCountChars;
11 DWORD dwYCountChars;
12 DWORD dwFillAttribute;
13 DWORD dwFlags;
14 WORD wShowWindow;
15 WORD cbReserved2;
16 LPBYTE lpReserved2;
17 HANDLE hStdInput;
18 HANDLE hStdOutput;
19 HANDLE hStdError;
20 } STARTUPINFOW, *LPSTARTUPINFOW;
程序启动时默认会通过explorer资源管理器,调用CreateProcess()函数创建的时候会把STARTUPINFO结构体中的值设置为0,但如果通过调试器启动程序时该值并不会发生变化,我们可以通过判断结构体中的dwFlags参数来实现反调试.
#include <Windows.h>
#include <stdio.h>
int IsDebug()
{
STARTUPINFO si = { 0 };
GetStartupInfo(&si);
if (si.dwFlags != 1)//tip:不仅仅是flag,其他参数也可以作为判断依据
return 1;
return 0;
}
int main(int argc, char* argv[])
{
int ret = IsDebug();
printf("%d \n", ret);
system("pause");
return 0;
}
tip:该方法在VS2022中无论是调试状态还是运行状态,都会显示为被调试状态,但是双击桌面运行,不会被标识为调试状态
父进程检测实现反调试
该反调试的原理非常简单,我们的系统在运行程序的时候,都是由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中断的情况下,调试将会被终止;可使用于吾爱破解版OD
#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;
}
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;
``}
}
TLS 线程局部存储反调试
Thread Local Storage(TLS),即线程本地存储,是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。
前面的那几种反调试手段都是在程序运行后进行判断的,这种判断可以通过OD断下后进行修改从而绕过反调试,但TLS则是在程序运行前抢占式执行TLS中断,显示在OD中,就是程序无法开始运行,所以这种反调试技术更加的安全,但也不绝对仍然能够被绕过.
要在程序中使用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段,应该立即怀疑它使用了反调试技术。
#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;
}
在OllyDbg中选择Options->Debugging Options->Events,然后设置System break-point作为第一个暂停的位置,这样就可以让OllyDbg在TLS回调执行前暂停。
在IDA Pro中按Ctrl+E快捷键看到二进制的入口点,该组合键的作用是显示应用程序所有的入口点,其中包括TLS回调。双击函数名可以浏览回调函数。
利用In指令检测虚拟机
GetLastError(不好用)
编写应用程序时,经常需要涉及到错误处理问题。许多函数调用只用TRUE和FALSE来表明函数的运行结果。一旦出现错误,MSDN中往往会指出请用GetLastError()函数来获得错误原因。恶意代码可以使用异常来破坏或者探测调试器。调试器捕获异常后,并不会立即将处理权返回被调试进程处理,大多数利用异常的反调试技术往往据此来检测调试器。多数调试器默认的设置是捕获异常后不将异常传递给应用程序。如果调试器不能将异常结果正确返回到被调试进程,那么这种异常失效可以被进程内部的异常处理机制探测。
BOOL CheckDebug()
{
DWORD errorValue = 12345;
SetLastError(errorValue);
OutputDebugString("Test for debugger!");
if (GetLastError() == errorValue)
{
return TRUE;
}
else
{
return FALSE;
}
}
对于OutputDebugString函数,它的作用是在调试器中显示一个字符串,同时它也可以用来探测调试器的存在。使用SetLastError函数,将当前的错误码设置为一个任意值。如果进程没有被调试器附加,调用OutputDebugString函数会失败,错误码会重新设置,因此GetLastError获取的错误码应该不是我们设置的任意值。但如果进程被调试器附加,调用OutputDebugString函数会成功,这时GetLastError获取的错误码应该没改变。
BOOL CheckDebug()
{
char fib[1024] = {0};
DeleteFiber(fib);
return (GetLastError() != 0x57);
}
对于DeleteFiber函数,如果给它传递一个无效的参数的话会抛出ERROR_INVALID_PARAMETER异常。如果进程正在被调试的话,异常会被调试器捕获。所以,同样可以通过验证LastError值来检测调试器的存在。如代码所示,0x57就是指ERROR_INVALID_PARAMETER。
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;
}
}
同样还可以使用CloseHandle、CloseWindow产生异常,使得错误码改变。
ZwSetInformationThread
ZwSetInformationThread 等同于 NtSetInformationThread,通过为线程设置 ThreadHideFromDebugger,可以禁止线程产生调试事件。函数原型如下:
VOID DisableDebugEvent(VOID){
HINSTANCE hModule;
ZW_SET_INFORMATION_THREAD ZwSetInformationThread;
hModule = GetModuleHandleA("Ntdll");
ZwSetInformationThread = (ZW_SET_INFORMATION_THREAD)GetProcAddress(hModule, "ZwSetInformationThread");
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);
}
ZwSetInformationThread拥有两个参数,第一个参数用来接收当前线程的句柄,第二个参数表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11),使用语句ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);调用该函数后,调试进程就会被分离出来。该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程序,因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终停止调试。还有一个函数DebugActiveProcessStop用来分离调试器和被调试进程,从而停止调试。两个API容易混淆,需要牢记它们的区别。
#include <iostream>
#include <tchar.h>
#include <Windows.h>
#include <stdio.h>
#pragma region 全局变量
/*
ZwSetInformationThread 第二个成员
*/
typedef enum _THREADINFOCLASS {
ThreadBasicInformation,
ThreadTimes,
ThreadPriority,
ThreadBasePriority,
ThreadAffinityMask,
ThreadImpersonationToken,
ThreadDescriptorTableEntry,
ThreadEnableAlignmentFaultFixup,
ThreadEventPair,
ThreadQuerySetWin32StartAddress,
ThreadZeroTlsCell,
ThreadPerformanceCount,
ThreadAmILastThread,
ThreadIdealProcessor,
ThreadPriorityBoost,
ThreadSetTlsArrayAddress,
ThreadIsIoPending,
ThreadHideFromDebugger
}THREAD_INFO_CLASS;
#pragma endregion
#pragma region 依赖函数
/*
ZwSetInformationThread
*/
typedef NTSTATUS(NTAPI* pZwSetInformationThread)(
IN HANDLE ThreadHandle, // 线程对象句柄
IN THREAD_INFO_CLASS ThreadInformaitonClass, // 线程信息类型
IN PVOID ThreadInformation, // 线程信息指针
IN ULONG ThreadInformationLength // 线程信息大小
);
#pragma endregion
#pragma region 功能函数
#pragma endregion
int main(int argc, _TCHAR* argv[])
{
// 获取 ZwSetInformationThread 函数地址
pZwSetInformationThread ZwSetInformationThread = (pZwSetInformationThread)GetProcAddress(LoadLibrary("ntdll.dll"), "ZwSetInformationThread");
// 执行 ZwSetInformationThread
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, NULL);
// 测试 ZwSetInformationThread 的效果
std::cout << "程序运行到了这里" << std::endl;
getchar();
return 0;
}
其表现为在该函数运行之前的断点能够正常运行,但是在该函数之后的断点无法正常运行,会导致程序直接退出。
破解方法:注意到该处 ZwSetInformationThread 函数的第 2 个参数为 ThreadHideFromDebugger,其值为 0x11。调试执行到该函数时,若发现第 2 个参数值为 0x11,跳过或者将 0x11 修改为其他值即可
查找窗体信息
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。
查找调试器引用的注册表项反调试
下面是调试器在注册表中的一个常用位置。
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;
}
}
查找进程信息反调试
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;
}
软件断点检查
调试器设置断点的基本机制是用软件中断指令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;
}
硬件断点检查
在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;
}
执行代码校验和检查
恶意代码可以计算代码段的校验并实现与扫描中断相同的目的。与扫描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;
}
}
判断是否具有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;
}
}
设置陷阱标志位
EFLAGS寄存器的第八个比特位是陷阱标志位。如果设置了,就会产生一个单步异常。
BOOL CheckDebug()
{
__try
{
__asm
{
pushfd
or word ptr[esp], 0x100
popfd
nop
}
}
__except(1)
{
return FALSE;
}
return TRUE;
}
利用中断
因为调试器使用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;
}
使用异常
RaiseException
RaiseException函数产生的若干不同类型的异常可以被调试器捕获。
BOOL TestExceptionCode(DWORD dwCode)
{
__try
{
RaiseException(dwCode, 0, 0, 0);
}
__except(1)
{
return FALSE;
}
return TRUE;
}
BOOL CheckDebug()
{
return TestExceptionCode(DBG_RIPEXCEPTION);
}
SetUnhandledExceptionFilter
进程中发生异常时若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 main(int argc, TCHAR* argv[])
{
AD_SetUnhandledExceptionFilter();
return 0;
}
在OllyDbg中,选择Options->Debugging Options->Exceptions来设置把异常传递给应用程序。