一.查看结构体数据
PEB是在三环中存储了进程的信息,每一个进程都对应了一个PEB结构体,我们可以通过查看PEB结构体的成员来获得调试信息。
对于PEB结构体的位置,我们可以通过TEB获得,它的地址位于TEB的0x30偏移处
nt!_TEB
+0x030 ProcessEnvironmentBlock
而对于TEB,我们可以通过fs段寄存器来定位,fs段寄存器存储了选择子,在GDT表中查找到相应的段描述符,其中的base基址就是TEB的地址,fs:[0]也就是[fs.base+0],三环中,fs:[0]一直指向了当前正在运行的线程的基址
1. BeingDebugged
nt!_PEB
+0x002 BeingDebugged : UChar
通过BeingDebugged成员,我们可以获得目标进程是否处于调试状态
_asm
{
mov eax, fs: [0x30]
mov al, BYTE ptr [eax+2]
mov result, al
}
由于这种方法很简单,一般的调试器都会将BeingDebugged清除
2. NtGlobalFlag
nt!_PEB
+0x068 NtGlobalFlag
NtGlobalFlag位于PEB的0x68偏移处,它会根据BeingDebugged的值来设置,当BeingDebugged位true时,NtGlobalFlag的值会被设置为0x70
//NtGlobalFlags
_asm
{
mov eax,fs:[0x30]
mov eax ,[eax+0x68]
and eax,0x70
cmp eax,0x70
}
想要绕过检查只需要手动修改相应位置的值
3. HeapFlags
HeapFlags包含两个根据NtGlobalFlag进行初始化的标志,Flags和ForceFlags,在windows xp中,Flags位于堆的0xC偏移处,而ForceFlags位于堆的0x10处
nt!_HEAP
+0x00c Flags : Uint4B
+0x010 ForceFlags : Uint4B
正常情况下,Flags的值为2,ForceFlags的值为0
堆的位置可以通过PEB来获得
nt!_PEB
+0x018 ProcessHeap
//Flags
_asm
{
mov eax, fs: [0x30] //eax->PEB
mov eax, [eax + 0x18] //eax->heap
mov eax, [eax + 0xc] //eax = Flags
cmp eax,0x2
}
//ForceFlags
_asm
{
mov eax, fs: [0x30] //eax->PEB
mov eax, [eax + 0x18] //eax->heap
mov eax, [eax + 0x10] //eax = ForceFlags
cmp eax, 0
}
二.使用Windwos API
1. IsDebuggerPresent
BOOL IsDebuggerPresent();
这个函数的实现,本质上是查看PEB.BeingDebugged
2. CheckRemoteDebuggerPresent
BOOL CheckRemoteDebuggerPresent( [in] HANDLE hProcess, [in, out] PBOOL pbDebuggerPresent );
typedef BOOL(WINAPI* CHECK_REMOTE_DEBUGGER_PRESENT)(
HANDLE hProcess,
PBOOL pbDebuggerPresent);
HMODULE hMod;//模块句柄
HANDLE hProcess;//进程句柄
CHECK_REMOTE_DEBUGGER_PRESENT CheckRemoteDebuggerPresent = nullptr;
BOOL bDebuggerPresent;//返回值
hMod = LoadLibrary(L"kernel32.dll");//加载kernel32.dll
CheckRemoteDebuggerPresent = (CHECK_REMOTE_DEBUGGER_PRESENT)GetProcAddress(hMod, "CheckRemoteDebuggerPresent");//获得导出函数地址
hProcess = GetCurrentProcess();
CheckRemoteDebuggerPresent(hProcess, &bDebuggerPresent);//调用函数
3. NtQueryInformationProcess
_ NTSTATUS NtQueryInformationProcess( [in] HANDLE ProcessHandle, //目标进程句柄
[in] PROCESSINFOCLASS ProcessInformationClass,//查看信息的类型
[out] PVOID ProcessInformation, // 接收传出的数据
[in] ULONG ProcessInformationLength,//查找数据的长度
[out, optional] PULONG ReturnLength//(可选)接收的数据的长度 );
NtQueryInformationProcess()可以用来获得进程的信息,它从零环的EPROCESS结构体中取出数据,然后传回到三环中,我们无法在应用程序中直接获得零环的数据,所以需要使用windows提供的API来实现零环和三环的通信
我们会用到的的DebugPort也是在存储在EPROCESS中,DebugPort是进程用来和调试器通信的,当进程处于调试状态时,DebugPort会被设置为非零的值,因此可以获得进程的DebugPort来检测调试器
nt!_EPROCESS
+0x0ec DebugPort : Ptr32 Void
typedef DWORD(WINAPI* NT_QUERY_INFORMATION_PROCESS)(
HANDLE hProcess,
DWORD ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
HMODULE hMod = nullptr;
HANDLE hProcess = nullptr;
NT_QUERY_INFORMATION_PROCESS NtQueryInformationProcess = nullptr;
DWORD dwProcessInformation = 0;
hMod = GetModuleHandle(L"ntdll.dll");
NtQueryInformationProcess = (NT_QUERY_INFORMATION_PROCESS)GetProcAddress(hMod, "NtQueryInformationProcess");
hProcess = GetCurrentProcess();
NtQueryInformationProcess(hProcess,
7, //7为宏,这里代表了DebugPort(0x7)
&dwProcessInformation,
4,
nullptr);
if (dwProcessInformation != 0)
printf("Find Debug!\n");
4. NtSetInformationThread
NTSYSCALLAPI NTSTATUS NtSetInformationThread(
[in] HANDLE ThreadHandle,//目标线程的句柄
[in] THREADINFOCLASS ThreadInformationClass,//查看数据的类型,同样也是宏
[in] PVOID ThreadInformation,//要传递的数据
[in] ULONG ThreadInformationLength//要传递数据的长度
);
NtSetInformationThread()可以设置线程的信息,线程和进程一样,在零环中也有一个结构体ETHEAD存储了自身的信息,但我们在应用程序中无法直接得到和修改,所以也要利用Windwos提供的API
nt!_ETHREAD
+0x280 HideFromDebugger
这里我们会用到的宏是ThreadHideFormDebug(17),它可以将ETHERAD的HideFormDebugger设置为TRUE,它会禁止对应的线程产生调试事件
调用ZwSetInformationThread()前后对比
调试器与进程无法通信,这与直接将DebugPort置零类似
三.其他技巧
1. 查找调试器进程
我们可以便利进程链表,去查看是否有调试器的存在,这种方法很容易跳过,只需要修改进程名称或者删除进程链表中的对应项即可
HANDLE hProcessSnap = nullptr;//进程快照句柄
PROCESSENTRY32 pe32 = { 0 };//进程结构体
pe32.dwSize = sizeof(PROCESSENTRY32);//初始化
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);//从第一个进程开始遍历
if (!Process32First(hProcessSnap, &pe32))//将第一个进程的信息写入pe32
return false;
do//继续向下遍历
{
if (!_wcsicmp(pe32.szExeFile, L"x64dbg.exe")|| !_wcsicmp(pe32.szExeFile, L"x32dbg.exe")|| !_wcsicmp(pe32.szExeFile, L"ollydbg.exe"))
{
printf("Find Debug!\n");
break;
}
}while (Process32Next(hProcessSnap, &pe32));
2. 时间差
一般来说,处于调试状态的进程运行的时间远长于正常状态的进程,因此我们可以获得指令前后的时钟周期数,如果前后两次时钟计数的差超过正常的范围,那么就可以确定存在调试器
- rdtsc指令
该指令可以获得CPU自开机运行到目前的时钟计数,值为64位,存于edx:eax中
DWORD time1, time2;
_asm
{
rdtsc
mov time1 ,eax
rdtsc
mov time2,eax
}
if (time2 - time1 > 0xff)
printf("Find Debug!\n");
- _rdtsc()
Windows提供了_rdtsc(),这个函数本质上就是rdtsc指令
time1 = __rdtsc();
time2 = __rdtsc();
if (time2 - time1 > 0xff)
printf("Find Debug\n");
- GetTickCout()
time1 = GetTickCount();
time2 = GetTickCount();
if (time2 - time1 > 0xff)
printf("Find Debug\n");
3. 父进程检测
我们所使用的进程一般都是由explorer.exe调用CreatProcess()创建的,但处于调试状态的进程却是由调试器创建的,我们可以通过查看目标进程的父进程是否是explorer.exe,即两者的PID是否相同,来实现判断
HANDLE hProcessSnap = nullptr;
HANDLE hProcess = nullptr;
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(PROCESSENTRY32);//初始化
DWORD dwPID;
DWORD dwParentID;
dwPID = GetCurrentProcessId();//获得当前进程ID
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, dwPID);//创建进程快照
if (!Process32First(hProcessSnap, &pe32))//获得当前进程的信息
return false;
dwParentID = pe32.th32ParentProcessID;//获得当前进程的父进程ID
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (!hProcessSnap)
return false;
if (!Process32First(hProcessSnap, &pe32))//获得第一个进程的信息
return false;
do//遍历进程,查找explorer.exe
{
if (!_wcsicmp(pe32.szExeFile, L"explorer.exe"))
if (dwParentID != pe32.th32ParentProcessID)//判断ID是否相同
printf("Find Debug!\n");
break;
}while (Process32Next(hProcessSnap, &pe32));
后续掌握了其他的方法会继续添加的