0%

反调试

静态反调试技术:

许多反调试对OS有较强的依赖性。

利用PEB结构体的一些信息可以判断反调试。

PEB结构体中与反调试密切相关的成员:

BeingDebugged(一个标志,表示进程是否处于被调试状态)

NtGlobalFlag(与被调试进程堆栈有关)

BeingDebugged:

进程处于被调试状态时,BeingDebugged(+0x2)的值被设置为1,未调试状态则为0;

可以通过IsDebuggerPresent()函数获取该值来判断是否处于调试状态。

破解之法:

将该标志位设置为0即可

NtGlobalFlag:

调试进程时,被调试进程堆内存有特殊标志,NtGlobalFlag(+0x68)的值被设置为0x70.

破解之法:

将该标志位设置为0即可

将运行进程附加到调试器,该标志位不变。

NtQueryInformationProcess():

通过该函数可以获取与进程相关的信息,函数定义如下:

1
2
3
4
5
6
7
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
1
[in] ProcessInformationClass

此参数可以是PROCESSINFOCLASS枚举中的值。

其中与调试器探测相关的成员有:

ProcessDebugPort,ProcessDebugObjectHandle,ProcessDebugFlags。

ProcessDebugPort(+0x7):

ProcessInformationClass参数值设置为ProcessDebugPort,调用NtQueryInformationProcess()函数就能获取调试端口。若进程处于调试状态,dwDebugPort设置为0,否则,设置为0xFFFFFFFF。

1
2
3
4
5
6
7
DWORD dwDebugPort = 0;
NtQueryInformationProcess(GetCurrentProcess(),
ProcessDebugPort,
&dwDebugPort,
sizeof(dwDebugPort),
NULL
)

CheckRemoteDebuggerPresent():

该函数可以检查当前进程和其他进程是否处于被调试状态。该函数也调用了NtQueryInformation Process()函数。

ProcessDebugObjectHandle(+0x1E):

1
2
3
4
5
6
7
DWORD hDebugObject = NULL;
NtQueryInformationProcess(GetCurrentProcess(),
ProcessDebugObjectHandle,
&hDebugObject,
sizeof(hDebugObject),
NULL
)

调用函数,调试状态时第三个参数为调试对象句柄,非调试状态则为NULL。

ProcessDebugFlags(+0x1F):

1
2
3
4
5
6
7
DWORD bDebugFlag = TRUE;
NtQueryInformationProcess(GetCurrentProcess(),
ProcessDebugFlags,
&bDebugFlag,
sizeof(bDebugFlag),
NULL
)

调用函数第三个参数可获取到调试标志位。为0处于被调试状态,为1则为非调试。

破解之法:

若只调用几次函数,可以手动设置值,如果反复调用,则需要使用API钩取技术。(钩取时最好从函数第二个字节钩取,有些保护器会检查第一个字节来判断是否被钩取)。

NtQuerySystemInformation():

基于调试环境检测的反调试技术。

1
2
3
4
5
6
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);

SYSTEM_INFORMATION_CLASS SystemInformationClass参数指定需要的系统信息类型,

将结构体地址传递给 PVOID SystemInformation。

SYSTEM_INFORMATION_CLASS是枚举类型

判断OS是否处于调试模式下运行的参数:

SystemKernelDebuggerInformation(0x23):

1
2
3
4
5
NtQuerySystemInformation(SystemKernelDebuggerInformation,
(PVOID)&DebuggerInfo,
sizeof(DebuggerInfo),
&ulReturnedLength
)

第一个参数为SystemKernelDebuggerInformation时,第二个参数为结构体SYSTEM_KERNEL_DEBUGGER_INFORMATION的地址。若系统处于调试状态,SYSTEM_KERNEL_DEBUGGER_INFORMATION.DebuggerEnabled值设置为1.

NtQueryObject():

NtQueryObject()可获取内核对象信息。

