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)(); // address of TEB
pPEB = (LPBYTE) * (LPDWORD)(pTEB + 0x30); // address of PEB

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();
}

破解方法和上面差不多。

NtQueryInformationProcess

用这个函数可以用来获取各种与进程调试的信息。官方文档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()
{
//test1();
//test2();
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)的对应值。

NtQuerySystemInformation

用来检测自己的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()
{
//test1();
//test2();
//test3();
test4();

system("pause");
}

当处于调试时,结构体的两个变量都是1,非调试时,结构体第一个变量为0,第二个变量为1。

破解方法,C:\WINDOWS\system32> bcdedit /debug off ;关闭

NtQueryObject

原理就是调试器调试进程时,会产生一个调试对象的内核对象,所以我们只需要检查这个内核对象是否存在即可。

代码太多了。。。焯

ZwSetInformationThread

该函数是用来设置线程信息的,也是一种反调试的手段,可以将被调试程序强制退出调试状态,对于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 // 17 (0x11)
} 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>

//linker spec通知链接器PE文件要创建TLS目录,当然32位和64位会有些差别。
#pragma comment(linker, "/INCLUDE:__tls_used")
//函数格式
//typedef void (NTAPI* PIMAGE_TLS_CALLBACK)(
// PVOID DllHandle,
// DWORD Reason,
// PVOID Reserved
// );

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);
}
}

//创建TLS段 并定义一个回调函数。
#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); //如果指定的窗口是父窗口,则检索到的句柄标识Z顺序顶部的子窗口;
hWnd = GetWindow(hWnd, GW_HWNDFIRST);

TCHAR szWindow[MAX_PATH] = { 0, };
//遍历所有桌面上的窗口名称来比较是否存在ida,xdbg
while (hWnd)
{
if (GetWindowText(hWnd, szWindow, MAX_PATH))
{

_tprintf(L"%s\n", szWindow);//打印窗口名称
//_tcsstr,字符1 在字符串2中首次出现的位置,未出现返回NULL值
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>
//将这个会被加密的函数放入SMC节区中,SMC节区的属性设置为ERW
#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;

//printf("%x\n", p);
//printf("%x\n", q);
for (i = 0; i < (int)q - (int)p; i++)
{
*((unsigned char*)p + i) = *((unsigned char*)p + i) ^ 5;
printf("%02X ", *((unsigned char*)p + i));
}


}

//50 8E E9 6D 05 34 45 05 ED 36 F5 FA FA 86 C1 01 58 C6 C9 C9 C9 C9 C9 C9 C9 C9 C9 C9 C9 C9 C9 C9
int main()
{
Decode();
func();
system("pause");
}

加密部分呢很简单就是一个异或,所以解密也是这个,如何实现的问题,上面的例子是通过获取函数地址来实现的,这也是为什么要关优化的原因。而且加了个SMC节区,主要是为了方便我们找到二进制文件的位置方便修改。编译的时候注意,在加密函数前加个断点,然后才开始执行,然后停止,然后将生成的exe,找到smc节区,去修改为加密后的数据,这样exe执行的时候就是解密了。

其实,还有中方法去定位到SMC节区,就是先把exe映射到内存,利用pe结构的知识点,找到smc节区,然后去加密解密。

异常处理

https://syclover.feishu.cn/docs/doccnmKCtL4ABewkf89aAJXIdmg#J1tPpN