windows下的反调试
2023-2-23 23:41:56 Author: MicroPest(查看原文) 阅读量:7 收藏

       明明VirtualProtect,也hook成功了,但用另一个程序调用此函数时,发现它处于原始状态(未hook),实在是摸不着头脑,找不到原因,一直在找资料,发现了这个,非常不错,特推荐给大家。文章很长,内容很多,收藏起来,慢慢看。

一、断点原理

断点其实就是和异常有非常大的关联,首先针对异常的机制,如果发生了异常会先看有没有调试器调试器有没有接受该异常如果有就停在哪里等调试器处理,如果没有调试器就用SEH来处理。

也就是说根据异常以及接受是什么异常导致了有非常多类型的断点,那么接下来大概看一下常用的一些吧。

OD中的F2断点

OD中的按F2可以产生一个断点,根据断点的原理可以猜到,这里肯定是用了什么手脚产生了一个异常,然后调试器捕获了才停到了这里。

用OD随便打开一个进程,然后随便对一条指令打一个F2断点

然后打开CE来查看该地址的内容

这里就突然发现,针对于这里打断点的地址的值其实并不是od里面的那样的,是被改动了的,第一个字节的内容是被CC填充了的,在触发CC指令时就会触发异常,然后这里正好捕获。

CC硬编码的异常就要int3异常,在od里面随便输入一个Int3指令可以看到对于的硬编码就是CC,所以这个又叫int3断点

内存断点

修改内存的一个属性来实现断点,比如修改一个内存属性为不可写属性,但是却往里面写内容,那么就会引发异常,调试器再有接受这种异常就会变成断点处理了

硬件断点

在寄存器中,有一些寄存器用于调试的时候使用,把这些寄存器称为调试寄存器。

调试寄存器一共有八个,名字分别是从Dr0到Dr7。

对于Dr0到Dr3这四个调试寄存器的作用的存放中断的地址。

对于Dr4,Dr5这两个调试寄存器一般不使用

Dr6,Dr7这两个调试寄存器的作业是用来记录在Dr0-Dr3中下断的地址属性。比如:对于401000是硬件断点的读还是写还是执行,是字节还是字,还是双字

二、Windows异常-SEH

概念:

SEH:Structured Exception Handling

SEH是Windows默认的异常处理机制

如何使用

在代码中使用

__try

__except()//结构类型的语句

__except()小括号里面填写表达式,表达式为真的时候执行里面的内容

__try里面包含的是可能触发异常的语句,except里面包含的是出现了异常后执行的操作。

__except()括号中表达式的取值范围:
1:处理异常
0:不处理异常交给下一个异常节点去处理
-1:继续执行也就是继续EIP处理执行,但是这里又有异常,所以这里就会一直卡在这里

例子:

int main()
{
__try
{
cout<<"hello,world"<<endl;
}
__except(1)
{
cout<<"异常"<<endl;
}
return 0;
}

异常的作用

1 便于查找错误

2 可以用在反调试里面

异常处理机制

当我们在非调试状态下运行一个程序,程序如果触发了异常,会先判断是否有异常处理器,如果存在则跳转到异常处理函数去执行,如果不存在则退出程序

如果程序处于被调试状态,触发异常时,操作系统会先把异常抛给调试进程,也就是让调试器来处理异常。可以看到的现象就是触发了异常后,程序会暂停下来,也就是断下来,也就是断点的原理。当异常抛给调试器后,调试器可以选择:

1 修改触发异常的代码继续执行(程序会停在触发异常的代码处,导致异常的代码无法执行)

2 忽略异常交给SEH执行

也就是说Windows发生异常后的处理顺序为:1、调试器处理。2、SEH处理 3、崩溃

剖析SEH

SEH结构:

typedef struct _EXCEPTION_REGISTRATION_RECORD {
  struct _EXCEPTION_REGISTRATION_RECORD *Next;
  PEXCEPTION_ROUTINE Handler;//异常处理器,异常处理函数
} EXCEPTION_REGISTRATION_RECORD;

SEH在程序中实际上是以链表形式存在,每一个next都指向下一个SEH,这个链表也叫SEH链(如图):

每次添加一个异常就会添加该异常到异常链的头结点的位置

观察SEH是如何生成和使用的

生成两个程序观察,两个程序的区别就是一个有异常SEH,一个没有

//有异常的程序代码
#include<Windows.h>
#include<iostream>
int main()
{
__try
{
char* str = NULL;
str[0] = 'a';
}
__except(1)
{
printf("触发异常了\n");
}
printf("Sna1lGo\n");
return 0;
}
//没有异常的程序代码
#include<Windows.h>
#include<iostream>
int main()
{
char* str = NULL;
str[0] = 'a';
printf("Sna1lGo\n");
return 0;
}

分别生成后,都用od打开调试比对

  然后都进入main函数

 重点查看SEH的代码:

这四条指令就是SEH的关键代码,这一系列操作相当于创建了一个异常的结构体,然后添加到异常链的表头里面。

分析为什么是这样:

首先,大概画一个堆栈图:

先存进来了一个0x01293609,然后调用了fs:[0]这个东西,这个东西有点眼熟之前的TEP-PEB查找核心模块的时候有用过,但是这里,可以将fs:[0]直接理解为SEH链的表头。

这里取出来fs:[0]给eax后又把eax入栈,然后将fs:[0]赋值为esp

 可以理解为在栈中创建了一个SEH变量,然后第一个字段存放了函数地址也就是0x01293690,next字段指向了fs:[0]也就是SEH链的第一个节点,然后再把fs:[0]的值修改为该节点的首地址,也就是让fs:[0]指向新的节点。(可能你的代码有点不一样,但是稍微分析下,对于异常处理这里也是一样的)

下面进入异常处理函数的地址查看(这里是0x01293690)

 然后再运行会暂停下来,因为出现了异常,异常优先给调试器处理,但是调试器也是可以选择处理异常的:

在od的选项中选择调试选项,再选择异常:

就可以设置要捕获的异常类型了,选择忽略掉一些异常后再遇到就不会中断下来了。

也可以选择插件的StrongOD:

 在取消掉Skip Some Exceptions之后,OD就会捕获所有异常了

SEH链存放的位置

 经过上面的分析可以知道了,SEH存放的位置在fs:[0]这里

操作系统如何使用SEH

创建异常后,操作系统会自动把异常添加到头结点,然后把头指针指向新的异常节点

Windows中有一个机制叫异常处理机制,当出现异常的时候操作系统会先把异常抛给调试器并停止在该指令位置,如果没有调试器就会抛给SEH来处理,SEH实现的原理是内部是一个SEH链表包含了一系列异常处理函数,当有异常类添加进来的时候操作系统会把异常添加到SEH链表里面,然后当异常出现的时候遍历异常类看看有没有对应的异常处理,如果该异常没有异常处理则直接程序崩溃。

三、自定义异常处理器并添加调试器检测

异常处理机制:

出现异常,首先看有没有调试器有的话交给调试器处理,没有就传一堆信息给异常函数来处理

异常处理函数原型:

EXCEPTION_DISPOSITION myExceptHandler(
struct _EXCEPTION_RECORD* ExceptionRecord,
PVOID EstablisherFrame,
PCONTEXT pcontext,
PVOID DispatcherContext
)
{
}

返回值

该函数的类型返回值是一个枚举类型