1
2
3
4
5
6
7
__kernel_entry NTSYSCALLAPI NTSTATUS NtQueryObject(
[in, optional] HANDLE Handle,
[in] OBJECT_INFORMATION_CLASS ObjectInformationClass,
[out, optional] PVOID ObjectInformation,
[in] ULONG ObjectInformationLength,
[out, optional] PULONG ReturnLength
);
1
2
3
4
5
6
7
typedef enum _OBJECT_INFORMATION_CLASS {
ObjectBasicInformation,
ObjectNameInformation,
ObjectTypeInformation,
ObjectAllTypesInformation,
ObjectHandleInformation
} OBJECT_INFORMATION_CLASS, *POBJECT_INFORMATION_CLASS;

首先使用ObjectAllTypesInformation获取系统所有对象信息,然后检测是否存在调试对象。

NtQueryObject()使用方法:

1.获取内核对象信息链表:

1
2
3
ULONG lSize = 0

pNtQueryObject(NULL,ObjectAllTypesInformation,&Size,sizeof(lSize),&lSize);

2.分配内存:

1
2
void *pBuf = NULL;
pBuf = VirtualAlloc(NULL,lSize,MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE);

3.获取内核对象信息链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
 typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING TypeName;
ULONG TotalNumberOfHandles;
ULONG TotalNumberOfObjects;
}OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;

typedef struct _OBJECT_ALL_INFORMATION {
ULONG NumberOfObjectsTypes;
OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
} OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;

pNtQueryObject((HANDLE)0xFFFFFFFF, ObjectAllTypesInformation, pBuf, lSize, NULL);
POBJECT_ALL_INFORMATION pObjectAllInfo = (POBJECT_ALL_INFORMATION)pBuf;

调用函数后,所有对象信息代码被存入pBuf,将pBuf转化为OBJECT_ALL_INFORMATION类型。OBJECT_ALL_INFORMATION结构体由OBJECT_TYPE_INFORMATION结构体数组组成。实际内核对象存储在OBJECT_TYPE_INFORMATION结构体数组中,通过循环检索即可查看是否存在“调试对象”对象类型。

4.确定”调试对象“对象类型。

破解之法:

在调用NtQueryObject()时第二个参数ObjectAllTypesInformation(3)将该值修改为0即可。

ZwSetInformationThread():

该函数可以强制分离被调试者和调试器,

1
2
3
4
5
6
NTSYSAPI NTSTATUS ZwSetInformationThread(
[in] HANDLE ThreadHandle,
[in] THREADINFOCLASS ThreadInformationClass,
[in] PVOID ThreadInformation,
[in] ULONG ThreadInformationLength
);

第二个参数表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11),调试该函数时,调试进程就被分离出来了。该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程序,因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终会停止调试。

还有一个函数 DebugActiveProcessStop 用来分离调试器和被调试进程,从而停止调试。

破解之法:

将第二个参数修改为0。

ETC

比较直接的反调试技术
FindWindow() ->检测OllyDbg窗口
CreateToolhelp32Snapshot()检测OllyDbg进程
… …

一片比较好的文章:

反调试

动态反调试:

异常:

SEH:

一些常见的异常:

EXCEPTION_BREAKPOINT:

若程序处于调试状态,触发异常时,系统会立刻停掉运行程序,将控制权转给调试器。修改调试器选项可以把处理中的进程产生的异常转给操作系统,自动调用SEH。

触发异常时,pContext->Eip成员会保存处理完异常后的返回地址。

SetUnhandledExceptionFilter():

当SEH未注册,或者不存在时,UnhandledExceptionFilter()函数内部会运行最后一个异常处理器,会弹出错误,然后终止程序。

该函数内部使用了NtQueryInformationProcess API,判断是否处于调试进程。

SetUnhandledExceptionFilter()可以修改程序最后的异常处理器。

