恶意样本分析手册——反调试篇(下)

使用Windows API函数检测调试器是否存在是最简单的反调试技术。Windows操作系统中提供了一些这样的API,应用程序可以通过调用这些API来探测自己是否正在被调试。这些API有些是专门用来探测调试器的存在的。而另外一些API是处于其他目的而设计的,但也可以被改造用来探测调试器的存在。

反调试技术总结

通常,防止恶意代码使用API进行反调试的最简单的方法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的API函数,或者修改这些API函数的返回值,确保恶意代码执行合适的路径。与这些方法相比,较复杂的方法是挂钩这些函数,比如使用rootkit技术。

IsDebuggerPresent

探测调试器是否存在的最简单的API函数是IsDebuggerPresent。它会查询进程环境块(PEB)中的IdDebugged标志,如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。

PEB结构体如下所示:

FindWindow

使用此函数查找目标窗口,如果找到,可以禁用窗口,也可以直接退出程序

函数声明:

参数表:

lpClassName:指向一个以NULL字符结尾的、用来指定类名的字符串或一个可以确定类名字符串的原子。如果这个参数是一个原子,那么它必须是一个在调用此函数前已经通过GlobalAddAtom函数创建好的全局原子。这个原子(一个16bit的值),必须被放置在lpClassName的低位字节中,lpClassName的高位字节置零。

如果该参数为null时,将会寻找任何与lpWindowName参数匹配的窗口

lpWindowName:指向一个以NULL字符结尾的、用来指定窗口名(即窗口标题)的字符串。如果此参数为NULL,则匹配所有窗口名。

枚举窗口

使用EnumWindow函数枚举窗口,并且为每一窗口调用一次回调函数,在回调函数中可以调用GetWindowText获取窗口的标题。与目标窗口名进行比对,如果比对成功,则说明发现调试器。

函数声明:

参数表:

lpEnumFunc:回调函数指针。

lParam:指定一个传递给回调函数的应用程序定义值。

回调函数原型:

参数表:

Hwnd:顶层窗口的句柄。

Lparam:应用程序定义的一个值(即EnumWindows中的lParam)。

参数表:

hWnd:带文本的窗口或控件的句柄。

IpString:指向接收文本的缓冲区指针

nMaxCount:指定要保存在缓冲区内的字符的最大个数,其中包含NULL字符。如果文本超过界限,它就被截断。

枚举进程

枚举进程列表,查看是否有调试器进程

函数声明:

通过获取进程信息为指定的进程、进程使用的堆[HEAP]、模块[MODULE]、线程建立一个快照。

参数表:

dwFlags:用来指定“快照”中需要返回的对象,可以是TH32CS_SNAPPROCESS等

th32ProcessID: 一个进程ID号,用来指定要获取哪一个进程的快照,当获取系统进程列表或获取 当前进程快照时可以设为0

process32First是一个进程获取函数,当我们利用函数CreateToolhelp32Snapshot()获得当前运行进程的快照后,我们可以利用process32First函数来获得第一个进程的句柄。

Process32Next是一个进程获取函数,当我们利用函数CreateToolhelp32Snapshot()获得当前运行进程的快照后,我们可以利用Process32Next函数来获得下一个进程的句柄。

查看父进程是不是Explorer

当我们双击运行应用程序的时候,父进程都是Explorer,如果是通过调试器启动的,父进程就不是Explorer。

通过GetCurrentProcessId()获得当前进程的ID

通过桌面窗口类和名称获得Explorer进程的ID

使用Process32First/Next()函数枚举进程列表,通过PROCESSENTRY32.th32ParentProcessID 获得的当前进程的父进程ID与Explorer的ID进程比对。如果不一样的很可能被调试器附加

函数声明:

找出某个窗口的创建者(线程或进程),返回创建者的标志符。

参数说明:

hWnd:(向函数提供的)被查找窗口的句柄.

lpdwProcessId:进程号的存放地址(变量地址)

使用方法:

检测系统时钟

当进程被调试时,调试器事件处理代码、步过指令等将占用 CPU 循环。如果相邻指令之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试。有如下两种时钟检测来探测调试器存在的方法:

  • 记录执行一段操作前后的时间戳,然后比较这两个时间戳,如果存在滞后,则可以认为存在调试器
  • 记录触发一个异常前后的时间戳。如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。默认情况下,调试器处理异常时需要认为干预,这导致大量延迟。虽然很多调试器允许我们忽略异常,将异常直接返回给程序,但这样操作仍然存在不小的延迟

