反调试总结

API 检测

1
2
3
4
5
6
PBYTE pCC = (PBYTE)MessageBoxW;
if (*pCC == 0xCC)
{
return true;
}
MessageBoxW(0, L"未发现调试器!\n", 0, 0);

在调试器中下断点时,会将目标地址的首字节替换为0xCC,上述代码以此进行判断会否处于调试状态

破解思路:避免在函数的首地址下断点,可以在函数的其他代码部分下断,通常在ret指令处下断

PEB BeingDebugged

1
2
3
4
5
__asm {
MOV EAX, DWORD PTR FS : [0x30] ;获取PEB
MOV AL, BYTE PTR DS : [EAX + 2]
MOV bDebugged, AL
}

当进程被调试时,AL的值为1,这个值取自PEB的BeingDebugged成员.

代码校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool CheckDebug_Checksum()
{
BOOL bDebugging = FALSE;
__asm {
call CHECKBEGIN
CHECKBEGIN:
pop esi
mov ecx, 0x15 // ecx : loop count
xor eax, eax // eax : checksum
xor ebx, ebx

_CALC_CHECKSUM :
movzx ebx, byte ptr ds : [esi]
add eax, ebx
rol eax, 1
inc esi
loop _CALC_CHECKSUM
cmp eax, 0x1859a602
je _NOT_DEBUGGING
mov bDebugging, 1
_NOT_DEBUGGING:
}
return bDebugging;
}

调试信号判断

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
int main()
{
// 尝试打开互斥体,确定是否首次运行程序
HANDLE hMutex = OpenMutex(MUTEX_MODIFY_STATE, FALSE, L"Global\\MyMutex");
if (hMutex)
{
// 打开成功说明第2次运行,执行正常代码
printf("正被调试运行!\n");
getchar();
}
else
{
// 打开失败说明第1次运行,创建互斥体,并调试创建自身进程
CreateMutex(NULL, FALSE, L"Global\\MyMutex");
TCHAR szPath[MAX_PATH] = {};
GetModuleFileName(NULL, szPath, MAX_PATH);
// 调试方式打开程序
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi = {};
// 正常创建,后面附加调试
BOOL bStatus = CreateProcess(szPath, NULL, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE,
NULL, NULL, &si, &pi);
if (!bStatus) {
printf("创建进程失败!\n");
return 0;
}
if (!DebugActiveProcess(pi.dwProcessId)) {
printf("附加进程失败!\n");
return 0;
}
// 初始化调试事件结构体
DEBUG_EVENT DbgEvent = { 0 };
DWORD dwState = DBG_EXCEPTION_NOT_HANDLED;
// 等待目标Exe产生调试事件
BOOL bExit = FALSE;
while (!bExit) {
WaitForDebugEvent(&DbgEvent, INFINITE);
if (DbgEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT)
{
// 被调试进程退出
bExit = TRUE;
}
ContinueDebugEvent(DbgEvent.dwProcessId, DbgEvent.dwThreadId, dwState);
}
return 0;
}
return 0;
}

以调试状态启动自身,等待调试事件触发,通过调试事件Code判断当前是否是调试状态,由于创建了互斥体,所以自身创建的调试进程启动后就退出了,不会出现两个软件同时运行的情况

破解思路: 跳转patch

窗口遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PROCESSENTRY32 pe32 = { sizeof(pe32) };
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
Process32First(hProcessSnap, &pe32);
do
{
// 这里只比较了OllyDbg,也可以添加其他的调试分析工具名
if (_tcsicmp(pe32.szExeFile, TEXT("OllyDbg.exe")) == 0)
{
CloseHandle(hProcessSnap);
return TRUE;
}
} while (Process32Next(hProcessSnap, &pe32));
CloseHandle(hProcessSnap);

通过遍历当前所有的窗口列表,以此判断是否有进程名为调试器名字的软件