typedef enum _EXCEPTION_DISPOSITION
{
  ExceptionContinueExecution,//继续执行
  ExceptionContinueSearch, //搜索下一个异常处理器
  ExceptionNestedException,
  ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

参数:

    struct _EXCEPTION_RECORD* ExceptionRecord,//异常的记录
PVOID EstablisherFrame,
PCONTEXT pcontext,
PVOID DispatcherContext

重点关注pcontext参数

pcontext

查看PCONTEXT定义:

typedef CONTEXT *PCONTEXT;

继续查看CONTEXT定义:

typedef struct DECLSPEC_NOINITALL _CONTEXT {

  //
  // The flags values within this flag control the contents of
  // a CONTEXT record.
  //
  // If the context record is used as an input parameter, then
  // for each portion of the context record controlled by a flag
  // whose value is set, it is assumed that that portion of the
  // context record contains valid context. If the context record
  // is being used to modify a threads context, then only that
  // portion of the threads context will be modified.
  //
  // If the context record is used as an IN OUT parameter to capture
  // the context of a thread, then only those portions of the thread's
  // context corresponding to set flags will be returned.
  //
  // The context record is never used as an OUT only parameter.
  //

  DWORD ContextFlags;

  //
  // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
  // set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
  // included in CONTEXT_FULL.
  //

  DWORD   Dr0;
  DWORD   Dr1;
  DWORD   Dr2;
  DWORD   Dr3;
  DWORD   Dr6;
  DWORD   Dr7;

  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
  //

  FLOATING_SAVE_AREA FloatSave;

  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_SEGMENTS.
  //

  DWORD   SegGs;
  DWORD   SegFs;
  DWORD   SegEs;
  DWORD   SegDs;

  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_INTEGER.
  //

  DWORD   Edi;
  DWORD   Esi;
  DWORD   Ebx;
  DWORD   Edx;
  DWORD   Ecx;
  DWORD   Eax;

  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_CONTROL.
  //

  DWORD   Ebp;
  DWORD   Eip;
  DWORD   SegCs;             // MUST BE SANITIZED
  DWORD   EFlags;             // MUST BE SANITIZED
  DWORD   Esp;
  DWORD   SegSs;

  //
  // This section is specified/returned if the ContextFlags word
  // contains the flag CONTEXT_EXTENDED_REGISTERS.
  // The format and contexts are processor specific
  //

  BYTE   ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

这里的注释表明了,该pcontext参数保存的是每个寄存器的内容,包括各种各样的寄存器。

例子:

#include<Windows.h>
#include<iostream>
EXCEPTION_DISPOSITION myExceptHandler(
struct _EXCEPTION_RECORD* ExceptionRecord,
PVOID EstablisherFrame,
PCONTEXT pcontext,
PVOID DispatcherContext
)
{

MessageBoxA(0, "出现异常了", "异常报错", MB_OK);
return ExceptionContinueExecution;
}
int main()
{
DWORD execptionFuncAddr = (DWORD)myExceptHandler;//异常处理的函数地址
__asm
{
push execptionFuncAddr;
mov eax, fs: [0] ;
push eax;
mov fs : [0] , esp;
//添加异常,并且把异常放到异常链里面,并且把异常的头指针指向新的头结点
}
char* str = NULL;
str[0] = 'a';
printf("Sna1lGo\n");
system("pause");
return 0;
}

这个程序按理说,我们是手动把异常处理函数添加进SEH链了,然后也触发异常了,但是程序却跑不起来。原因是VS2019的有一个定义没有关闭

 把这里的3关闭掉就好了。

然后再调用就可以了。

分析一下例子的流程:

操作系统在执行到有问题的语句的时候,就会在发生异常的位置的一些环境保存下来,然后传给异常处理的回调函数,再处理完之后再回到异常的位置继续执行,但是我们这个例子的异常处理器里面并没有解决这个异常,而只是弹出一个框而已,所以就会一直重复该操作。

例子修改

通过修改EIP,来跳过有异常的代码位置:在异常处理函数里面添加指令使得跳过使程序发生异常的指令,这里有一个str[0] = 'a'才导致的异常,通过od反调试可以知道str[0]='a'这条指令在硬编码中占4个字节,所以只需要将eip的地址+4就好了,跳过该有异常的指令就好了。

#include<Windows.h>
#include<iostream>
EXCEPTION_DISPOSITION myExceptHandler(
struct _EXCEPTION_RECORD* ExceptionRecord,
PVOID EstablisherFrame,
PCONTEXT pcontext,
PVOID DispatcherContext
)
{

MessageBoxA(0, "出现异常了", "异常报错", MB_OK);
pcontext->Eip += 4;
return ExceptionContinueExecution;
}
int main()
{
DWORD execptionFuncAddr = (DWORD)myExceptHandler;//异常处理的函数地址
__asm
{
push execptionFuncAddr;
mov eax, fs: [0] ;
push eax;
mov fs : [0] , esp;
//添加异常,并且把异常放到异常链里面,并且把异常的头指针指向新的头结点
}
char* str = NULL;
str[0] = 'a';
printf("Sna1lGo\n");
system("pause");
return 0;
}

这样就好了,异常处理函数里面跳过了异常的指令,并且返回值是ExceptionContinueExecution,表示继续执行

添加调试器检测处理

因为调试器添加断点的原理就是通过异常处理,如果我们在异常处理里面添加一个验证,来验证有没有调试器在调试该程序的话,那么就可以防止被调试了。

在teb-peb核心模块中的peb进程环境块里面,具体可以查看该博客:

https://www.cnblogs.com/Sna1lGo/p/14590794.html

在peb进程核心模块里面,有一个字段是专门用来表示是否被调试器所调试,如果为1就表示1,如果为0就表示不是。

#include<Windows.h>
#include<iostream>
EXCEPTION_DISPOSITION myExceptHandler(
struct _EXCEPTION_RECORD* ExceptionRecord,
PVOID EstablisherFrame,
PCONTEXT pcontext,
PVOID DispatcherContext
)
{

MessageBoxA(0, "出现异常了", "异常报错", MB_OK);

DWORD IsDebugerFlag = 0;
__asm
{
mov eax, fs: [0x18] ;//teb模块
mov eax, [eax + 0x30];//peb模块
movzx eax, byte ptr[eax + 2];//这里有一个标志位表示是否在被调试
mov IsDebugerFlag, eax;
}
if (IsDebugerFlag)
{
MessageBoxA(0, "检查到调试器", "警告", MB_OK);
}
exit(0);

pcontext->Eip = pcontext->Eip+ 4;
return ExceptionContinueExecution;
}
int main()
{
DWORD execptionFuncAddr = (DWORD)myExceptHandler;//异常处理的函数地址
__asm
{
push execptionFuncAddr;
mov eax, fs: [0] ;
push eax;
mov fs : [0] , esp;
//添加异常,并且把异常放到异常链里面,并且把异常的头指针指向新的头结点
}
char* str = NULL;
str[0] = 'a';
printf("Sna1lGo\n");
system("pause");
return 0;
}

这样处理之后,如果该程序再被调试就会报错并退出

 总结

异常处理机制很重要,可以通过利用该机制来限制程序被别人调试,例子采用的是触发一个调试器捕获不了的异常,然后再在异常处理函数里面验证是否被调试,如果被调试就退出这样来实现。

四、VEH

VEH:向量化异常(vectored exception handle)

VEH和SEH的区别

SEH是基于线程的,而VEH是基于进程的。

因为很清楚的可以看到SEH的数据结构是保存在栈空间的,直接在函数体内用push来处理,而VEH是以双链表的形式保存在堆里面。

优先顺序:调试器 > VEH > SEH

添加VEH异常

VEH有提供的API可以直接拿来添加VEH

AddVectoredExceptionHandler(ULONG First,//1表示添加到链表头部 0表示添加到链表尾部
PVECTORED_EXCEPTION_HANDLER Handler//异常处理器的地址
  );

定义异常处理程序


LONG PvectoredExceptionHandler(
_EXCEPTION_POINTERS *ExceptionInfo
)
{...}

使用该函数定义,可以得到基于VEH的异常处理程序的函数原型

异常处理程序的参数

typedef struct _EXCEPTION_POINTERS {
  PEXCEPTION_RECORD ExceptionRecord;//记录着异常的信息
  PCONTEXT ContextRecord; //PCONSTEXT表示寄存器的内容
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

返回值

To return control to the point at which the exception occurred, return EXCEPTION_CONTINUE_EXECUTION (0xffffffff). To continue the handler search, return EXCEPTION_CONTINUE_SEARCH (0x0).

有两种情况,一种是返回EXCEPTION_CONTINUE_EXECUTION ,另一种是返回EXCEPTION_CONTINUE_SEARCH。

EXCEPTION_CONTINUE_EXECUTION :表示继续执行

EXCEPTION_CONTINUE_SEARCH:表示查找下一个VEH来处理

#include<Windows.h>
#include<iostream>
EXCEPTION_DISPOSITION myExceptHandler(
struct _EXCEPTION_RECORD* ExceptionRecord,
PVOID EstablisherFrame,
PCONTEXT pcontext,
PVOID DispatcherContext
)
{
MessageBoxA(0, "SEH处理了异常", "异常报错", MB_OK);
pcontext->Eip = pcontext->Eip+ 4;
return ExceptionContinueExecution;
}

LONG WINAPI PvectoredExceptionHandler(
_EXCEPTION_POINTERS* ExceptionInfo
)
{
MessageBoxA(0, "VEH处理了异常", "异常处理", MB_OK);
ExceptionInfo->ContextRecord->Eip += 4;
return EXCEPTION_CONTINUE_EXECUTION;
}
int main()
{
AddVectoredExceptionHandler(1, PvectoredExceptionHandler);
DWORD execptionFuncAddr = (DWORD)myExceptHandler;//异常处理的函数地址
__asm
{
push execptionFuncAddr;
mov eax, fs: [0] ;
push eax;
mov fs : [0] , esp;
//添加异常,并且把异常放到异常链里面,并且把异常的头指针指向新的头结点
}
char* str = NULL;
str[0] = 'a';
printf("Sna1lGo\n");
system("pause");
return 0;
}#include<Windows.h>
#include<iostream>
EXCEPTION_DISPOSITION myExceptHandler(
struct _EXCEPTION_RECORD* ExceptionRecord,
PVOID EstablisherFrame,
PCONTEXT pcontext,
PVOID DispatcherContext
)
{
MessageBoxA(0, "SEH处理了异常", "异常报错", MB_OK);
pcontext->Eip = pcontext->Eip+ 4;
return ExceptionContinueExecution;
}

LONG WINAPI PvectoredExceptionHandler(
_EXCEPTION_POINTERS* ExceptionInfo
)
{
MessageBoxA(0, "VEH处理了异常", "异常处理", MB_OK);
ExceptionInfo->ContextRecord->Eip += 4;
return EXCEPTION_CONTINUE_EXECUTION;
}
int main()
{
AddVectoredExceptionHandler(1, PvectoredExceptionHandler);
DWORD execptionFuncAddr = (DWORD)myExceptHandler;//异常处理的函数地址
__asm
{
push execptionFuncAddr;
mov eax, fs: [0] ;
push eax;
mov fs : [0] , esp;
//添加异常,并且把异常放到异常链里面,并且把异常的头指针指向新的头结点
}
char* str = NULL;
str[0] = 'a';
printf("Sna1lGo\n");
system("pause");
return 0;
}

VEH是进程的异常处理,SEH是线程的异常处理,SEH可以直接用__try 和except语句使用,也可以直接创建SEH的处理函数和调用SHE来实现内部逻辑结构,VEH有提供的API,只需要创建一个VEH然后再创建一个VEH的异常处理函数就好了

五、异常过滤器

首先什么是异常:很简答就是CPU遇到了无法执行的指令。

异常处理的流程:

CPU->操作系统->调试器(如果有)->VEH->SEH->异常过滤器(SetUnhandleExceptionFilter)

异常过滤器:

https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter 这里微软的官方文档url地址。

#include<Windows.h>
#include<iostream>
using namespace std;

LONG WINAPI myExceptionFilter(_EXCEPTION_POINTERS* ExceptionInfo)
{
MessageBoxA(0, 0, 0, 0);

ExceptionInfo->ContextRecord->Eip += 3;
return EXCEPTION_CONTINUE_EXECUTION;
}

int main()
{

SetUnhandledExceptionFilter(myExceptionFilter);
__asm
{
mov eax,0
mov [eax],1
}
MessageBoxA(NULL, NULL, "跳过了异常代码", MB_OK);
system("pause");
return 0;
}

这段代码就是添加了一个异常过滤器。

    __asm
{
mov eax,0
mov [eax],1
}
这一段就是故意触发一个异常来跳转到自己写的异常过滤器函数里面
LONG WINAPI myExceptionFilter(_EXCEPTION_POINTERS* ExceptionInfo)
{
MessageBoxA(0, 0, 0, 0);

ExceptionInfo->ContextRecord->Eip += 3;
return EXCEPTION_CONTINUE_EXECUTION;
}
这一段就是微软提供的异常过滤器的函数格式,参数ExceptionInfo是触发异常的指令的当时环境,也就是寄存器啊各种类型
然后返回值也很重要返回值可以标识是继续执行异常还是跳过异常还是什么样。等等。

有关异常处理的机制经常被用到反调试里面,因为它故意触发异常然后写异常处理来检测是否有调试器。

六、开始反调试

调试器的工作流程

调试器调试分两种情况,一种是附加进程,一种是通过可执行程序来创建进程调试。

创建进程:

通过CreateProcess并设置为DEBUG_PROCESS模式来启动。

BOOL CreateProcessA(
LPCSTR               lpApplicationName,
LPSTR                 lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL                 bInheritHandles,
DWORD                 dwCreationFlags,
LPVOID               lpEnvironment,
LPCSTR               lpCurrentDirectory,
LPSTARTUPINFOA       lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);

附加进程:

通过调用DebugActiveProcess函数来附加进程。

在进程被调试时,进程执行的一些操作会以事件的方式通知给调试器。。

当有事件需要通知调试器时,操作系统会挂起进程所有线程,然后把事件发送给调试器。

调试器通过WaitForDebugEvent来等待事件。

BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent, //操作系统传递给调试器的事件
DWORD         dwMilliseconds //等待时间
);

程序被调试的特征

调试程序时,调试器会对被调试程序的程序设置标志。

标志描述位置函数
BeginDebugged被创建/附加进程都会被设置为1PEB+02偏移处IsDebuggerPresent()
NtGlobalFlag创建进程会被设置为0x70,但是以附加进程设置时该值不会发生变化PEB+0x68处没有API
ProcessDebugPort程序处于调试状态下可以获取调试端口,通过该端口也可以判断是否被调试。checkRemoteDebugPresent

代码实现简单的反调试:

#include<Windows.h>
#include<iostream>
using namespace std;

void testBeginDebugged()
{
if (IsDebuggerPresent())
{
MessageBoxA(0, "BeginDebugged验证失败,程序被调试", 0, 0);
}
else
{
MessageBoxA(0, "BeginDebugged验证正常", 0, 0);
}
}
void testNtGlobalFlag()
{
DWORD IsDebug = 1;
__asm
{
push eax
mov eax,fs:[0x30]
mov eax,[eax+0x68]
mov IsDebug,eax
pop eax
}
if (IsDebug == 0x70)
{
MessageBoxA(0, "NtGlobalFlag验证失败,程序被调试", 0, 0);
}
else
{
MessageBoxA(0, "NtGlobalFlag验证正常", 0, 0);
}
}
void testProcessDebugPort()
{
BOOL IsDebug = FALSE;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &IsDebug);
if(IsDebug == TRUE)
{
MessageBoxA(0, "ProcessDebugPort验证失败,程序被调试", 0, 0);
}
else
{
MessageBoxA(0, "ProcessDebugPort验证正常,程序未被调试", 0, 0);

}
}

int main()
{
cout << "Welcome" << endl;
testBeginDebugged();
testNtGlobalFlag();
testProcessDebugPort();

return 0;
}

通过xdbg反汇编来分析下:

testBeginDebugged()

 这个是我们自己写的void testBeginDebugged()函数,然后通过该函数查看IsDebuggerPresent函数:

就很明显的可以看到了这个就是一个简答的赋值语句,将fs:[30]也就是PEB偏移0x2的内容作为返回值赋值给eax。

testNtGlobalFlag()

 这里就很明显了,因为本来就是我们自己写的汇编代码,哈哈。

testProcessDebugPort()

这个函数跟我们源代码比较一下可以看到有一个比较重要的函数CheckRemoteDebuggerPresent:

通过这样一层一层解析下来,其实也还是很明确了。它最后是调用了一个比较关键的NtQueryInformationProcess查询进行信息的一个API,这个是NtDll里面的:

七、深入NtQueryInformationProcess

ProcessDebugPort这个标志位里面涉及到的东西比较复杂,所以要展开来讲。

CheckRemoteDebuggerPresent():

CheckRemoteDebuggerPresent function (debugapi.h) - Win32 apps | Microsoft Docs 该API的微软官方文档。

主要就是调用这个函数如果程序在被调试就会返回非零值,如果没有被调试就会返回0。

但是前面我们也解析了,其实CheckRemoteDebuggerPresent内部调用的是NtQueryInformationProcess函数来处理的。

两个函数的共同本质:如果程序处于调试状态,这Nt函数会查询调试端口并返回,Check函数会返回一个bool值。

BOOL CheckRemoteDebuggerPresent(
HANDLE hProcess,
PBOOL pbDebuggerPresent
);
__kernel_entry NTSTATUS NtQueryInformationProcess(
HANDLE           ProcessHandle,//进程句柄
PROCESSINFOCLASS ProcessInformationClass,//进程信息类型,传不同的值表示查进程不同的信息
PVOID           ProcessInformation,//输出参数 存储进行信息的缓冲区
ULONG           ProcessInformationLength,//缓冲区大小
PULONG           ReturnLength//实际大小
);

进程信息类型:(这里面有相当多的消息)pinvoke.net: PROCESSINFOCLASS (ntdll)

//其中比较常用有文档记录的
typedef enum _PROCESSINFOCLASS {
  ProcessDebugPort = 7,//调试端口
  ProcessDebugObjectHandle = 30,//获取调试对象句柄,句柄为空表示未调试
ProcessDebugFlags = 31 //检测调试标志位为0表示处于调试状态
} PROCESSINFOCLASS;

代码实现简单的NtQueryInformationProcess函数调用

首先这里需要从DLL里面获得函数,这里由于PROCESSINFOCLASS是一个枚举值,而且值都是0x00这样类型的,所以这里我们可以直接使用DWORD来取代,更加方便:

typedef NTSTATUS(NTAPI *_NtQueryInformationProcess)(
HANDLE           ProcessHandle,
DWORD ProcessInformationClass,
PVOID           ProcessInformation,
ULONG           ProcessInformationLength,
PULONG           ReturnLength
);
//头文件
#pragma once
#include<Windows.h>
#include<iostream>
typedef NTSTATUS(NTAPI *_NtQueryInformationProcess)(
HANDLE           ProcessHandle,
DWORD ProcessInformationClass,
PVOID           ProcessInformation,
ULONG           ProcessInformationLength,
PULONG           ReturnLength
);
using namespace std;

void testBeginDebugged();
void testNtGlobalFlag();
void testProcessDebugPort();
void testNtQueryInformationProcess();
void testNtPort(_NtQueryInformationProcess NtQueryInformationProcess);
void testNtObjectHandle(_NtQueryInformationProcess NtQueryInformationProcess);
void testFlag(_NtQueryInformationProcess NtQueryInformationProcess);

//源文件
#include"TestC++.h"

void testBeginDebugged()
{
if (IsDebuggerPresent())
{
cout << "BeginDebugged验证失败,程序被调试" << endl;
}
else
{
cout << "BeginDebugged验证正常" << endl;
}
}
void testNtGlobalFlag()
{
DWORD IsDebug = 1;
__asm
{
push eax
mov eax, fs: [0x30]
mov eax, [eax + 0x68]
mov IsDebug, eax
pop eax
}
if (IsDebug == 0x70)
{
cout << "NtGlobalFlag验证失败,程序被调试" << endl;
}
else
{
cout << "NtGlobalFlag验证正常" << endl;
}
}
void testProcessDebugPort()
{
BOOL IsDebug = FALSE;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &IsDebug);
if(IsDebug == TRUE)
{
cout << "ProcessDebugPort验证失败,程序被调试" << endl;
}
else
{
cout << "ProcessDebugPort验证正常,程序未被调试" << endl;
}
}
void testNtQueryInformationProcess()
{
HMODULE hDll = LoadLibraryW(L"Ntdll.dll");
_NtQueryInformationProcess NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress(hDll, "NtQueryInformationProcess");
testNtPort(NtQueryInformationProcess);
testNtObjectHandle(NtQueryInformationProcess);
testFlag(NtQueryInformationProcess);
}
void testNtPort(_NtQueryInformationProcess NtQueryInformationProcess)
{
HANDLE hProcess = GetCurrentProcess();
DWORD DebugPort;
NtQueryInformationProcess(hProcess,7,&DebugPort,sizeof(DWORD),NULL);
if (DebugPort != 0)
{
cout << "DebugPort验证失败,程序正在被调试" << endl;
}
else
{
cout << "DebugPort验证成功" << endl;
}
}
void testNtObjectHandle(_NtQueryInformationProcess NtQueryInformationProcess)
{
HANDLE hProcess = GetCurrentProcess();
HANDLE ObjectHandle;
NtQueryInformationProcess(hProcess, 30, &ObjectHandle, sizeof(ObjectHandle), NULL);
if (ObjectHandle != NULL)
{
cout << "调试端口验证失败,程序正在被调试" << endl;
}
else
{
cout << "调试端口验证成功" << endl;
}
}
void testFlag(_NtQueryInformationProcess NtQueryInformationProcess)
{
HANDLE hProcess = GetCurrentProcess();
BOOL Flags;
NtQueryInformationProcess(hProcess, 31, &Flags, sizeof(Flags), NULL);
if (Flags != 1)
{
cout << "调试端口验证失败,程序正在被调试" << endl;
}
else
{
cout << "调试端口验证成功" << endl;
}
}

