Windows反调试总结
本文总结下windows下常见的反调试技术,尽量完全,如果不适用win10的就不总结了,也是对《逆向工程核心原理相关部分的一个总结》,目前更新状态,静态反调试快弄完了,动态的尽快吧,hook api应该也是最近更。
期末也是过去了,表面上复习,实际上是休息了一个月左右,嘿嘿,现在也是寒假了,也是准备提升下自己的编程能力,学习下常见病毒编程技术,网络编程,和python进阶。
静态反调试
PEB结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| typedef struct _PEB { BYTE Reserved1[2]; BYTE BeingDebugged; BYTE Reserved2[1]; PVOID Reserved3[2]; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID Reserved4[3]; PVOID AtlThunkSListPtr; PVOID Reserved5; ULONG Reserved6; PVOID Reserved7; ULONG Reserved8; ULONG AtlThunkSListPtr32; PVOID Reserved9[45]; BYTE Reserved10[96]; PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine; BYTE Reserved11[128]; PVOID Reserved12[1]; ULONG SessionId; } PEB, *PPEB;
|
如何获取PEB结构体的地址
1
| mov eax,DWORD PTR FS:[0x30];address of PEB
|
1 2
| mov eax,DWORD PTR FS:[0x18];address of TEB mov eax,DWORD PTR DS:[eax+0x30]
|
1 2 3 4 5 6 7 8
| FARPROC pProc = NULL; LPBYTE pTEB = NULL; LPBYTE pPEB = NULL;
pProc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCurrentTeb"); pTEB = (LPBYTE)(*pProc)(); // address of TEB pPEB = (LPBYTE) * (LPDWORD)(pTEB + 0x30); // address of PEB
|
BeingDebugged
进程处于调试状态,PEB.BeingDebugged(偏移+2)的值就会变为1,未调试状态为0。
isDebuggerPresent函数就是通过修改这个PEB.BeingDebugged来达到检测进程是否处于调试的目的。
下面是自己写的一个例子,我们然后自己来调试看看isDebuggerPresent内部是什么样子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include<stdio.h> #include<windows.h>
void test1() { BOOL check;
check = IsDebuggerPresent(); if (check == TRUE) { MessageBoxA(NULL, "Being Debugger","The_Itach1", MB_OK); } else { MessageBoxA(NULL, "No Debugger", "The_Itach1", MB_OK); } }
int main() { test1(); }
|
ida中调试isDebuggerPresent函数。
可以看到就是先获取了PEB结构体的地址,然后找到偏移为2的PEB.BeingDebugged的值,然后返回。
破解的话就直接,修改返回值,或者改跳转之类,或者直接修改PEB.BeingDebugged的值。
NtGlobalFlag
进程处于调试状态,PEB.NtGlobalFlag(+68)的值会变为0x70,所以该特点也可以作为反调试的一个标志。
代码如下,vs直接运行不报被调试,ida调试会报。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include<stdio.h> #include<windows.h>
void test2() { FARPROC pProc = NULL; LPBYTE pTEB = NULL; LPBYTE pPEB = NULL;
pProc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCurrentTeb"); pTEB = (LPBYTE)(*pProc)(); pPEB = (LPBYTE) * (LPDWORD)(pTEB + 0x30);
DWORD dwNtGlobalFlag = *(LPDWORD)(pPEB + 0x68); if (dwNtGlobalFlag == 0x70) { MessageBoxA(NULL, "Being Debugger", "The_Itach1", MB_OK); } else { MessageBoxA(NULL, "No Debugger", "The_Itach1", MB_OK); } } int main() { test2(); }
|
破解方法和上面差不多。
用这个函数可以用来获取各种与进程调试的信息。官方文档https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess
1 2 3 4 5 6 7 8
| __kernel_entry NTSTATUS NtQueryInformationProcess( [in] HANDLE ProcessHandle, [in] PROCESSINFOCLASS ProcessInformationClass, [out] PVOID ProcessInformation, [in] ULONG ProcessInformationLength, [out, optional] PULONG ReturnLength );
|
ProcessInformationClass,实际上官方文档根本不全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| typedef enum _PROCESS_INFORMATION_CLASS { ProcessMemoryPriority, ProcessMemoryExhaustionInfo, ProcessAppMemoryInfo, ProcessInPrivateInfo, ProcessPowerThrottling, ProcessReservedValue1, ProcessTelemetryCoverageInfo, ProcessProtectionLevelInfo, ProcessLeapSecondInfo, ProcessMachineTypeInfo, ProcessInformationClassMax } PROCESS_INFORMATION_CLASS;
|
主要看二三参数
ProcessInformationClass代表要检索的进程信息的类型,实际上就是调用相关的函数来获取信息,我们主要关注和调试相关的,也就是ProcessDebugPort(0x7),ProcessDebugObjectHandle(0x1e),ProcessDebugFlags(0x1f)。
ProcessInformation,相关函数的返回信息到这个参数。
下面以一个例子来说说这三种。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| #include<stdio.h> #include<windows.h>
typedef enum _PROCESSINFOCLASS { ProcessBasicInformation = 0, ProcessQuotaLimits, ProcessIoCounters, ProcessVmCounters, ProcessTimes, ProcessBasePriority, ProcessRaisePriority, ProcessDebugPort = 7, ProcessExceptionPort, ProcessAccessToken, ProcessLdtInformation, ProcessLdtSize, ProcessDefaultHardErrorMode, ProcessIoPortHandlers, ProcessPooledUsageAndLimits, ProcessWorkingSetWatch, ProcessUserModeIOPL, ProcessEnableAlignmentFaultFixup, ProcessPriorityClass, ProcessWx86Information, ProcessHandleCount, ProcessAffinityMask, ProcessPriorityBoost, MaxProcessInfoClass, ProcessWow64Information = 26, ProcessImageFileName = 27, ProcessDebugObjectHandle = 30, ProcessDebugFlags = 31, SystemKernelDebuggerInformation = 35 }PROCESSINFOCLASS;
void test3() { typedef NTSTATUS(WINAPI* NTQUERYINFORMATIONPROCESS)( HANDLE ProcessHandle, PROCESSINFOCLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength );
HANDLE pProcessHandle; DWORD dwDebugPort; HANDLE hDebugObject = NULL; BOOL bDebugFlag; NTQUERYINFORMATIONPROCESS myNtQueryInformationProcess;
myNtQueryInformationProcess = (NTQUERYINFORMATIONPROCESS)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueryInformationProcess");
pProcessHandle = GetCurrentProcess(); myNtQueryInformationProcess(pProcessHandle, ProcessDebugPort, &dwDebugPort, sizeof(dwDebugPort),NULL); printf("ProcessDebugPort:%x\n", dwDebugPort);
myNtQueryInformationProcess(pProcessHandle, ProcessDebugObjectHandle, &hDebugObject, sizeof(hDebugObject), NULL); printf("ProcessDebugObjectHandle:%x\n", hDebugObject);
myNtQueryInformationProcess(pProcessHandle, ProcessDebugFlags, &bDebugFlag, sizeof(bDebugFlag), NULL); printf("ProcessDebugFlags:%x\n", bDebugFlag);
} int main() { test3(); system("pause"); }
|
上面的代码是自己定义了一个函数指针,来调用NtQueryInformationProcess函数,并且枚举了更多的PROCESSINFOCLASS数据,需要注意enum类型重定义的问题,这是官方文档中没有的,应该是作者通过自己分析NtQueryInformationProcess内部得到的。
上面的3种在非调试情况返回的值分别如下
ProcessDebugPort:0
ProcessDebugObjectHandle:0
ProcessDebugFlags:1
而在调试时却是这种
ProcessDebugPort:ffffffff
ProcessDebugObjectHandle:100 ;代表调试对象的句柄值
ProcessDebugFlags:0
官方文档实际上写的信息很少,如果想要深入了解如何返回的值,可以看看https://www.52pojie.cn/thread-1409104-1-1.html这篇文章,但是我一直没找到如何看这个函数内部的方法,可能和内核调试有关吧。
破解方法,修改每次的返回值就行,如果出现次数太多,或者每时每刻都在调用反调试函数,就需要用打补丁的方式hook这个api,使其每次都返回正确的值,具体思路就是在进入函数前先jmp到我们的补丁代码处,然后调用真正的NtQueryInformationProcess函数,根据栈上ProcessInformationClass[esp+c]值的不同,从而再来修改栈上ProcessInformation(esp+10)的对应值。
用来检测自己的OS(操作系统)是否处于调试模式。
windows10启动调试模式和win7差不多,命令如下,需要重启电脑
C:\WINDOWS\system32> bcdedit /debug on ;打开
C:\WINDOWS\system32> bcdedit /debug off ;关闭
而这个函数就可以用来判断我们的OS是否是处于调试模式。
NtQuerySystemInformation的基本信息,https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation,官方文档还是给的不全。
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);
主要参数一二,SystemInformationClass这个参数传入SystemKernelDebuggerInformation=0x23时,能通过其返回到第二个参数的结构体的信息来判断OS是否处于调试模式,https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/kernel_debugger.htm#:~:text=The%20SYSTEM_KERNEL_DEBUGGER_INFORMATION%20structure%20is%20what%20a%20successful%20call,buffer%20when%20given%20the%20information%20class%20SystemKernelDebuggerInformation%20%280x23%29.这个网站里面有SYSTEM_KERNEL_DEBUGGER_INFORMATION的一些信息。
测试代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include<stdio.h> #include<windows.h>
void test4() { typedef NTSTATUS(WINAPI* NTQUERYSYSTEMINFORMATION)( ULONG SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength );
typedef struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION { BOOLEAN DebuggerEnabled; BOOLEAN DebuggerNotPresent; } SYSTEM_KERNEL_DEBUGGER_INFORMATION, * PSYSTEM_KERNEL_DEBUGGER_INFORMATION;
NTQUERYSYSTEMINFORMATION myNtQuerySystemInformation; ULONG ulReturnedLength = 0; SYSTEM_KERNEL_DEBUGGER_INFORMATION SystemInformation = {0,};
myNtQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQuerySystemInformation"); myNtQuerySystemInformation(SystemKernelDebuggerInformation, &SystemInformation, sizeof(SystemInformation), &ulReturnedLength);
printf("SystemKernelDebuggerInformation%d %d\n", SystemInformation.DebuggerEnabled, SystemInformation.DebuggerNotPresent);
if (SystemInformation.DebuggerEnabled) { MessageBoxA(NULL, "Being Debugger", "The_Itach1", MB_OK); } else { MessageBoxA(NULL, "No Debugger", "The_Itach1", MB_OK); } }
int main() { test4();
system("pause"); }
|
当处于调试时,结构体的两个变量都是1,非调试时,结构体第一个变量为0,第二个变量为1。
破解方法,C:\WINDOWS\system32> bcdedit /debug off ;关闭
NtQueryObject
原理就是调试器调试进程时,会产生一个调试对象的内核对象,所以我们只需要检查这个内核对象是否存在即可。
代码太多了。。。焯
该函数是用来设置线程信息的,也是一种反调试的手段,可以将被调试程序强制退出调试状态,对于ZwSetInformationThread这个函数,先看看结构。
NTSYSAPI NTSTATUS ZwSetInformationThread(
[in] HANDLE ThreadHandle,
[in] THREADINFOCLASS ThreadInformationClass,
[in] PVOID ThreadInformation,
[in] ULONG ThreadInformationLength
);
第一个参数是线程的句柄,第二个参数表示一个在THREADINFOCLASS枚举(见ntddk.h中)的系统定义的值,如下,官方文档又没给。
typedef enum _THREAD_INFORMATION_CLASS {
ThreadBasicInformation,
ThreadTimes,
ThreadPriority,
ThreadBasePriority,
ThreadAffinityMask,
ThreadImpersonationToken,
ThreadDescriptorTableEntry,
ThreadEnableAlignmentFaultFixup,
ThreadEventPair,
ThreadQuerySetWin32StartAddress,
ThreadZeroTlsCell,
ThreadPerformanceCount,
ThreadAmILastThread,
ThreadIdealProcessor,
ThreadPriorityBoost,
ThreadSetTlsArrayAddress,
ThreadIsIoPending,
ThreadHideFromDebugger // 17 (0x11)
} THREAD_INFORMATION_CLASS, *PTHREAD_INFORMATION_CLASS;
当设置为ThreadHideFromDebugger,然后调用该函数,如果处于非调试状态,什么事没有,如果处于调试状态,调试器和该进程都停止。
下面还是举个例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include<stdio.h> #include<windows.h>
void test5() { typedef enum _THREAD_INFORMATION_CLASS { ThreadBasicInformation, ThreadTimes, ThreadPriority, ThreadBasePriority, ThreadAffinityMask, ThreadImpersonationToken, ThreadDescriptorTableEntry, ThreadEnableAlignmentFaultFixup, ThreadEventPair, ThreadQuerySetWin32StartAddress, ThreadZeroTlsCell, ThreadPerformanceCount, ThreadAmILastThread, ThreadIdealProcessor, ThreadPriorityBoost, ThreadSetTlsArrayAddress, ThreadIsIoPending, ThreadHideFromDebugger } THREAD_INFORMATION_CLASS, * PTHREAD_INFORMATION_CLASS;
typedef NTSTATUS(WINAPI* ZWSETINFORMATIONTHREAD)( HANDLE ThreadHandle, THREAD_INFORMATION_CLASS ThreadInformationClass, PVOID ThreadInformation, ULONG ThreadInformationLength );
ZWSETINFORMATIONTHREAD myZwSetInformationThread;
myZwSetInformationThread=(ZWSETINFORMATIONTHREAD) GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwSetInformationThread"); myZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
MessageBoxA(NULL, "No Debugger", "The_Itach1", MB_OK);
}
int main() { test5(); system("pause"); }
|
ida调试看看是不是真的。
可以看到ida无法退出,程序也闪退了。
破解方法,patch掉,或者钩取该函数,修改传入的第二个参数为0。
TLS回调函数
TLS回调函数是指,没当创建/终止进程的线程时会自动调用执行的函数,创建进程的主线程时也会自动调用回调函数,且其调用执行先于EP代码,反调试技术利用的就是TLS回调函数的这一特征。
可以先于EP执行,意味着我们可以干很多事情,搞反调试在TLS函数里面,修改全局变量数据,加密解密代码区等等事情。
下面就来举个TLS反调试的简单例子,x86,Debug下编译,release编译出来的好像有问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include<stdio.h> #include<Windows.h>
#pragma comment(linker, "/INCLUDE:__tls_used")
void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved) { BOOL check;
check = IsDebuggerPresent(); if (check == TRUE) { MessageBoxA(NULL, "Being Debugger", "The_Itach1", MB_OK); exit(0); } else { MessageBoxA(NULL, "No Debugger", "The_Itach1", MB_OK); } }
#pragma data_seg(".CRT$XLX") PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1, 0 }; #pragma data_seg()
int main() { printf("this is main function"); }
|
程序在检测到调试就会弹出被调试时窗口。
由于TLS回调函数的特性,该方法可结合各种反调试手段。
ETC
简单来说就是针对各种调试器信息的,检测有没有这些调试器的痕迹出现。
常见函数,FindWindow函数看看是否有ida,xdbg等等窗口。CreateToolhelp32Snapshot(),给进程拍快照,遍历进程,看看有没有ida,xdbg的进程。包括检测虚拟机中的特殊进程之类的。
CreateToolhelp32Snapshot,ctf中就曾出现过,忘了哪个比赛了,还是比较好辨认。
vs的spy++工具可以直接查看窗口的标题和所属类名称。使用findwindows,通过提供窗口标题或者窗口所属类名来判断的方式,自己试了下不太可靠
下面举个GetWindowText查看是否存在xdbg,ida窗口的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <locale.h> //必备头文件 void test6() { setlocale(LC_ALL, ""); HWND hWnd = GetDesktopWindow(); hWnd = GetWindow(hWnd, GW_CHILD); hWnd = GetWindow(hWnd, GW_HWNDFIRST);
TCHAR szWindow[MAX_PATH] = { 0, }; while (hWnd) { if (GetWindowText(hWnd, szWindow, MAX_PATH)) {
_tprintf(L"%s\n", szWindow); if (_tcsstr(szWindow, L"IDA") || _tcsstr(szWindow, L"dbg") ) { MessageBoxA(NULL, "Being Debugger", "The_Itach1", MB_OK); } else { MessageBoxA(NULL, "No Debugger", "The_Itach1", MB_OK); } }
hWnd = GetWindow(hWnd, GW_HWNDNEXT); } }
|
用findwindows有点不准确。
破解方法,绕过就行。
SMC
这个技术就是将代码区给加密或部分加密,在调试的时候进行自解密,就不会对程序产生影响,而带来的效果是ida静态分析时看不到相应代码部分,利用汇编,或者c语言都可实现,可配合TLS回调函数进行,在ep前进行main函数的自解密。
先讲讲汇编实现的原理,实际上和21章内嵌补丁练习差不多,本次安洵出题也是这样实现的,编译好程序exe后,先指定好需要加密的部分,用idapython进行加密,选定一个这个代码部分前一个要跳转的部分(jmp指令之类的),修改跳转地址,改到一个空白区,当然空白区部分的权限需要修改为RWE(读写执行),然后编写汇编代码进行自解密,解密完成后再跳转到原应跳转的位置。例子可以去看安洵的那个题。
下面讲讲如何用c语言代码实现。
环境配置,release,x86,优化关闭,数据执行保护(DEP)关闭,随机基地关闭。
下面讲为什么需要设置这些,优化关闭可以直接定位到函数地址,不关闭的话会进入函数会有一个jmp 然后再跳到函数地址,简单来说就是优化时,函数所代表的地址不是函数真正的地址。数据执行保护关闭可以让你直接修改函数代码的二进制。随机地址关闭可以让你每次加载的基地址一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include<stdio.h> #include<windows.h>
#pragma code_seg(".SMC"); void func() { printf("this is a function"); }
void func2() { }
#pragma code_seg() #pragma comment(linker, "/SECTION:.SMC,ERW") void Decode() { void* p,*q; p = func; q = func2;
int i;
for (i = 0; i < (int)q - (int)p; i++) { *((unsigned char*)p + i) = *((unsigned char*)p + i) ^ 5; printf("%02X ", *((unsigned char*)p + i)); }
}
int main() { Decode(); func(); system("pause"); }
|
加密部分呢很简单就是一个异或,所以解密也是这个,如何实现的问题,上面的例子是通过获取函数地址来实现的,这也是为什么要关优化的原因。而且加了个SMC节区,主要是为了方便我们找到二进制文件的位置方便修改。编译的时候注意,在加密函数前加个断点,然后才开始执行,然后停止,然后将生成的exe,找到smc节区,去修改为加密后的数据,这样exe执行的时候就是解密了。
其实,还有中方法去定位到SMC节区,就是先把exe映射到内存,利用pe结构的知识点,找到smc节区,然后去加密解密。
异常处理
https://syclover.feishu.cn/docs/doccnmKCtL4ABewkf89aAJXIdmg#J1tPpN