父进程判断

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
struct PROCESS_BASIC_INFORMATION
{
DWORD ExitStatus;
DWORD PebBaseAddress;
DWORD AffinityMask;
DWORD BasePriority;
ULONG UniqueProcessId;
ULONG InheritedFromUniqueProcessId;
}pbi = {};
NtQueryInformationProcess(GetCurrentProcess(), ProcessBasicInformation, (PVOID)&pbi, sizeof(pbi), NULL);
PROCESSENTRY32 pe32 = { sizeof(pe32) };
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
Process32First(hProcessSnap, &pe32);
do
{
if (pbi.InheritedFromUniqueProcessId == pe32.th32ProcessID)
{
if (_tcsicmp(pe32.szExeFile, TEXT("explorer.exe")) == 0)
{
CloseHandle(hProcessSnap);
return FALSE;
}
else
{
CloseHandle(hProcessSnap);
return TRUE;
}
}
} while (Process32Next(hProcessSnap, &pe32));
CloseHandle(hProcessSnap);
return FALSE;

NtQueryInformationProcess这个函数是未被公开的一个函数,在ntdll中,可以通过getprocaddress来调用,函数原型:

1
2
3
4
5
6
7
NTSTATUS WINAPI NtQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);

ProcessHandle:查询进程的句柄

ProcessInformationClass: 查找信息的标识符,可以取以下值:

1
2
3
4
5
ProcessBasicInformation   0
ProcessDebugPort 7
ProcessWow64Information 26
ProcessImageFileName 27
ProcessBreakOnTermination 29

ProcessInformation:要存放查询结果的缓冲区,这个结构要根据第二个参数来决定,
ProcessInformationLength:缓冲区大小

ReturnLength:实际返回的写入缓冲区的字节数

当调用此函数,传入当前进程的句柄,参数2传入ProcessBasicInfomation,在参数3 就会返回PROCESS_BASIC_INFORMATION这个结构体,文中有定义,这个结构体中就包含了父进程的句柄,在通过遍历的方式判断父进程的名字是不是explorer即可

FindWindow

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

bool CheckDebug_FindWindow() {
// OD的主窗口类名为 OLLYDBG,也可以查询其他调试器的类名
// 其他常用调试器的类名可以使用Spy++查看
if (FindWindow(TEXT("OLLYDBG"), NULL))
return true;
return false;
}
BOOL CALLBACK EnumWindowProc(HWND hWnd, LPARAM lParam) {
TCHAR winTitle[0x100] = {};
GetWindowText(hWnd, winTitle, 0x100);
if (_tcsstr(winTitle, TEXT("OllyDbg")))
{
// nFind=true
*((int*)lParam) = true;
// 找到目标窗口停止遍历
return false;
}
// 继续遍历下一个窗口
return true;
}
bool CheckDebug_EnumWindow() {
int nFind = false;
EnumWindows(EnumWindowProc, (LPARAM)&nFind);
return nFind != 0;
}

这个和之前的窗口遍历差不多,不过实现方式不同

硬件断点 & 异常

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

// API查询
bool CheckDebug_HB()
{
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;
}
// 触发异常查询
bool CheckDebug_HB_EXCEPTION()
{
BOOL bDebugging = FALSE;
__asm {
// install SEH
push handler
push DWORD ptr fs : [0]
mov DWORD ptr fs : [0], esp
__emit(0xcc)
mov bDebugging, 1
jmp normal_code
handler :
mov eax, dword ptr ss : [esp + 0xc];// ContextRecord
mov dword ptr ds : [eax + 0xb8], offset normal_code;
mov ecx, [eax + 4]; // Dr0
or ecx, [eax + 8]; // Dr1
or ecx, [eax + 0x0C];// Dr2
or ecx, [eax + 0x10];// Dr3
je NoDebugger;
mov ecx, [eax + 0xb4];// ebp
// vs2015 debug下bDebugging的地址为ebp-c
mov [ecx-0x0c],1 // bDebugging
NoDebugger:
xor eax, eax
retn
normal_code :
// remove SEH
pop dword ptr fs : [0]
add esp, 4
}
return bDebugging;
}

CheckDebug_HB函数,通过判断是否有硬件断点,如果有则为调试状态

CheckDebug_HB_EXCEPTION函数,设置SEH异常处理函数后触发异常,如果未在调试状态,则会走到自己的函数中,否则在调试状态下

INT 2D

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

bool CheckDebug_INT_2D() {
BOOL bDebugging = FALSE;
__asm {
// install SEH
push handler
push DWORD ptr fs : [0]
mov DWORD ptr fs : [0], esp
// OD会忽略0x2d和nop,继续向后执行
// 这时候可以选择只是检测调试器还是跑飞
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
}
return bDebugging;
}