1
2
3
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
[in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

只需要将新的TopLevelExceptionFilter传给lpTopLevelExceptionFilter即可。返回值为上一个函数的

Exception Filter.

破解之法:

先要让UnhandledExceptionFilter()函数内部调用的NtQueryInformationProcess API失效(API钩取技术)。然后调用SetUnhandledExceptionFilter()函数跟踪注册的Exception Filter,在正常运行时要调到哪个位置。

Timing Cheak:

该技术运用了检测运行时间来判断是否处于调试。

破解方法也很简单,直接修改判断语句就可以。

时间间隔检测法:

大致两种方法:

1.利用CPU计数器:

RDTSC

QueryPerformanceCounter/NtQueryPerformanceCounter

GetTickCount

2.利用系统实际时间:

timeGetTime()

_ftime()

计数器准确度:

RDTSC>NtQueryPerformanceCounter()>GetTickCount()

RDTSC:

CPU对每个时钟周期进行计数,保存到TSC寄存器中.RDTSC是一条汇编指令,用来读取TSC值读入EDX:EAX中.

破解之法:

直接run过这段代码

操作第二个RDTS的值

操做条件分支指令

陷阱标志:

陷阱标志指EFLAGS寄存器的第九个比特位.

单步执行:

TF值为1时,CPU进入单步运行模式.CPU执行一条指令后就触发EXECEPTION_SINGLE_STEP异常,然后标志位自动清0 .

破解之法:

打开OD调试选项忽略该异常.

INT 2D:

原为内核模式触发断点异常的指令,也可以在用户模式下触发异常.但程序调试运行时不会触发异常,只会简单忽略.这种正常模式和调试模式表现出的不同可以很好的运用于反调试.

忽略下条指令的第一个字节:

该反调试技术可以形成较强的代码混淆.

一直运行到断点处:

用F7和F8时,不会停在下一条指令,直到遇到断点.

0xCC探测:

若程序检测到0xCC指令,可以判断处于调试状态.

但这并不完全正确.

API断点:

断点一般设置在API代码的开始部分,只要检测API代码的第一个字节是否为CC就可以判断进程是否处于调试当中.

破解之法:

在APi设置断点时,一般避开第一个字节.

设置硬件断点

比较校验和:

比较特定代码区域的校验和.如果在比较区域设置断点,指令变成0xcc,校验和就和原值不同,从而判断是否处于调试之中.(一般运用CRC32算法)

破解之法:

只要不在计算CRC代码区域设置断点或者修改其中代码,该反调试就会失效.

修改CRC比较语句.

高级反调试技术:

垃圾代码:

向程序中添加大量垃圾代码,而且代码中有真正有用的代码或者运用于其他反调试的代码.

打乱代码对齐:

向代码中插入不必要的代码来降低反汇编的可读性的技术称为打乱代码对齐.

加密/解密:

隐藏代码数据,有效防止调试分析程序.

在程序某段代码中插入解密代码,只有执行这段代码才能解密出其他区域.

反转储技术:加密代码被解密后,会再次被加密.

代码重组:

为了增加跟踪难度,采用了实时组合执行代码的技术手法.

在解码代码中若设置断点,会计算出不同的结果,从而造成解析错误,所以最好在该区域禁止写入初值.

Stolen Bytes(Remove OEP):

该技术将部分源代码转移到压缩器或者保护器创建的内存区域运行.

优点:

转储进程时,一部分OEP代码会被删除,转储的文件无法正常运行反转储技术.

运用该技术的文件再次经过压缩器/保护器压缩后,会给逆向分析人员造成困难.文件脱壳,也不能找到OEP.

API重定向:

程序会将全部或部分主要API代码复制到其他内存区域,然后分析要保护的目标进程的代码,修改调用API代码,从而使自身复制的API代码得以执行.这样,即使在原代码处设断点也没用(支持反转储功能)

Debug Bloker(Self Debugging ):

Debug Bloker时自我创建技术(以子进程形式运行自身进程).自我创建技术中,子进程负责执行实际原代码,父进程负责创建子进程,修改内存(代码/数据),更改EP地址.所以仅调试父进程无法跳到OEP处.但将子进程附加到调试器,这种反调试就会失效.而Debug Bloker技术弥补这一缺点.

Debug Bloker技术优点:

防止代码调试.因子进程运行实际原代码已处于调试当中.原则上无法使用调试器附加操作.

能够控制子进程.在调试器-被调试者关系中,调试器具有很大权限,可以处理被调试进程的异常,控制代码执行流程.