int main()
{
printf("%s\n", "Welcome");
cout << "Welcome" << endl;
testBeginDebugged();
testNtGlobalFlag();
testProcessDebugPort();
testNtQueryInformationProcess();

return 0;
}

采用OD查看:

采用VS调试器查看:

采用直接运行:

这个我一直想不通,按理来说应该是都正常啊,然后我想了一下有没有可能是Debug模式的问题,所以我打开了release版本的:

运行Release版本的exe:

八、反调试手段

反调试的方法有非常非常多,这里介绍一些比较常见的。

通过CloseHandle()

通过CloseHandle来试图关闭一个不存在的句柄,如果程序处于调试状态就会引发异常,否则没有任何反应。

这种机制在User下看起来就是调用一个CloseHandle,但是其实它的内部逻辑在内核层次下是实现了的。 这个大家可以试一下,反正我试出来是可行的。

void testCloseHandle()
{
CloseHandle((HANDLE)0x1234);
}

所以我们就可以采用处理异常的办法来处理:

void testCloseHandle()
{
__try
{
CloseHandle((HANDLE)0x1234);
}
__except(1)
{
cout << "CloseHandle验证失败,程序正在被调试" << endl;
return;
}
cout << "CloseHandle验证成功" << endl;
}

设置线程信息分离调试器