此方法和CheckDebug_HB_EXCEPTION差不多,只不过触发异常的方式不同

NtGlobalFlag

1
2
3
4
5
6
7
int nNtFlag = 0;
__asm {
MOV EAX, DWORD PTR FS : [0x30]
MOV EAX, DWORD PTR DS : [EAX + 0x68]
MOV nNtFlag, EAX
}
return nNtFlag==0x70;

在32位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64位机器则是在偏移0xBC位置. 该字段的默认值为0. 当调试器正在运行时, 该字段会被设置为一个特定的值:

1
2
3
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

可以通过内存访问断点来定位反调试代码

或者修改注册表,来替换GlobalFlag的值:

注册表HKLM\System\CurrentControlSet\Control\SessionManagerGlobalFlag的值会替换进行NtGlobalFlag字段. 尽管它随后还可能由Windows改变(以下会介绍), 注册表键值会对系统中所有进程产生影响并在重启后生效.

NtQueryInformationProcess的另外三种调试判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

bool CheckDebug_DebugPort() {
DWORD dwDebugPort = 0;
NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &dwDebugPort, 4, 0);
return dwDebugPort == -1;
}

bool CheckDebug_DebugHandle() {
DWORD dwDebugHandle = 0;
NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)0x1E, &dwDebugHandle, 4, 0);
return dwDebugHandle != 0;
}

// 此方法win7下已失效
bool CheckDebug_DebugFlags() {
bool bDebugFlags = 0;
NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)0x1F, &bDebugFlags, 1, 0);
printf("%d\n", bDebugFlags);
return bDebugFlags == 0;
}

参数二 = ProcessDebugPort

非调试状态下debugport == 0

参数二 = ProcessDebugObjectHandle

第三个参数返回为被调试对象句柄,当返回NULL时说明进程处于非调试状态

参数二 = ProcessDebugFlags

rocessDebugFlags参数用于获取调试标识,DebugFlag的值若为0则处于调试状态,若为1则处于非调试状态

NtQueryObject

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
bool CheckDebug_QueryObject() {
typedef struct _OBJECT_TYPE_INFORMATION
{
UNICODE_STRING TypeNames;
ULONG TotalNumberOfHandles;
ULONG TotalNumberOfObjects;
}OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
typedef struct _OBJECT_ALL_INFORMATION
{
ULONG NumberOfObjectsTypes;
OBJECT_TYPE_INFORMATION ObjectTypeInfo[1];
}OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;
//1.获取欲查询信息大小
ULONG uSize = 0;
NtQueryObject(NULL, (OBJECT_INFORMATION_CLASS)0x03, &uSize, sizeof(uSize), &uSize);
//2.获取对象大信息
POBJECT_ALL_INFORMATION pObjectAllInfo = (POBJECT_ALL_INFORMATION) new BYTE[uSize+4];
NtQueryObject(NULL, (OBJECT_INFORMATION_CLASS)0x03, pObjectAllInfo, uSize, &uSize);
//3.循环遍历并处理对象信息
POBJECT_TYPE_INFORMATION pObjectTypeInfo = pObjectAllInfo->ObjectTypeInfo;
for (int i = 0; i < pObjectAllInfo->NumberOfObjectsTypes; i++)
{
//3.1查看此对象的类型是否为DebugObject
if (!wcscmp(L"DebugObject", pObjectTypeInfo->TypeNames.Buffer))
{
delete[] pObjectAllInfo;
return true;
}
//3.2获取对象名占用空间大小(考虑到了结构体对齐问题)
ULONG uNameLength = pObjectTypeInfo->TypeNames.Length;
ULONG uDataLength = uNameLength - uNameLength % sizeof(ULONG) + sizeof(ULONG);
//3.3指向下一个对象信息
pObjectTypeInfo = (POBJECT_TYPE_INFORMATION)pObjectTypeInfo->TypeNames.Buffer;
pObjectTypeInfo = (POBJECT_TYPE_INFORMATION)((PBYTE)pObjectTypeInfo + uDataLength);
}
delete[] pObjectAllInfo;
return false;
}

系统中某个调试器调试进程时,会创建一个调试对象类型的内核对象。检测该对象是否存在即可判断是否有进程正在被调试

