反调试技术

Posted by nop on 2021-08-08
Words 1.6k In Total

原文 Anti-Debug: Assembly instructions

检测思路:根据调试器在CPU执行指令时的行为来检测调试器的存在

INT 3

INT 3是被用作软件断点的中断(中断号为3)。在没有调试器的情况下,进入该中断后会触发异常EXCEPTION_BREAKPOINT(0x80000003);相反地,如果存在调试器,就不会进入到异常处理。c/c++对应的代码如下(x86):

1
2
3
4
5
6
7
8
9
10
bool IsDebugged()
{
__try{
__asm int 3;
return true;
}
__except(EXCEPTION_EXECUTE_HANDLER){
return false;
}
}

INT 3对应的机器码为0xCC(短指令形式),此外还有长指令形式,如CD 03。当捕捉到异常 EXCEPTION_BREAKPOINT时,Windows会将EIP寄存器递减到机器码(0x03)假定的位置,然后将控制权转交给异常处理句柄。在INT 3的长指令机器码为CD 03的情况下,EIP会指向指令的中间(0x03 byte的偏移),因此,如果我们想要在INT3指令之后想要继续执行,就应该在异常处理中编辑EIP,否则就很可能得到异常 EXCEPTION_ACCESS_VIOLATION。当然,也可以忽略指令指针的修改。

实际测试时发现CD 03中的03是对应的3号中断,比如CD 10对应int 10h

INT3
INT3
INT3