首先我们可以使用rdtsc指令,它返回至系统重新启动以来的时钟数,并且将其作为一个64位的值存入edx:eax中。恶意代码运行两次rdstc指令,然后比较两次读取之间的差值来判断是否存在调试器。

另外也可以使用函数QueryPerformanceCounter和GetTickCount。同rdstc指令一样,这两个API函数也可以被用来执行一个反调试的时钟检测。为了获取比较的时间差,调用两次函数查询这个计数器,如果两次调用时间话费时间过于长,则可以认为存在调试器。

GetTickCount返回从操作系统启动所经过的毫秒数。调用两次此函数,如果返回值的差值相差反常,就说明在调试状态。

QueryPerformanceCounter和GetTickCount的原理类似,这里以GetTickCount为例说明使用方法:

查看StartupInfo结构

在windows操作系统中,Explorer创建进程的时候会把STARTUPINFO结构中的某些值设为0,非Explorer创建进程的时候会忽略这个结构中的值,所以可以通过查看这个结构中的值是不是为0来判断是否在调试状态。

函数说明:

BeingDebugged,NTGlobalFlag

IsDebuggerPresent() API 检测进程环境块(PEB)中的 BeingDebugged 标志检查这个标志以确定进程是否正在被用户模式的调试器调试。通过fs:[30]可以获得PEB地址。然后通过不同的偏移访问不同的值

通常程序没有被调试时, PEB 另一个成员 NtGlobalFlag(偏移 0x68)值为 0,如果进程被调试通常值为 0x70(代表下述标志被设置):

CheckRemoteDebuggerPresent

这个函数同IsDebuggerPresent函数几乎一致,它用来检测本机器中的一个进程是否运行在调试器中。同时,它也检查PEB结构中的IsDebugged属性。他不仅可以探测进程自身是否被调试,同时可以探测系统其他进程是否被调试。这个函数将一个进程句柄作为参数,检查这个句柄对应的进程是否被调试器附加,同时,CheckRemoteDebuggerPresent也可以通过传递自身进程句柄探测自己是否被调试。

函数声明:

此函数用来确定是否有调试器附加到进程。

参数表:

hProcess:进程句柄

pbDebuggerPresent:指向一个BOOL的变量,如果进程被调试,此变量被赋值为TRUE。

使用方法:

NtQueryInformationProcess

ntdll!NtQueryInformationProcess()有 5 个参数。为了检测调试器的存在,需要将 ProcessInformationclass 参数设为 ProcessDebugPort(7)。NtQueryInformationProcess()检索内核结构 EPROCESS 的 DebugPort 成员,这个成员是系统用来与调试器通信的端口句柄。非 0 的 DebugPort 成员意味着进程正在被用户模式的调试器调试。如果是这样的话, ProcessInformation 将被置为 0xFFFFFFFF ,否则 ProcessInformation将被置为 0。

函数声明:

参数表:

ProcessHandle:进程句柄

ProcessInformationClass:信息类型

ProcessInformation: 缓冲指针

ProcessInformationLength: 以字节为单位的缓冲大小

ReturnLength:写入缓冲的字节数

使用方法:

SetUnhandledExceptionFilter

调试器中步过 INT3 和 INT1 指令的时候,由于调试器通常会处理这些调试中断,所以设置的异常处理例程默认情况下不会被调用, Debugger Interrupts 就利用了这个事实。这样我们可以在异常处理例程中设置标志,通过 INT 指令后如果这些标志没有被设置则意味着进程正在被调试。

函数说明:

设置异常捕获函数.,当异常没有处理的时候,系统就会调用SetUnhandledExceptionFilter所设置异常处理函数.

参数表:

lpTopLevelExceptionFilter :函数指针。当异常发生时,且程序不处于调试模式(在vs或者别的调试器里运行)则首先调用该函数。

使用方法:

SeDebugPrivilege进程权限

默认情况下进程没有 SeDebugPrivilege 权限,调试时,会从调试器继承这个权限,可以通过打开 CSRSS.EXE 进程间接地使用SeDebugPrivilege来判断进程是否被调试。

使用方法:

GuardPages

这个检查是针对 OllyDbg 的,因为它和 OllyDbg 的内存访问/写入断点特性相关。除了硬件断点和软件断点外, OllyDbg 允许设置一个内存访问/写入断点,这种类型的断点是通过页面保护来实现的。简单地说,页面保护提供了当应用程序的某块内存被访问时获得通知这样一个途径。