ntdll!NtQueryObject可获得系统各种内核对象信息

1
2
3
4
5
6
7
NTSTATUS NtQueryObject(
_In_opt_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_Out_opt_ PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength,
_Out_opt_ PULONG ReturnLength
);

该API与上面讲过的API使用方法类似

1
2
3
4
5
6
7
typedef enum _OBJECT_INFORMATION_CLASS { 
ObjectBasicInformation,
ObjectTypeInformation,
ObjectNameInformation,
ObjectAllInformation, //3
ObjectHandleInformation
} OBJECT_INFORMATION_CLASS;

ObjectAllInformation(0x3)

使用ObjectAllInformation可获得系统所有对象信息,然后从中检测是否存在调试对象

NtQuerySystemInformation

1
2
3
4
5
6
7
8
9
10
11
12
13
14

bool CheckDebug_KernelDebug() {
struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION
{
BOOLEAN DebuggerEnabled;
BOOLEAN DebuggerNotPresent;
}DebuggerInfo = { 0 };
NtQuerySystemInformation(
(SYSTEM_INFORMATION_CLASS)0x23, //查询信息类型
&DebuggerInfo, //输出查询信息
sizeof(DebuggerInfo), //查询类型大小
NULL); //实际返回数据大小
return DebuggerInfo.DebuggerEnabled;
}

NtQuerySystemInformation这个函数用来获取当前的运行信息