关键函数:ZwSetInformationThread()

NTSYSAPI NTSTATUS ZwSetInformationThread(
HANDLE         ThreadHandle,
THREADINFOCLASS ThreadInformationClass,
PVOID           ThreadInformation,
ULONG           ThreadInformationLength
);//有点类似NtQueryInformation

同样的THREADINFOCLASS这个也是一个枚举类型,和上一章非常相识:

https://geoffchappell.com/studies/windows/km/ntoskrnl/api/ps/psquery/class.htm这个网站里面有这个枚举变量的值的详细介绍。

这里我们只需要用到0x11 也就是十进制的17这个值:0x11:ThreadHideFromDebugger,这个枚举量的意思就是将线程和调试器隔离。

同样它也是ntdll.dll里面的函数:

void testZwSetInformationThread()
{
HMODULE hModule = LoadLibraryA("Ntdll.dll");
_ZwSetInformationThread ZwSetInformationThread = (_ZwSetInformationThread)GetProcAddress(hModule, "ZwSetInformationThread");
ZwSetInformationThread(GetCurrentThread(),(THREAD_INFORMATION_CLASS)0x11, NULL, NULL);
}

这样调用了该函数之后就直接崩溃了。

九、硬件断点反调试

首先需要明白什么是硬件断点,硬件断点其实是通过一个调试寄存器来实现的,这个调试寄存器是CPU上的东西,就是前面截图的这个东西,叫做Debug Registers,在intel手册卷3 17章第二节里面)。

DR0-DR3为设置断点的地址,DR4和DR5为保留内容。DR6为调试异常产生后显示的一些信息,DR7保存了断点是否启用、断点类型和长度等信息。

重点在DR0-DR3和DR7这四个里面。

DR0-DR3中存放的是断点的地址。

然后DR7根据各各字段的不同意义不同:

字段意义
L0-L3对于DR0-DR3存放的地址断点是否有效,并且是一个局部断点
G0-G3对应DR0-DR3存放的地址是否有效,但是是一个全局断点(在Windows中用不了。)
LEN0-LEN3对应DR0-DR3断点的长度。00表示1字节,01表示二字节,11表示四字节
RW0-RW3对应DR0-DR3断点的类型。00表示执行断点,01表示写入断点,11表示读写断点。

获取硬点断点信息:

这里我们要使用到一个API:

BOOL GetThreadContext(
 HANDLE    hThread,
 LPCONTEXT lpContext
);

用这个API来获取当前线程的环境,肯定有人要问为什么是线程,不是进程,不是啥啥啥的。因为在操作系统中,线程才是真正执行代码流程的东西,进程只是一个分配资源的概念,而每个代码执行的时候,自己的寄存器或多或少都会受到影响,简单来说就是线程才能实实在在执行代码,如果用进程的话每个线程都会受到影响,而且进程也没有这个GetProcessContext得到进行环境寄存器的API。

参数:

参数意义
hThread线程句柄
lpContext指向CONTEXT结构体的指针,需要注意的是context是一个输出参数就是通过这个API会修改CONTEXT结构体里的值,但是需要设置CONTEXT里的ContextFlags值来限制要获得什么内容,如果不设置则不能获得内容。

所以如果我们要获取Debug Register的内容,这里需要设置为CONTEXT_DEBUG_REGISTERS了。

简单实现硬件断点反调试:

#include<Windows.h>
#include<iostream>
using namespace std;

int main()
{

CONTEXT TestContext{ 0 };
TestContext.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &TestContext);
if (TestContext.Dr0 != 0 || TestContext.Dr1 != 0 || TestContext.Dr2 != 0 || TestContext.Dr3 != 0)
{
MessageBoxA(NULL, "硬件断点检测成功,程序正在被调试!", "硬件断点检测", MB_OK);
}
else
{
MessageBoxA(NULL, "硬件断点检测失败", "硬件断点检测", MB_OK);
}

system("pause");
return 0;
}

#pragma once
#include<Windows.h>
#include<iostream>
using namespace std;
void MyTestExceptionFilter();
LONG WINAPI MyUnhandledExceptionFilter(_EXCEPTION_POINTERS* ExceptionInfo);