页面保护是通过 PAGE_GUARD 页面保护修改符来设置的,如果访问的内存地址是受保护页面的一部分,将会产生一个 STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。如果进程被 OllyDbg 调试并且受保护的页面被访问,将不会抛出异常,访问将会被当作内存断点

使用方法:

软件断点

调试器设置断点的基本机制是用软件中断指令INT3.临时替换运行程序中的一条指令,然后当程序运行这条指令时,调用调试异常处理例程,INT 3指令的机器码是0xCC,因此无论何时,使用调试器设置一个断点,它都会插入一个0xCC来修改代码,恶意代码常用的一种反调试技术是在它的代码中查找机器码0xCC来扫描调试器对它代码的INT 3修改。所以可以通过在受保护的代码段和(或) API 函数中扫描字节 0xCC 来识别软件断点,可以搜索在一般地方下的断点,或者是在函数中下的断点

使用方法:

普通断点:

硬件断点

硬件断点是通过设置名为 Dr0 到 Dr7 的调试寄存器来实现的。 Dr0-Dr3 包含至多 4 个断点的地址, Dr6 是个标志,它指示哪个断点被触发了, Dr7 包含了控制 4 个硬件断点诸如启用/禁用或者中断于读/写的标志。

由于调试寄存器无法在 Ring3 下访问,硬件断点的检测需要执行一小段代码。可以利用含有调试寄存器值的 CONTEXT 结构,该结构可以通过传递给异常处理例程的 ContextRecord 参数来访问。

使用方法:

封锁键盘,鼠标输入

BlockInput函数阻塞键盘及鼠标事件到达应用程序。该参数指明函数的目的。如果参数为TRUE,则鼠标和键盘事件将被阻塞。如果参数为FALSE, 则鼠标和键盘事件不被阻塞。

可以在代码中的关键位置调用此函数。

使用方法:

禁用窗口

与BlockInput函数的功能类似,用来禁用窗口

函数说明:

BOOL EnableWindow(HWND hWnd,BOOL bEnable)

hWnd:被允许/禁止的窗口句柄

bEnable: 定义窗口是被允许,还是被禁止。若该参数为TRUE,则窗口被允许。若该参数为FALSE,则窗口被禁止。

Windows API函数。该函数允许/禁止指定的窗口或控件接受鼠标和键盘的输入,当输入被禁止时,窗口不响应鼠标和按键的输入,输入允许时,窗口接受所有的输入。

使用方法:

ThreadHideFromDebugger

NtSetInformationThread()用来设置一个线程的相关信息。把 ThreadInformationClass参数设为 ThreadHideFromDebugger(11H)可以禁止线程产生调试事件。

函数声明:

ThreadHideFromDebugger 内部设置内核结构 ETHREAD 的 HideThreadFromDebugger 成员。一旦这个成员设置以后,主要用来向调试器发送事件的内核函数_DbgkpSendApiMessage()将不再被调用

使用方法:

OutputDebugString

OutputDebugString 函数用于向调试器发送一个格式化的字符串, Ollydbg 会在底端显示相应的信息。 OllyDbg 存在格式化字符串溢出漏洞,非常严重,轻则崩溃,重则执行任意代码。这个漏洞是由于 Ollydbg 对传递给 kernel32!OutputDebugString()的字符串参数过滤不严导致的,它只对参数进行那个长度检查,只接受 255 个字节,但没对参数进行检查,所以导致缓冲区溢出。

使用方法

使用TLS回调

TLS回调被用来在程序入口点执行之前运行代码,因此这些代码可以在调试器中秘密执行。TLS是windows的一个存储类,其中数据对象不是一个自动的堆栈变量,而是代码中运行的每个线程的一个本地变量。大致而言,TLS允许每个线程维护一个TLS声明的专有变量。在应用程序实现TLS的情况下,可执行程序的PE头部会包含一个.tls段。TLS提供了初始化和终止TLS数据对象的回调函数。windows系统在执行程序正常的入口点之前运行这些回调函数。

可以使用PEview查看应用程序的.tls段,可以发现TLS回调函数。通常情况下,正常程序不适用.tls段,如果看到了程序的.tls段,就可以怀疑它使用了反调试技术。

使用IDA Pro可以很容易的分析TLS回调函数。一旦IDA Pro完成对应用程序的分析,可以通过Ctrl+E看到二进制的入口点,该组合键的作用是显示应用程序的所有入口点,其中包含TLS回调。

上篇:http://blog.nsfocus.net/anti-test-articles1/

如果您需要了解更多内容,可以
加入QQ群:570982169
直接询问:010-68438880

 

发表评论