当调用NtQuerySystemInfromation()API的时候,第一个参数SystemInformationClass的值被设置为systemKernelDebuggerInfromation(0x32的时候,函数调用返回的时候,若系统处于调试状态下,DebuggerEnable的值设置为1

ss寄存器

1
2
3
4
5
6
7
__asm
{
push ss
pop ss
mov eax, 0xC000C1EE // This line will be traced over by debugger
xor edx, edx // Debugger will step to this line
}

当使用ss堆栈段寄存器进行操作时,调试器将跳过指令跟踪。如下图所示,调试器将立即移至xor edx,edx指令,同时执行上一条指令

代码运行时长

1
2
3
4
5
6
7
8
bool CheckDebug_QueryPerformanceCounter() {
LARGE_INTEGER startTime , endTime ;
QueryPerformanceCounter(&startTime);
printf("我是核心代码\n也可以是核心代码前的反调试时间检测代码\n");
QueryPerformanceCounter(&endTime);
printf("%llx\n", endTime.QuadPart - startTime.QuadPart);
return endTime.QuadPart - startTime.QuadPart > 0x500;
}

代码运行时长检测不止这一种方法,像GetTickCount等函数都可以实现

rdtsc指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool CheckDebug_RDTSC() {
int64_t t1=0, t2=0;
int lo=0, hi=0;
__asm {
rdtsc
mov [lo], eax
mov [hi], edx
}
t1 = ((int64_t)lo) | ((int64_t)hi << 32);
__asm{
rdtsc
mov[lo], eax
mov[hi], edx
}
t2 = ((int64_t)lo) | ((int64_t)hi << 32);
printf("t2-t1=%llx\n", t2 - t1);
// 不同的CPU该差值不同,所以谨慎使用这种反调试方法
return t2 - t1 > 0x100;
}

此方法与代码运行时长类似

注册表查找调试器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool CheckDebug_Registry() {
// 判断当前系统是32还是64位
BOOL b64 = FALSE;
IsWow64Process(GetCurrentProcess(), &b64);
HKEY hkey = NULL;
TCHAR *reg = b64 ?
TEXT("SOFTWARE\\Wow6432Node\\Microsoft\\WindowsNT\\CurrentVersion\\AeDebug")
: TEXT("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug");
// 打开注册表键
DWORD ret = RegCreateKey(HKEY_LOCAL_MACHINE, reg, &hkey);
if (ret != ERROR_SUCCESS) return FALSE;
TCHAR *subkey = TEXT("Debugger");
TCHAR value[256] = {};
DWORD len = 256;
// 查询对应项的值
ret = RegQueryValueEx(hkey, subkey, NULL,NULL,(LPBYTE)value, &len);
RegCloseKey(hkey);
// 这里只查找了OD,也可以同时查找WinDbg,x64Dbg等常用调试器
if (_tcsstr(value, TEXT("OLLYDBG")) != NULL)
return TRUE;
return FALSE;
}

通过注册表查询是否包含调试器

rep指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool CheckDebug_CheckRepCC()
{
BOOL bDebugging = FALSE;
__asm {
xor eax,eax
xor ecx,ecx
inc ecx
lea esi,key
// 此处步过时key处会被下0xCC断点
// 将key处的首字节给AL
rep lodsb
key:
cmp al,0xcc
je debuging
jmp over
debuging:
mov bDebugging,1
over :
}
return bDebugging;
}

执行到rep指令时,key处地址会被下断,从而与0xCC判断,是否处于调试状态

权限判断

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

bool CheckDebug_SeDebugPrivilege() {
// 获取CsrGetProcessId函数地址
HMODULE hMod = GetModuleHandle(L"ntdll.dll");
typedef int(*CSRGETPROCESSID)();
CSRGETPROCESSID CsrGetProcessId = (CSRGETPROCESSID)GetProcAddress(hMod, "CsrGetProcessId");
// 获取csrss.exe的PID
DWORD pid = CsrGetProcessId();
// 打开成功说明管理员+调试权限
HANDLE hCsr = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (!hCsr)
{
return false;
}
CloseHandle(hCsr);
return true;
}


BOOL CheckDebug_EnumProcess_Csrss()
{
DWORD pid=0;
DWORD ret = 0;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
Process32First(hProcessSnap, &pe32);
do
{
if (wcscmp(pe32.szExeFile, L"csrss.exe") == 0)
{
pid = pe32.th32ProcessID;
break;
}
} while (Process32Next(hProcessSnap, &pe32));
CloseHandle(hProcessSnap);
HANDLE hCss = OpenProcess(PROCESS_QUERY_INFORMATION, NULL, pid);
if (!hCss)
{
return FALSE;
}
CloseHandle(hCss);
return FALSE;
}

上述两个函数的实现不同,但作用是相同的,通过当前权限来判断是否处于调试状态

SEH异常1

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
bool CheckDebug_SEH() {
BOOL bDebugging = FALSE;
__asm {
// install SEH
push handler
push DWORD ptr fs : [0]
mov DWORD ptr fs : [0], esp
__emit(0xcc)
// 只检测有无调试器
// 若把mov bDebugging, 1改成__emit(0xE9)
// 在调试器中就会跑飞
mov bDebugging, 1
jmp normal_code
handler :
mov eax, dword ptr ss : [esp + 0xc];// ContextRecord
mov dword ptr ds : [eax + 0xb8], offset normal_code
xor eax, eax
retn
normal_code :
// remove SEH
pop dword ptr fs : [0]
add esp, 4
}
return bDebugging;
}

如果不在调试器中,则会走到代码中设置的的seh handler,否则将会继续执行

SEH异常2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LONG WINAPI Fun(
_In_ struct _EXCEPTION_POINTERS *ExceptionInfo
) {
// 跳过mov bDebug, 1这条指令
// int 3异常时,eip会被回拨到cc处,所以要+5
ExceptionInfo->ContextRecord->Eip += 5;
return EXCEPTION_CONTINUE_EXECUTION;
}
bool CheckDebug_SetUnhandledExceptionFilter() {
bool bDebug = false;
__asm {
__emit(0xCC);
// 正常运行时,Fun函数会跳过这条指令
// 调试时,调试器会不停收到int 3异常,程序崩溃
mov bDebug, 1
}
return bDebug;
}

在调用CheckDebug_SetUnhandledExceptionFilter之前要先调用SetUnhandledExceptionFilter将Fun设置为SEH的handler,此方法与SEH异常处理1相似

单步检测

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
bool CheckDebug_SingleStep()
{
bool bDebugged = false;
__asm {
// install SEH
push handler
push DWORD ptr fs : [0]
mov DWORD ptr fs : [0], esp
pushfd
or dword ptr ss : [esp], 0x100
popfd
// 被调试就继续执行
nop
mov bDebugged,1
jmp normal_code
handler :
mov bDebugged, 1
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
}
return bDebugged;
}

TF=1 的时候,会触发单步异常。该方法属于异常处理,不过比较特殊:未修改的 OD 无论
是 F9 还是 F8 都不能处理异常,有插件的 OD 在 F9 时能正确处理,F8 时不能正确处理。

StartInfo检测

1
2
3
4
5
6
7
8
9
10
bool CheckDebug_STARTUPINFO() {
STARTUPINFO si = {};
GetStartupInfo(&si);
if (si.dwX || si.dwY || si.dwXSize || si.dwYSize)
{
printf("%x %x %x %x\n", si.dwX, si.dwY, si.dwXSize, si.dwYSize);
return true;
}
return false;
}

ZwSetInformationThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool CheckDebug_SetInformationThread() {
enum THREAD_INFO_CLASS
{
ThreadHideFromDebugger = 17
};
typedef NTSTATUS(NTAPI *ZW_SET_INFORMATION_THREAD)(
IN HANDLE ThreadHandle,
IN THREAD_INFO_CLASS ThreadInformationClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength);
ZW_SET_INFORMATION_THREAD ZwSetInformationThread;
ZwSetInformationThread = (ZW_SET_INFORMATION_THREAD)GetProcAddress(LoadLibrary(L"ntdll.dll"), "ZwSetInformationThread");
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, NULL);
return true;
}bool CheckDebug_STARTUPINFO() {
STARTUPINFO si = {};
GetStartupInfo(&si);
if (si.dwX || si.dwY || si.dwXSize || si.dwYSize)
{
printf("%x %x %x %x\n", si.dwX, si.dwY, si.dwXSize, si.dwYSize);
return true;
}
return false;
}