最后查看通过OD打下硬件断点的情况,和直接运行的情况:

采用异常实现硬件断点反调试:

如果说直接用前面那种来实现反调试就太辣鸡了,很容易就被识破,但是如果我们添加一个异常呢,这样就会好很多,因为异常要通过一些寄存器,一些不是上面那种直接调用API的办法,会麻烦一点。

采用异常过滤器来处理:

关于异常过滤器可以查看直接的博客:反调试——异常过滤器 - Sna1lGo - 博客园 (cnblogs.com)

这里我直接上代码了:

#include"Anti-debugging.h"
LONG WINAPI MyUnhandledExceptionFilter(_EXCEPTION_POINTERS* ExceptionInfo)
{
||
ExceptionInfo->ContextRecord->Dr3 != 0)
{
cout << "Fuck" << endl;
cout << "异常过滤器验证成功,程序正在被调试" << endl;
cout << "即将退出进程" << endl;
ExitProcess(0);
}
else
{
cout << "异常过滤器验证失败,程序有没有被调试不知道" << endl;
}

ExceptionInfo->ContextRecord->Eip += 3;

return EXCEPTION_CONTINUE_EXECUTION;
}

void MyTestExceptionFilter()
{
printf("test\n");
SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
int a = 0;
int b = 2 / a;
cout << "跳过了异常代码" << endl;
cout << "程序正常结束" << endl;
}
int main()
{
MyTestExceptionFilter();
system("pause");
return 0;
}

这里eip+3的原因是错误代码的硬编码是三个字节,跳过就好了。

 但是这个用调试器就不行了,会一直在异常这里,不知道为什么。

 修正:

      官方文档上写了这个异常过滤器的函数的描述,如果这个程序正在被调试,那么异常会直接传递给调试器,所以当我们进行调试的时候这个异常过滤器是没有作用的。

 采用SEH来处理:

EXCEPTION_DISPOSITION mySEH(struct _EXCEPTION_RECORD* ExceptionRecord,PVOID EstablisherFrame,PCONTEXT pcontext,PVOID DispatcherContext)
{
0)
{
printf("SEH验证异常,程序正在被调试,即将退出程序\n");
ExitProcess(0);
}
else
{
printf("SEH验证正常\n");
}
pcontext->Eip += 3;

return ExceptionContinueExecution;
}
void TestSeh()
{
printf("test SEH\n");

__asm
{
push mySEH
mov eax,fs:[0]
push eax
mov fs:[0],esp
mov eax, 0
mov[eax], 1
}
cout << "SEH: 跳过了异常代码" << endl;
cout << "SEH: 程序正常结束" << endl;
}

然后通过调试就OK了,当调试器没有选择捕获你这个异常的时候,异常就是交给程序的VEH,SEH,异常过滤器来处理的。

采用VEH来处理:

LONG WINAPI MyPvectoredExceptionHandler(_EXCEPTION_POINTERS* ExceptionInfo)
{
0)
{
printf("VEH 验证异常,程序正在被调试,即将退出程序\n");
ExitProcess(0);
}
else
{
printf("硬件断点的VEH验证正常,无硬件断点\n");
}
ExceptionInfo->ContextRecord->Eip += 3;
return EXCEPTION_CONTINUE_EXECUTION;
}
void TestVeh()
{
printf("test VEH\n");
AddVectoredExceptionHandler(1, MyPvectoredExceptionHandler);
__asm
{
mov eax, 0
mov[eax], 1
}
cout << "VEH: 跳过了异常代码" << endl;
cout << "VEH: 程序正常结束" << endl;
}

首先需要理解硬件断点的原理,然后通过各种异常验证的时候比对硬件断点是否存在来进行验证。然后就是VEH,SEH,还有异常过滤器的使用方法。

十、检测调试对象

有一些内容采用的是WRK里面的定义。因为这个算是没有公开的文档,公开的不能这样使用。

查询父进程实现反调试

正常打开(双击运行)的程序的父进程是explorer.exe(资源管理器)(Windows的内置机制),通过查询父进程的ID是否是explorer.exe来判断程序是否被调试了。

这里我直接上代码了。

NTSTATUS
NtQueryInformationProcess(
   __in HANDLE ProcessHandle,
   __in PROCESSINFOCLASS ProcessInformationClass,
   __out_bcount(ProcessInformationLength) PVOID ProcessInformation,
   __in ULONG ProcessInformationLength,
   __out_opt PULONG ReturnLength
  )
#include<iostream>
#include<Windows.h>
#include<winternl.h>
#include<tlhelp32.h>
#include<string.h>
using namespace std;