在Windbg下调试时发现,执行前CD 03对应的汇编指令为INT 3,执行之后机器码`03与后面的机器码结合,被错误的识别为其他指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int filter(unsigned int code, struct _EXCEPTION_POINTERS *ep)
{
isDebug = code != EXCEPTION_BREAKPOINT;
return EXCEPTION_EXECUTE_HANDLER;
}

bool IsDebugged()
{
__try {
__asm __emit(0xCD);
__asm __emit(0x03);
}
__except (filter(GetExceptionCode(), GetExceptionInformation())) {
return isDebug;
}
}

INT 2D

INT 3一样,进入中断后会抛出异常EXCEPTION_BREAKPOINT,但是不同的是Windows使用EIP寄存器记录异常地址并随后增加EIP的值。此外,INT 2D执行时,Windows会检查EAX的值,如果是1,3,4(所有的Windows版本)或5(Vista+),异常的地址会加一。
该指令造成一些问题,以为执行之后EIP的增加会跳过一个字节的代码数据,导致继续执行的代码“错位”。
下述示例中,使用NOP指令填充EIP跳过的代码数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool IsDebugged()
{
__try{
__asm{
xor eax, eax
int 0x2d
nop
}
return true;
}
__except(EXCEPTION_EXECUTE_HANDLER){
return false;
}
}

ICE

ICE指令是Intel位公开的一个指令,对应的opcode为 0xF1。这个指令可以用来检测程序是否被“跟踪”。ICE指令执行之后会触发异常EXCEPTION_SINGLE_STEP(0x8000004),但是需要注意的是如果这个程序已经被“跟踪”了,那调试器会把触发的异常当作执行指令设置了标志寄存器的SingleSetp位生成的普通异常来处理,因此在调试器下,对应的异常处理句柄不会被调用而是在ICE执行之后继续执行其后面的代码。

1
2
3
4
5
6
7
8
9
10
bool IsDebugged()
{
__try {
__asm __emit 0xF1;
return true;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}

Stack Segment Register

这属于一个小技巧可以用来检测程序是否被“跟踪”,检测通过下述的几个指令完成:

1
2
3
push ss
pop ss
pushf

单步执行这部分代码时,会设置TrapFlag标志位,正常情况下这个标志位不可见,因为调试器在每个调试事件传递之后清空这个标志位。但是,我们可以保存标志寄存器到栈中,进而就能判断该标志位是否被设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool IsDebugged()
{
bool bTraced = false;
__asm {
push ss
pop ss
pushf
test byte ptr [esp + 1], 1
jz movss_not_being_debugged
}
bTraced = true;
movss_not_being_debugged:
__asm popf;
return bTraced;
}

Instruction Counting

这个技术利用了一些调试器对异常EXCEPTION_SINGLE_STEP的处理方式

这个技巧的想法是在一些预定义序列(如NOP序列)的每一个指令设置硬件断点。设置了硬件断点之后执行这些指令会触发异常EXCEPTION_SINGLE_STEP,并且这个异常可以被特定的异常处理函数捕获。在异常处理中,使用一个寄存器用来计算异常触发次数,当预定义序列执行完成之后通过比较序列长度和计数器计算的数值来判断进程是否被调试。

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
#include "hwbrk.h"

static LONG WINAPI InstructionCountingExeptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
{
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{
pExceptionInfo->ContextRecord->Eax += 1;
pExceptionInfo->ContextRecord->Eip += 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}

__declspec(naked) DWORD WINAPI InstructionCountingFunc(LPVOID lpThreadParameter)
{
__asm
{
xor eax, eax
nop
nop
nop
nop
cmp al, 4
jne being_debugged
}

ExitThread(FALSE);

being_debugged:
ExitThread(TRUE);
}

bool IsDebugged()
{
PVOID hVeh = nullptr;
HANDLE hThread = nullptr;
bool bDebugged = false;

__try
{
hVeh = AddVectoredExceptionHandler(TRUE, InstructionCountingExeptionHandler);
if (!hVeh)
__leave;

hThread = CreateThread(0, 0, InstructionCountingFunc, NULL, CREATE_SUSPENDED, 0);
if (!hThread)
__leave;

PVOID pThreadAddr = &InstructionCountingFunc;
// Fix thread entry address if it is a JMP stub (E9 XX XX XX XX)
if (*(PBYTE)pThreadAddr == 0xE9)
pThreadAddr = (PVOID)((DWORD)pThreadAddr + 5 + *(PDWORD)((PBYTE)pThreadAddr + 1));

for (auto i = 0; i < m_nInstructionCount; i++)
m_hHwBps[i] = SetHardwareBreakpoint(
hThread, HWBRK_TYPE_CODE, HWBRK_SIZE_1, (PVOID)((DWORD)pThreadAddr + 2 + i));

ResumeThread(hThread);
WaitForSingleObject(hThread, INFINITE);

DWORD dwThreadExitCode;
if (TRUE == GetExitCodeThread(hThread, &dwThreadExitCode))
bDebugged = (TRUE == dwThreadExitCode);
}
__finally
{
if (hThread)
CloseHandle(hThread);

for (int i = 0; i < 4; i++)
{
if (m_hHwBps[i])
RemoveHardwareBreakpoint(m_hHwBps[i]);
}

if (hVeh)
RemoveVectoredExceptionHandler(hVeh);
}

return bDebugged;
}

POPF and Trap Flag

TrapFlag是标志寄存器中的一个标志位,当这个标志位被设置时,会抛出异常SINGLE_STEP,因为如果我们跟踪代码,这个标志位会被调试器清零,所以我们看不到这个异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool IsDebugged()
{
__try
{
__asm
{
pushfd
mov dword ptr [esp], 0x100
popfd
nop
}
return true;
}
__except(GetExceptionCode() == EXCEPTION_SINGLE_STEP
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_EXECUTION)
{
return false;
}
}

实际测试时发现,如果直接步过检测调试的函数,那么调试不会被检测到,只有步入检测函数执行时才会检测到

Instruction Prefixes

这个技术利用了一些调试器处理指令前缀的方式,所以只对一些调试器有效。

原文描述为:

If we execute the following code in OllyDbg, after stepping to the first byte F3, we’ll immediately get to the end of try block. The debugger just skips the prefix and gives the control to the INT1 instruction.
If we run the same code without a debugger, an exception will be raised and we’ll get to except block.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool IsDebugged()
{
__try
{
// 0xF3 0x64 disassembles as PREFIX REP:
__asm __emit 0xF3
__asm __emit 0x64
// One byte INT 1
__asm __emit 0xF1
return true;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return false;
}
}

测试时和上一个方式一样,只有在单步跟入的时候才会检测到调试,调试过程中发现vs和WinDbg并未识别这部分代码对应的汇编代码,单步时直接执行了return true

INT3
INT3


You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.