ZwSetInformationThread是一个未公开的函数,该API被调试者可将自身从调试器中分离出来(使调试进程终止运行,同时终止自身进程),该API不会对正常运行的程序(非调试运行)产生任何影响

TLS反调试

TLS相关知识:https://bbs.pediy.com/thread-249572.htm

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <iomanip>
using namespace std;

#pragma comment(linker, "/INCLUDE:__tls_used")
void lookupprocess(void);
void Debugger(void);
void NTAPI tls_callback(PVOID h, DWORD reason, PVOID pv)
{
lookupprocess();
Debugger();
MessageBox(NULL,"Not Main!","Test1",MB_OK);
return;
}
//创建TLS段
#pragma data_seg(".CRT$XLT")
//定义回调函数
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
#pragma data_seg()

// 定义静态TLS全局变量
__declspec(thread) int value =0xcccccccc;

DWORD WINAPI NewThread ( LPVOID lParam )
{
// 设置子线程value,并不影响其他线程
value = *((int*)lParam);

while(1)
{
printf("%d\n",value);
Sleep(1000);
}

return 0 ;
}

#define THREAD_NUM 3

int main(int argc, char* argv[])
{
int arry[3]={1,2,3};
// 设置主线程静态TLS的value为5
value = 5 ;
// 创建子线程
HANDLE hThread[THREAD_NUM];

for (int loop = 0; loop < THREAD_NUM; loop++)
{
hThread[loop] = CreateThread ( NULL, 0, NewThread, &arry[loop], 0, NULL );
Sleep(20000);
}

// 等待直到子线程结束
WaitForMultipleObjects(THREAD_NUM, hThread, TRUE, INFINITE);

return 0;
}

void lookupprocess()
{
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
BOOL bMore = ::Process32First(hProcessSnap,&pe32);
while(bMore)
{
strlwr(pe32.szExeFile);
if (!strcmp(pe32.szExeFile,"ollyice.exe"))
{
exit(0);
}
if (!strcmp(pe32.szExeFile,"ollydbg.exe"))
{
exit(0);
}
if (!strcmp(pe32.szExeFile,"peid.exe"))
{
exit(0);
}
if (!strcmp(pe32.szExeFile,"idaq.exe"))
{
exit(0);
}
bMore = ::Process32Next(hProcessSnap,&pe32);
}
::CloseHandle(hProcessSnap);
}

//anti-debug2
void Debugger(void)
{
int result=0;
__asm
{
mov eax, dword ptr fs:[30h]//TEB偏移30H处
movzx eax, byte ptr ds:[eax+2h]//取PEB中BeingDebug,若为1则被调试
mov result,eax
}
if (result)
exit(0);
}
Author: BarretGuy
Link: https://basicbit.cn/2018/11/30/2018-11-30-反调试总结/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.