typedef struct _PROCESS_BASIC_INFORMATION_1 {
   NTSTATUS ExitStatus;
   PPEB PebBaseAddress;
   ULONG_PTR AffinityMask;
   KPRIORITY BasePriority;
   ULONG_PTR UniqueProcessId;
   ULONG_PTR InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION_1;

//定义函数指针
typedef NTSTATUS (NTAPI *_NtQueryInformationProcess)(
   __in HANDLE ProcessHandle,
   __in PROCESSINFOCLASS ProcessInformationClass,
   __out_bcount(ProcessInformationLength) PVOID ProcessInformation,
   __in ULONG ProcessInformationLength,
   __out_opt PULONG ReturnLength
);

_NtQueryInformationProcess NtQueryInformationProcess_My;

int main()
{
//获得NtQueryInformation函数指针
   HMODULE hNtdll = GetModuleHandleA("ntdll.dll");//因为每个程序都会有ntdll,所以就不用动态loadlibrary加载了,直接获取dll的句柄
   if (hNtdll == NULL)
  {
       cout << "获取ntdll句柄错误" << endl;
       return 0;
  }
   NtQueryInformationProcess_My = (_NtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");

   //获取当前进程的信息
   HANDLE hCurrentProcess = GetCurrentProcess();
   PROCESS_BASIC_INFORMATION_1 CurrentInfo = { 0 };
   NtQueryInformationProcess_My(hCurrentProcess, ProcessBasicInformation, &CurrentInfo,sizeof(CurrentInfo),NULL);
   auto CurrentParentProcessId = CurrentInfo.InheritedFromUniqueProcessId;

   //遍历所有进程信息,对比进程ID
   PROCESSENTRY32 SnapshotInfo = { 0 };
   SnapshotInfo.dwSize = sizeof(PROCESSENTRY32);
   HANDLE  hSnapshot =  CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
   if (hSnapshot == INVALID_HANDLE_VALUE)
  {
       cout << "创建进程快照失败" << endl;
       return 0;
  }
   auto Sucess = Process32First(hSnapshot, &SnapshotInfo);
   if (Sucess == TRUE)
  {
       do {
           if (wcscmp(SnapshotInfo.szExeFile, L"explorer.exe")==0)//这里使用宽字符
          {
               if (SnapshotInfo.th32ProcessID == CurrentParentProcessId)
              {
                   cout << "程序通过父进程检测,程序的父进程正常" << endl;
              }
               else
              {
                   cout << "检测到父进程不对,程序正在被调试" << endl;
              }
               break;
          }
      } while (Process32Next(hSnapshot, &SnapshotInfo));
  }
   else
  {
       cout << "获取第一个进程信息失败" << endl;
       return 0;
  }
   system("pause");
   return 0;
}

查询调试内核调试对象

当调试器调试某进程时,会创建一个内核的调试对象,通过检测它是否存在来实现反调试。(这里就采用WRK里面的内容了,官方文档的内容不好使。)

通过NtQueryObject可以检测是否存在调试对象,通过这个东西来看程序是否在被调试。

__kernel_entry NTSYSCALLAPI NTSTATUS NtQueryObject(
 HANDLE                   Handle,
 OBJECT_INFORMATION_CLASS ObjectInformationClass,
 PVOID                    ObjectInformation,
 ULONG                    ObjectInformationLength,
 PULONG                   ReturnLength
);//官方文档版本

NTSTATUS
NtQueryObject (
   __in HANDLE Handle,//需要查询的句柄
   __in OBJECT_INFORMATION_CLASS ObjectInformationClass,
    //查询对象枚举类
   __out_bcount_opt(ObjectInformationLength) PVOID ObjectInformation,
       //输出结构缓冲区
   __in ULONG ObjectInformationLength,
    //缓冲区大小

   __out_opt PULONG ReturnLength
  //实际需要大小
  );//wrk版本

NtQueryObject中的OBJECT_INFORMATION_CLASS参数:

typedef enum _OBJECT_INFORMATION_CLASS {
   ObjectBasicInformation,
   ObjectNameInformation,
   ObjectTypeInformation,
   ObjectTypesInformation,//所有内核对象类型信息
   ObjectHandleFlagInformation,
   ObjectSessionInformation,
   MaxObjectInfoClass  // MaxObjectInfoClass should always be the last enum
} OBJECT_INFORMATION_CLASS;
//UNICODE_STRING结构体
typedef struct _UNICODE_STRING {
   USHORT Length;
   USHORT MaximumLength;
#ifdef MIDL_PASS
  [size_is(MaximumLength / 2), length_is((Length) / 2) ] USHORT * Buffer;
#else // MIDL_PASS
   PWSTR  Buffer;
#endif // MIDL_PASS
} UNICODE_STRING;

然后初步代码是这样:

#include"Anti-Debug.h"
int main()
{
   //先获取ntdll的句柄
   HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

   //获取函数指针
   NtQueryObject = (_NtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");

   //来一个很大的缓冲区
   char* buffer = (char*)malloc(0x4000);
   DWORD realSize = 0;
   auto ret = NtQueryObject(NULL, ObjectTypesInformation, buffer, 0x4000, &realSize);
   if(ret != 0)
  {
       printf("NtQueryObject error\n");
  }

   CloseHandle(hNtdll);
   return 0;
}

//Anti-Debug.h
#pragma once
#include<iostream>
#include<Windows.h>
using namespace std;
typedef enum _OBJECT_INFORMATION_CLASS {
   ObjectBasicInformation,
   ObjectNameInformation,
   ObjectTypeInformation,
   ObjectTypesInformation,
   ObjectHandleFlagInformation,
   ObjectSessionInformation,
   MaxObjectInfoClass  // MaxObjectInfoClass should always be the last enum
} OBJECT_INFORMATION_CLASS;
typedef  NTSTATUS(NTAPI* _NtQueryObject)(
   __in HANDLE Handle,
   __in OBJECT_INFORMATION_CLASS ObjectInformationClass,
   __out_bcount_opt(ObjectInformationLength) PVOID ObjectInformation,
   __in ULONG ObjectInformationLength,
   __out_opt PULONG ReturnLength
  );
_NtQueryObject NtQueryObject;
typedef struct _UNICODE_STRING {
   USHORT Length;
   USHORT MaximumLength;
#ifdef MIDL_PASS
  [size_is(MaximumLength / 2), length_is((Length) / 2)] USHORT* Buffer;
#else // MIDL_PASS
   PWSTR  Buffer;
#endif // MIDL_PASS
} UNICODE_STRING;

然后来通过对这个buffer进行一个处理。

处理缓冲区

这个缓冲区是一个比较麻烦的结构,存放了所有内核对象的内容。

typedef struct _OBJECT_TYPES_INFORMATION
{
   ULONG numberOfTypesInfo;
   OBJECT_TYPE_INFORMATION typeInfo[1];
};
//也就是说TYPES包含了type在里面,因为TYPES是所有的type的内容

//缓冲区结构体
//   __out_bcount_opt(ObjectInformationLength) PVOID //ObjectInformation,
typedef struct _OBJECT_TYPE_INFORMATION {
   UNICODE_STRING TypeName;
//内核对象类型的名称,比如互斥体,事件等等  
   ULONG TotalNumberOfObjects;//对象的数量
   ULONG TotalNumberOfHandles;
   ULONG TotalPagedPoolUsage;
   ULONG TotalNonPagedPoolUsage;
   ULONG TotalNamePoolUsage;
   ULONG TotalHandleTableUsage;
   ULONG HighWaterNumberOfObjects;
   ULONG HighWaterNumberOfHandles;
   ULONG HighWaterPagedPoolUsage;
   ULONG HighWaterNonPagedPoolUsage;
   ULONG HighWaterNamePoolUsage;
   ULONG HighWaterHandleTableUsage;
   ULONG InvalidAttributes;
   GENERIC_MAPPING GenericMapping;
   ULONG ValidAccessMask;
   BOOLEAN SecurityRequired;
   BOOLEAN MaintainHandleCount;
   ULONG PoolType;
   ULONG DefaultPagedPoolCharge;
   ULONG DefaultNonPagedPoolCharge;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;

然后大致上是完成了:

#include"Anti-Debug.h"
int main()
{
   //先获取ntdll的句柄
   HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

   //获取函数指针
   NtQueryObject = (_NtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");

   //来一个很大的缓冲区
   char* buffer = (char*)malloc(0x4000);
   DWORD realSize = 0;
   auto ret = NtQueryObject(NULL, ObjectTypesInformation, buffer, 0x4000, &realSize);
   if(ret != 0)
  {
       printf("NtQueryObject error\n");
       return 0;
  }

   //定义TYPES对于的结构体来获取缓冲区的内容
   POBJECT_TYPES_INFORMATION typesInfo = NULL;
   typesInfo = (POBJECT_TYPES_INFORMATION)buffer;
   //循环遍历typesInfo
   POBJECT_TYPE_INFORMATION typeInfo = typesInfo->typeInfo;
   //采用单个的type结构体来获取每个type的内容
   for(int i = 0; i < typesInfo->numberOfTypesInfo; i++)
  {
       if (wcscmp(L"DebugObject", typeInfo->TypeName.Buffer)==0)//是调试类型的内核对象
      {
           if (typeInfo->TotalNumberOfObjects > 0)
          {
               cout << "有调试内核对象,正在进行调试" << endl;
          }
           else
          {
               cout << "没有检测到内核调试对象" << endl;
          }
           break;
           typeInfo++;
      }
  }
   return 0;
}

但是有一个问题出现了,由于typeinfo这个单type的内容的结构体里面有一个UNICODE_STRING字段这个字段里面又有个buffer,这个buffer根据内容的不同而大小不同,所以不能直接++

//修正指针偏移
      DWORD temp = typeInfo->TypeName.MaximumLength;
      temp +=temp % 4;
      typeInfo = (POBJECT_TYPE_INFORMATION)((DWORD)typeInfo + temp);
      typeInfo++;

最终版本:

#include"Anti-Debug.h"
int main()
{
  //先获取ntdll的句柄
  HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

  //获取函数指针
  NtQueryObject = (_NtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");

  //来一个很大的缓冲区
  char* buffer = (char*)malloc(0x4000);
  DWORD realSize = 0;
  auto ret = NtQueryObject(NULL, ObjectTypesInformation, buffer, 0x4000, &realSize);
  if(ret != 0)
  {
      printf("NtQueryObject error\n");
      return 0;
  }

  //定义TYPES对于的结构体来获取缓冲区的内容
  POBJECT_TYPES_INFORMATION typesInfo = NULL;
  typesInfo = (POBJECT_TYPES_INFORMATION)buffer;
  //循环遍历typesInfo
  POBJECT_TYPE_INFORMATION typeInfo = typesInfo->typeInfo;
  //采用单个的type结构体来获取每个type的内容
  for(int i = 0; i < typesInfo->numberOfTypesInfo; i++)
  {
      if (wcscmp(L"DebugObject", typeInfo->TypeName.Buffer)==0)//是调试类型的内核对象
      {
          if (typeInfo->TotalNumberOfObjects > 0)
          {
              cout << "有调试内核对象,正在进行调试" << endl;
          }
          else
          {
              cout << "没有检测到内核调试对象" << endl;
          }
          break;
      }
      DWORD temp = typeInfo->TypeName.MaximumLength;
      temp +=temp % 4;
      typeInfo = (POBJECT_TYPE_INFORMATION)((DWORD)typeInfo + temp);
      typeInfo++;
  }
  return 0;
}

//头文件
#pragma once
#include<iostream>
#include<Windows.h>
using namespace std;
typedef enum _OBJECT_INFORMATION_CLASS {
  ObjectBasicInformation,
  ObjectNameInformation,
  ObjectTypeInformation,
  ObjectTypesInformation,
  ObjectHandleFlagInformation,
  ObjectSessionInformation,
  MaxObjectInfoClass // MaxObjectInfoClass should always be the last enum
} OBJECT_INFORMATION_CLASS;
typedef NTSTATUS(NTAPI* _NtQueryObject)(
  __in HANDLE Handle,
  __in OBJECT_INFORMATION_CLASS ObjectInformationClass,
  __out_bcount_opt(ObjectInformationLength) PVOID ObjectInformation,
  __in ULONG ObjectInformationLength,
  __out_opt PULONG ReturnLength
  );
_NtQueryObject NtQueryObject;

typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
#ifdef MIDL_PASS
  [size_is(MaximumLength / 2), length_is((Length) / 2)] USHORT* Buffer;
#else // MIDL_PASS
  PWSTR Buffer;
#endif // MIDL_PASS
} UNICODE_STRING;

typedef struct _OBJECT_TYPE_INFORMATION {
  UNICODE_STRING TypeName;
  ULONG TotalNumberOfObjects;
  ULONG TotalNumberOfHandles;
  ULONG TotalPagedPoolUsage;
  ULONG TotalNonPagedPoolUsage;
  ULONG TotalNamePoolUsage;
  ULONG TotalHandleTableUsage;
  ULONG HighWaterNumberOfObjects;
  ULONG HighWaterNumberOfHandles;
  ULONG HighWaterPagedPoolUsage;
  ULONG HighWaterNonPagedPoolUsage;
  ULONG HighWaterNamePoolUsage;
  ULONG HighWaterHandleTableUsage;
  ULONG InvalidAttributes;
  GENERIC_MAPPING GenericMapping;
  ULONG ValidAccessMask;
  BOOLEAN SecurityRequired;
  BOOLEAN MaintainHandleCount;
  ULONG PoolType;
  ULONG DefaultPagedPoolCharge;
  ULONG DefaultNonPagedPoolCharge;
} OBJECT_TYPE_INFORMATION, * POBJECT_TYPE_INFORMATION;

typedef struct _OBJECT_TYPES_INFORMATION
{
  ULONG numberOfTypesInfo;
  OBJECT_TYPE_INFORMATION typeInfo[1];
}OBJECT_TYPES_INFORMATION,*POBJECT_TYPES_INFORMATION;

十一、附加进程反调试

经常在给调试器附加进程时进程直接崩溃了,比如说用比较常见的ollydbg没有加任何处理来附加DNF就会直接崩溃。

附加调试流程

debugger(调试器)在调用DebugActiveProcess来附加进程后,进程中有多少个线程,调试器就会发送多少个CREATE_THREAD_DEBUG_EVENT事件给调试器,加载了多少dll也会发送多少个LOAD_DLL_DEBUG_EVENT事件给调试器,调试器通过WaitForDebugEvent函数来获取所有的调试事件,并做处理。

然后调用ContinueDebugEvent让程序继续运行,同时也会等待新的调试事件,然后操作系统恢复进程中的所有线程,在第一个线程被恢复的时候会调用Ntdll.dll中的DbgBreakPoint函数,而这个DbgBreakPoint函数就是一个int3断点,利用这点可以HOOK掉DbgBreakPoint来直接调用结束进程函数来实现反调试

验证:

这里我随便找了个程序用xdbg来附加:

可以看到就是停在了DbgBreakPoint这个int3断点这里。

十二、CRC检测

CRC的全称是循环冗余校验,作用是为了检测数据的完整性。

CRC32的检测原理:

程序被编译后,代码段是固定的,因为已经被写死了。

我们在调试程序的时候,打断点或者修改代码都会影响CRC32的值,这个时候只需要检测CRC32的某一时刻值和最初的CRC32值是否一致就可以判断代码是否被修改了。

如果装了好压这个软件可以直接通过右键查看到CRC32的值:

这里我找了一个简单的CRC32的代码来测试一下:

#include<iostream>
uint32_t crc32_table[256];
int make_crc32_table()
{
   uint32_t c;
   int i = 0;
   int bit = 0;

   for (i = 0; i < 256; i++)
  {
       c = (uint32_t)i;

       for (bit = 0; bit < 8; bit++)
      {
           if (c & 1)
          {
               c = (c >> 1) ^ (0xEDB88320);
          }
           else
          {
               c = c >> 1;
          }

      }
       crc32_table[i] = c;
  }

   return 1;
}
uint32_t make_crc(unsigned char* string, uint32_t size)
{
   uint32_t crc= 0xFFFFFFFF;
   make_crc32_table();
   while (size--)
       crc = (crc >> 8) ^ (crc32_table[(crc ^ *string++) & 0xff]);

   return crc;
}

利用CRC32检测自己程序是否被修改:

通过对代码段进行CRC32判断来处理。首先要拿到代码段,通过Windows 的PE文件的结构体 Dos头->NT头->(采用VS里的结构体)拿到区段头->然后通过区段头得到区段的信息。

    char *buffer=(char*)GetModuleHandleA(0);//参数为0就获取当前进程的句柄
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + buffer);
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections - 1; i++)
{

}

判断是否是代码段:

代码段顾名思义,就是执行代码的东西。在PE文件中的区段头里有一个字段可以判断属性:

typedef struct _IMAGE_SECTION_HEADER {
  BYTE   Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
          DWORD   PhysicalAddress;
          DWORD   VirtualSize;
  } Misc;
  DWORD   VirtualAddress;
  DWORD   SizeOfRawData;
  DWORD   PointerToRawData;
  DWORD   PointerToRelocations;
  DWORD   PointerToLinenumbers;
  WORD   NumberOfRelocations;
  WORD   NumberOfLinenumbers;
  DWORD   Characteristics;//这个
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER

Characteristics这个字段标识了是否是可执行。在CFF里面查看:

这里通过我的测试,在修改可读可写的属性时会修改最高字段的值,如果有就加2,然后别的就修改别的值,所以这里我们判断首位是不是6就可以判断是不是代码段了。

void Crc32Test()
{
char *buffer=(char*)GetModuleHandleA(0);//参数为0就获取当前进程的句柄
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS pNtHeader
= (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + buffer);
PIMAGE_SECTION_HEADER pSectionHeader
= IMAGE_FIRST_SECTION(pNtHeader);
for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections - 1; i++)
{
if (pSectionHeader->Characteristics / 0x10000000 == 6)
{
cout
<< pSectionHeader->Name << endl;
auto CrcNum
= make_crc((unsigned char*)(pSectionHeader->VirtualAddress + buffer), pSectionHeader->Misc.VirtualSize);
cout
<< CrcNum << endl;
}
pSectionHeader
++;
}
}

然后通过ollydbg随便打一个int3断点(因为int3断点会添加一个CC在里面所以肯定的改变了代码段的内容的)再运行来看CRC的值是否改变:

这里果然是改变了。

过CRC检测

  因为CRC的原理是要扫描你的整个代码段,那么可以通过CE工具来通过代码段的地址,查看是谁访问了该代码段,由此来找到CRC检测的函数,从而进行魔改通过CRC检测。要想逆一个东西,知道这个东西的原理再来逆会非常方便。

CRC有点类似于hash函数,就是把一坨东西来计算一个值出来,由于一个程序写好了之后代码段是不会改变的,所以我们可以把它用来计算代码段的内容,然后再程序里面时刻计算这个值有没有改变来判断是否被修改了代码。

十三、虚拟机检测

其实虚拟机检测也无外乎就是检测一些虚拟机特有的特征,然后来判断是否是在虚拟机里面,比如说这里(查看系统中运行的服务):

但是不能直接盲目的通过VMware这六个关键字母就判断是在虚拟机里面了,因为你的电脑如果开了虚拟机,也会有一些VMware开头的虚拟机服务:

所以我们必须要选取一些比较唯一的,比如说这里我们选这个VMware SVGA Helper Service这个把,大家可以自行选择。然后需要做的就是遍历系统中的服务,看看有没有这个东西就行了。

遍历系统中的服务:

这里会用到一个API:

EnumServicesStatusA function (winsvc.h) - Win32 apps | Microsoft Docs

BOOL EnumServicesStatusA(
 SC_HANDLE              hSCManager,
 DWORD                  dwServiceType,
 DWORD                  dwServiceState,
 LPENUM_SERVICE_STATUSA lpServices,
 DWORD                  cbBufSize,
 LPDWORD                pcbBytesNeeded,
 LPDWORD                lpServicesReturned,
 LPDWORD                lpResumeHandle
);

然后这个API的第一个参数需要用OpenSCManager这个API来使用:OpenSCManagerA function (winsvc.h) - Win32 apps | Microsoft Docs

SC_HANDLE OpenSCManagerA(
 LPCSTR lpMachineName,//如果值为0表示本机
 LPCSTR lpDatabaseName,
 DWORD  dwDesiredAccess
);

然后这个服务数据库的句柄必须调用一个API来关闭:

CloseServiceHandle()

void TestVirtual()
{
auto hSCM
= OpenSCManagerA(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE);
/*
第一个为NULL表示是本机,
第二个参数是指定值,
第三个参数表示权限,如果要调用枚举服务API就需要是这个权限。
*/
if (hSCM == NULL)
{
cout
<< "打开系统服务数据库错误" << endl;
return;
}
LPENUM_SERVICE_STATUSA lpServiceStatus
= NULL;
lpServiceStatus
= (LPENUM_SERVICE_STATUSA)malloc(0x8000);
DWORD servieceSize
= 0;
DWORD needSize
= 0;
DWORD resumeHandle
= 0;
auto ret
= EnumServicesStatusA(hSCM, SERVICE_WIN32, SERVICE_STATE_ALL, lpServiceStatus, 0x8000, &needSize, &servieceSize, &resumeHandle);
/*
第一个参数:是服务数据库的句柄
第二个参数:是要的服务的类型,这里不直到怎么查看我就全部加上了
第三个参数:服务的状态,是否启动
第四个参数:服务结构体的缓冲区
第五个参数:缓冲区大小
第六个参数:如果缓冲区大小不够,需要的剩下的大小是多少
第七个参数:得到的服务的数量
第八个参数:缓冲区不够的时候下一个服务的结构体指针
*/
if(needSize>0x8000)
{
cout
<< "缓冲区过小" << endl;
return;
}
for (int i = 0; i < servieceSize; i++)
{
if (strcmp(lpServiceStatus->lpDisplayName, "VMware SVGA Helper Service")==0)
{
cout
<<"检测到了虚拟机"<<endl;
return ;
}
lpServiceStatus
++;
}
cout
<< "没有检测到虚拟机" << endl;
CloseServiceHandle(hSCM);
}


还有很多方式,大家可以入手,只要是可以唯一标识虚拟机就好。

特殊的办法

这个办法我也不知道为什么。不是虚拟机的环境运行下面的代码会崩溃。是虚拟机则安然无恙。

            push   edx

            push   ecx
push ebx
mov eax, 'VMXh'
mov ebx, 0
mov ecx, 10
mov edx, 'VX'
in eax, dx
cmp ebx, 'VMXh'
pop ebx
pop ecx
pop edx
//代码实现:
void testAssemer()
{
int FLAG = FALSE;
__try
{
__asm
{
push edx
push ecx
push ebx
mov eax,
'VMXh'
mov ebx,
0
mov ecx,
10
mov edx,
'VX'
in eax, dx
cmp ebx,
'VMXh'
pop ebx
pop ecx
pop edx
}
}
__except(
1)
{
FLAG
= TRUE;
}
if (FLAG == FALSE)
{
cout
<< "检测到了虚拟机" << endl;
}
else
{
cout
<< "未检测到虚拟机" << endl;
}
}

小结

虚拟机和本机肯定是有一些不同的,抓住这些验证不同的办法就可以来检测是否是在虚拟机里了。

十四、调试器原理

调试一个程序分两种情况:

1 打开这个程序。

2:这个程序已经是一个运行状态了,将其进程进行附加。

打开进程

通过打开运行进程方式来调试进程需要调用一个API:

BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
//dwCreatetionFlags需要设置为DEBUG_PROCESS
//打开进程示例程序
STARTUPINFOA sw{ 0 };
PROCESS_INFORMATION pInfo{
0 };
auto retCP
= CreateProcessA("F:\\Sublime Text 3\\sublime_text.exe",
NULL,NULL,NULL,FALSE,
DEBUG_PROCESS,NULL,NULL,
&sw,&pInfo);
if (retCP == 00)
{
cout
<< "打开进程失败" << endl;
return;
}

附加进程

通过DebugActiveProcess这个API来附加到进程。

BOOL DebugActiveProcess(
DWORD dwProcessId
);

细节

无论是通过打开进程还是附加进程来实现调试,都只是开始调用的方式不一样,在调试器和操作系统之间的交互方式都是相同的。

创建了调试进程后接下来就是死循环等待调试事件

当调试进程时,被调试进程执行的一些操作事件将会被通知给调试器,比如dll的加载和卸载,thread的创建和销毁,异常信息等等。当这些事件需要被发送到调试器时,Windows内核将首先挂起进程中的所有线程,然后把发生的事件通知给调试器,等待调试器的处理。

调试器通过WaitForDebugEvent API来等待调试事件,调试事件被封装到了DEBUG_EVENT结构体中,调试器需要处理的就是循环接受调试事件然后处理DEBUG_EVENT结构体中传递过来的不同调试信息。

在发送事件event给调试器debugger时,被调试进程会被挂起,直到调试器调用了continueDebugEvent函数

利用调试器原理实现附加反调试

利用调试器的原理,我们可以通过创建一个调试模式下的进程,那么这个以调试模式创建的进程就不能被其它进程拿去调试了,因为它已经在被一个我们自己的进程以调试模式创建了。

#include<iostream>
#include
<Windows.h>
using namespace std;void TestDebugger()
{
STARTUPINFOA sw{
0 };
PROCESS_INFORMATION pInfo{
0 };
auto retCP
= CreateProcessA("E:\\test\\Debug\\02 CStaticText.exe",
NULL, NULL, NULL, FALSE,
DEBUG_PROCESS, NULL, NULL,
&sw, &pInfo);
if (retCP == 0)
{
cout
<< "打开进程失败" << endl;
return;
}
while (TRUE)
{
DEBUG_EVENT debugEvent{
0 };
auto rDebugEvent
= WaitForDebugEvent(&debugEvent, -1);
if (rDebugEvent)
{
cout
<< debugEvent.dwDebugEventCode << endl;
//dwDebugEventCode是用来区分不同事件的事件码,用来判断事件
}
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId,
DBG_CONTINUE);
//在发送事件event给调试器debugger时,被调试进程会被挂起,直到调试器调用了continueDebugEvent函数
}
}
int main()
{

TestDebugger();
system(

"pause");
return 0;
}

然后来测试一下,这样启动后,是否还能被调试器附加上:

这样一来就不会被调试器附加上了。

十五、检测进程和窗口

检测一些常见的调试器的进程以及窗口是否存在也可以作为一种反调试的手段。

这个比较简单就直接上代码了:

#include<Windows.h>
#include<iostream>
#include<tlhelp32.h>
using namespace std;

void TestProcess()
{
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
if (hSnapshot == INVALID_HANDLE_VALUE)
{
cout << "打开进程快照失败" << endl;
return;
}
PROCESSENTRY32 tempContext{ 0 };
tempContext.dwSize = sizeof(PROCESSENTRY32);
Process32First(hSnapshot, &tempContext);
do
{
if (wcscmp(tempContext.szExeFile,L"x32dbg.exe") ==0)
{
cout << "检测到了xdbg调试进程" << endl;
break;
}
} while (Process32Next(hSnapshot,&tempContext));

CloseHandle(hSnapshot);
}

void CheckWindow()
{
HANDLE hWindow = FindWindowA(0,"x32dbg");
if (hWindow != NULL)
{
cout << "检测到了x32dbg" << endl;
}

}

int main()
{
TestProcess();
CheckWindow();
return 0;
}

#include<Windows.h>

#include<iostream>

#include<tlhelp32.h>

using namespace std;
void TestProcess(){

HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);

if (hSnapshot == INVALID_HANDLE_VALUE){

cout << "打开进程快照失败" << endl;

return;

}

PROCESSENTRY32 tempContext{0 };

tempContext.dwSize = sizeof(PROCESSENTRY32);

Process32First(hSnapshot, &tempContext);

do{if (wcscmp(tempContext.szExeFile,L"x32dbg.exe") ==0){

cout << "检测到了xdbg调试进程" << endl;break;

}} while (Process32Next(hSnapshot,&tempContext));

CloseHandle(hSnapshot);}


void CheckWindow(){

HANDLE hWindow = FindWindowA(0,"x32dbg");

if (hWindow != NULL){cout << "检测到了x32dbg" << endl;}
}
int main(){

TestProcess();

CheckWindow();

return 0;}

十六、检测TF标志寄存器

在intel的x86寄存器中有一种叫标志寄存器:

标志寄存器中的TF(Trap Flag)位,CPU在执行完一条指令后,如果检测到标志寄存器的TF位为1,则会产生一个int 1中断,然后再将TF置为0,后进行int 1中断后继续执行。操作系统上可以视为异常。

再加上出现了异常如果有调试器就会返回给调试器,如果调试器继续往下走也没啥问题,因为我们这里的异常代码是一个nop,而一个普通运行的进程因为没调试器,所以就会走异常处理程序,所以就是走异常处理程序的就是正常进程,没走的就是异常。

代码实践:

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

#include<Windows.h>

#include<iostream>

#include<tlhelp32.h>

using namespace std;

void HaveStep()

{

    cout << "检测到了单步调试" << endl;

    ExitProcess(0);

}

void NoStep()

{

    cout << "没有检测到单步调试" << endl;

}

void CheckTFflag()

{

    cout << "test" << endl;

    DWORD HaveStepAddr = (DWORD)HaveStep;

    __try

    {

        __asm

        {

            pushfd

            or dword ptr ss : [esp] , 0x100

            popfd

            nop

            jmp HaveStepAddr

        }

    }

    __except (1)

    {

        NoStep();

    }

}

int main()

{

    CheckTFflag();

    system("pause");

    return 0;

}

正常运行的结果和采用vs单步调试的结果:


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NDcxMDQzNA==&mid=2247487665&idx=1&sn=e0edd38fab21c5f438264de26756a1e2&chksm=a682c67c91f54f6a8770be132bf0a02a0bab5f28c81a806eb0e194d8991529a2b5fd517a8bf9#rd
如有侵权请联系:admin#unsafe.sh