常见动态反调试技术总结

动态反调试

本文介绍了几种常见的动态反调试技术,在阅读之前您可能要了解一些静态反调试手段.

前文 : 常见静态反调试技术总结

本文实例所用资源 : https://download.csdn.net/download/weixin_45551083/12555778

1 SEH

1.0 SEH基本概念

基本概念

百度百科:

SEH(“Structured Exception Handling”),即结构化异常处理·是(windows)操作系统提供给程序设计者的强有力的处理程序错误或异常的武器。

SEH是Windows操作系统默认的异常处理机制,逆向分析中,SEH除了基本的异常处理功能外,还大量运用于反调试程序. 就是异常时会启动SEH,SEH里面包括了处理异常的代码.

与C语言中的__try,__except,finally等处理机制类似,不过SEH要早于C语言中的异常处理

示例程序 seh.exe

地址0x401019处尝试向DS:[0]处写入数据,引发非法访问异常(较为常见的一种异常)

1593085446597

正常运行:

1593085742784

调试器下 : F9运行程序,调试器会暂停在此处,调试器下方会提示

1593085553809

Shift+F7/F8/F9 运行

1593085604080

调试器将异常交给SEH处理,而SEH中又有反调试技术,所以程序显示Debugger detected

1.1 异常

(1) 几个常见异常
  • EXCEPTION_ACCESS_VIOLATION ( 0xC0000005 )

试图访问不存在或不具有访问权限的内存区域时,就会发生EXCEPTION_ACCESS_VIOLATION(非法访问,较为常见)

举例:

MOV DWORD PTR DS:[0],1  ;内存地址0处是未分配的区域
ADD DWORD PTR DS:[401000],1;.text节区起始地址0x401000仅具有读权限,无写权限
XOR DWORD PTR DS:[80000000],1234;内存地址0x80000000属于内核区域,用户无法访问
  • EXCEPTION_BREAKPOINT ( 0x80000003 )

运行代码中设置断点后,CPU尝试执行该地址处的指令是,将发生EXCEPTION_BREAKPOINT异常.

调试器就是利用该异常实现断点功能. 设置断点时会将该位置设置为0xCC(int 3) 但是为了方便代码可读性,并不会显示出来(仍然是最开始的指令).

  • EXCEPTION_ILLEGAL_INSTRUCTION ( 0xC000001D )

CPU遇到无法解析的指令时引发该异常,比如"0FFFF"指令在x86CPU中未定义,CPU遇到该指令将引发EXCEPTION_ILLEGAL_INSTRUCTION异常.

  • EXCEPTION_INT_DIVIDE_BY_ZERO ( 0xC0000094 )

整数除法运算中,若分母为0,则引发EXCEPTION_INT_DIVIDE_BY_ZERO异常

  • EXCEPTION_SINGLE_STEP ( 0x80000004 )

单步的含义是执行一条指令,然后暂停.CPU进入单步模式后,每执行一条指令就会引发EXCEPTION_SINGLE_STEP异常,暂停运行.将EFLAGS寄存器的TF位设置为1后,CPU就会进入单步模式

(2) 异常编码对照表
异常 描述
EXCEPTION_ACCESS_VIOLATION 0xC0000005 程序企图读写一个不可访问的地址时引发的异常。例如企图读取0地址处的内存。
EXCEPTION_ARRAY_BOUNDS_EXCEEDED 0xC000008C 数组访问越界时引发的异常。
EXCEPTION_BREAKPOINT 0x80000003 触发断点时引发的异常。
EXCEPTION_DATATYPE_MISALIGNMENT 0x80000002 程序读取一个未经对齐的数据时引发的异常。
EXCEPTION_FLT_DENORMAL_OPERAND 0xC000008D 如果浮点数操作的操作数是非正常的,则引发该异常。所谓非正常,即它的值太小以至于不能用标准格式表示出来。
EXCEPTION_FLT_DIVIDE_BY_ZERO 0xC000008E 浮点数除法的除数是0时引发该异常。
EXCEPTION_FLT_INEXACT_RESULT 0xC000008F 浮点数操作的结果不能精确表示成小数时引发该异常。
EXCEPTION_FLT_INVALID_OPERATION 0xC0000090 该异常表示不包括在这个表内的其它浮点数异常。
EXCEPTION_FLT_OVERFLOW 0xC0000091 浮点数的指数超过所能表示的最大值时引发该异常。
EXCEPTION_FLT_STACK_CHECK 0xC0000092 进行浮点数运算时栈发生溢出或下溢时引发该异常。
EXCEPTION_FLT_UNDERFLOW 0xC0000093 浮点数的指数小于所能表示的最小值时引发该异常。
EXCEPTION_ILLEGAL_INSTRUCTION 0xC000001D 程序企图执行一个无效的指令时引发该异常。
EXCEPTION_IN_PAGE_ERROR 0xC0000006 程序要访问的内存页不在物理内存中时引发的异常。
EXCEPTION_INT_DIVIDE_BY_ZERO 0xC0000094 整数除法的除数是0时引发该异常。
EXCEPTION_INT_OVERFLOW 0xC0000095 整数操作的结果溢出时引发该异常。
EXCEPTION_INVALID_DISPOSITION 0xC0000026 异常处理器返回一个无效的处理的时引发该异常。
EXCEPTION_NONCONTINUABLE_EXCEPTION 0xC0000025 发生一个不可继续执行的异常时,如果程序继续执行,则会引发该异常。
EXCEPTION_PRIV_INSTRUCTION 0xC0000096 程序企图执行一条当前CPU模式不允许的指令时引发该异常。
EXCEPTION_SINGLE_STEP 0x80000004 标志寄存器的TF位为1时,每执行一条指令就会引发该异常。主要用于单步调试。
EXCEPTION_STACK_OVERFLOW 0xC00000FD 栈溢出时引发该异常。
(3) OS 的异常处理方式

同一程序在正常运行与调试运行时表现处的行为动作是不同的

  • 正常运行时的异常处理方法

进程运行过程中若发生异常,OS会委托进程处理.若进程代码中存在具体的异常处理(如SEH异常处理器)代码,则能顺利处理异常,程序继续运行.

若没有相关的SEH,则就无法处理,OS会启动默认的异常处理机制,终止进程运行.

(SEH处理代码和默认的异常处理机制中均可以加入反调试代码)

  • 调试运行时的异常处理方法

调试运行中发生异常时,OS会首先把异常抛给调试器(调试器就会暂停运行)

调试时遇到异常的几种处理方法:

​ (a) 直接修改异常有关的代码,寄存器,内存.(如用NOP填充异常代码)

​ (b) OllyDbg中的Shift+F7/F8/F9,将异常抛给被调试进程

​ (c ) 终止调试进程,终止调试

1.2 SEH详细说明

(1) SEH链

SEH以链表的形式存在.链中存在一系列异常处理器.遇到异常先由第一个异常处理器处理,若第一个异常处理器未正常处理异常,则交由链上的第二个异常处理器,直到得到处理.(最后一个异常处理器会终止进程)

代码层面: SEH是由_EXCEPTION_REGISTER_RECODE 结构体构成的链表

_EXCEPTION_REGISTER_RECODE 声明:

typedef struct _EXCEPTION_REGISTRATION_RECORD
{
PEXCEPTION_REGISTRATION_RECORD Next;//指针,指向下一个_EXCEPTION_REGISTER_RECODE结构体
PEXCEPTION_DISPOSITION Handler;//异常处理函数(异常处理器)地址
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

若Next成员的值为0xFFFFFFFF,则表示它是链表最后一个节点.

1593086361598

(2)异常处理函数

定义:

EXCEPTION_DISPOSITION _except_handler
(
	EXCEPTION_RECORD	*pRecord, 
	EXCEPTION_REGISTRATION_RECORD 	*pFrame ,
	CONTEXT		*pContext,
	PVOID		pValue
);
  • _except_handler 第一个参数 *pRecord 指向 EXCEPTION_RECORD 结构体的指针
typedef struct _EXCEPTION_RECORD {
 DWORD ExceptionCode;	//异常代码的值(如EXCEPTION_ACCESS_VIOLATION 就是0xC0000005)
 DWORD ExceptionFlags;
 struct _EXCEPTION_RECORD *ExceptionRecord;
 PVOID ExceptionAddress;	//发生异常的代码地址
 DWORD NumberParameters;
 DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
 }  EXCEPTION_RECORD;

注意ExceptionCode(第一个参数)和ExceptionAddress(第四个参数即可)

  • _except_handler 第三个参数*pContext指向Context结构体

CONTENT 线程结构体

typedef struct _CONTEXT
 {
     DWORD ContextFlags;
     DWORD   Dr0;	//04h
     DWORD   Dr1;	//08h
     DWORD   Dr2;	//0Ch
     DWORD   Dr3;	//10h
     DWORD   Dr6;	//14h
     DWORD   Dr7;	//18h
     FLOATING_SAVE_AREA FloatSave;
     DWORD   SegGs;	//88h
     DWORD   SegFs;	//90h
     DWORD   SegEs;	//94h
     DWORD   SegDs;	//98h
     DWORD   Edi;	//9Ch
     DWORD   Esi;	//A0h
     DWORD   Ebx;	//A4h
     DWORD   Edx;	//A8h
     DWORD   Ecx;	//ACh
     DWORD   Eax;	//B0h
     DWORD   Ebp;	//B4h
     DWORD   Eip;	//B8h
     DWORD   SegCs;	//BCh
     DWORD   EFlags;//C0h
     DWORD   Esp;	//C4h
     DWORD   SegSs;	//C8h
    
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; //512bytes
 } CONTEXT;

多线程环境下,每一个线程内部都有一个CONTEXT结构体用来备份CPU寄存器的值.

CPU离开当前线程去其他线程时,CPU寄存器的值就会保存到当前线程的CONTEXT结构体

CPU再次运行该线程时,会使用保存在CONTEXT结构体中的值来覆盖CPU寄存器的,从EIP处开始继续执行

异常发生时,执行异常代码的线程就会终端运行,转而运行SEH.

此时OS会把当前线程的CONTEXT结构体传递给异常处理函数.

异常处理函数可以修改CONTEXT.Eip的值(偏移B8),设置为其他的地址.这样,之前暂停的线程会执行新设置的EIP地址处的代码.(反调试中可以采用这条技术)

  • _except_handler返回值
typedf enum _EXCEPTION_DISPOSITION
{
	ExceptionContinueExecution=0,//继续执行异常代码,从发生异常处的代码继续运行
    ExceptionContinueSearch=1,//使用下一个异常处理器 将异常派送给下一个SEH链的异常处理器
    ExceptionNestedException=2,//在OS内部使用
    ExceptionCollidedUnwind=3//在OS外部使用
}EXCEPTION_DISPOSITION;
(3) 访问SEH链 - TEB.NtTib.ExceptionList

通过TEB结构体的NtTib访问SEH链:

TEB.NtTib.ExceptionList是TEB结构体第一个成员 即位于FS:[0]处

TEB结构体:

   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : Ptr32 Void
  ... ... ...
   ... ... ...
typedef struct	_NT_TIB{
	 struct	_EXCEPTION_REGISTRATION_RECORD *ExceptionList; //TEB.NtTib.ExceptionList
	 PVOID StackBase;
	 PVOID StackLimit; 
	 PVOID SubSystemTib; 
	 union {
		PVOID FiberData; 
		DWORD Version;
	 }; 
	 PVOID ArbitraryUserPointer;
 	 struct	_NT_TIB *Self;
} NT_TIB;
typedef NT_TIB *PNT_TIB;
(4) SEH安装

C语言中使用__try,__except,finally

汇编中:

push @myhandler	;压入异常处理器的地址 handler
push DWORD PTR FS:[0] ;SEH链的头部 next
mov DWORD PTR FS:[0]:ESP ;向链表中添加该异常处理器

新添加的异常处理器将取代原来链表中的第一个位置成为第一个异常处理器

这里DWORD PTR FS:[0]是next (原来的第一个异常处理器地址,安装了新的SEH后变为第二个)

1.3 调试seh.exe

  • 运行到0x401000(断点处)

1593093666071

  • 0x401000-0x40100C 为SEH的安装 handler地址为0040105A

  • F7运行到00401005查看FS:[0]内存处

1593094562135

  • 0019FF60处就是SEH链的开始(先压入Handler的地址再压入next)
  • 下面为未安装我们的SEH异常处理器的时候的SEH链(编译器,操作系统产生)

1593094631507

1593094661378

  • 安装的SEH异常处理器(接上了原来的链)

1593094827122

  • 继续跟踪,有非法访问异常

1593094868050

  • 运行到此处时在0040105A(第一个SEH处理器处下断点),然后Shift+F7 运行到SEH代码处

  • 异常处理器代码

1593095039870

  • 前面提到异常处理函数有四个参数,分别来看一下前三个参数

1593095241571

  • 第一个参数 : ESP+4处 : 是一个指向EXCEPTION_RECORD 结构体的指针

  • 这个结构体第一个参数为异常值,第三个为发生异常代码的地址

1593095410070

  • 异常值:C0000005 异常代码地址:00401019
  • 再看第二个参数ESP+8处:0019FF24

1593095699729

  • 虽然之前没有提到第二个参数,这个参数值为0019FF60, 正是当前FS:[0]的位置

  • 第三个参数:*pContext指向CONTEXT的指针0019FA44

  • CONTEXT.Eip的位置0019FA44+B8=0019FAFC

1593096083080

  • 里面正好是 00401019 (触发异常处)
  • 三个参数了解之后再看异常调试器代码

1593096154008

  • 这里SS:[ARG.3]为异常处理函数第三个参数,取异常处理函数第三个参数(CONTEXT指针)到ESI
  • 这里FS:[30]为PEB的位置,取地址到EAX
  • PEB偏移0x02处为BeingDebugged , 该值在调试状态下为1.
  • 与1对比看是否在调试状态.
  • 处于调试状态则不跳转.修改DS:[ESI+0B8]处值为00401023(CONTEXT.Eip)
  • 未调试状态跳转,在00401076处修改CONTEXT.Eip值为00401039

1593096621858

  • 这两处刚好是两种处理方式 , 这里只是弹个框,实际过程中为别的指令(干扰调试)

  • 再看XOR EAX,EAX(EAX为返回值) ,返回0 与前面异常处理函数返回值匹配

  • 然后是SEH函数的删除

1593096809412

1.4 OllyDbg中针对SEH的设置

1593096912968

1593096920645

  • 可以在此让调试器选择忽略一些SEH,即交给被调试进程处理
  • 但是SEH中有反调试手段的话也不可忽略,必须调试异常处理代码

2 Time Checking

通过比较一段代码的运行时长是否正常来判断是否处于反调试状态

2.1 时间测量方法

常用的有如下两种方法:

  1. 利用CPU的计数器

    RDTSC

    kernel32! QueryPerformanceCounter( ) / ntdll !NtQueryPerformanceCounter( )
    kernel32! GetTickCount ( )

    准确度:RDTSC > NtQueryPerformanceCounter( ) > GetTickCount ( )

  2. 利用系统的实际时间

    timeGetTime()

    _ftime()

2.2 RDTSC

  • x86CPU中存在一个名为TSC(Time Stamp Counter)64位寄存器, TSC保存着精确的时钟周期计数
  • RDTSC是一条汇编指令,用来将TSC的值读入EDX:EAX 寄存器(高32位EDX,低32位EAX)
示例DynAD_ RDTSC.exe
  • 在0x401000下断点运行到用户代码

1593227160530

  • 在0x40101C处是第一次RDTSC指令,0x40102A 第二次
  • 代码部分先比较了高32位,再比较了低32位
  • 即若前后两次计数器差大于FFFFFFFF则属于异常(高32位比较)
  • 大于FFFFFF也异常(低32位比较)
  • 大于FFFFFFFF直接跳转到0040103E ,大于FFFFFF也会执行0040103E处指令

1593227390999

  • 0040103E 处非法访问异常,程序会中止

  • 破解之法:

    • 直接run过去这段(F9)
    • 修改指令 如:

1593227505199

3 陷阱标志TF

原理

  • TF是EFLAGS上面的第九个比特位

1593227619574

  • TF设置为1时,CPU将进入单步模式,单步模式下,CPU执行一条指令就会触发一个EXCEPTION_SINGLE_STEP异常
  • 实际上用到了前面的SEH反调试技术
  • 可以在SEH异常处理器中使用反调试技术,修改Context.EIP的值

示例DynAD_SingleStep.exe

  • 0x401000下断点运行到用户代码

1593228584588

  • 这里调试器下TF位始终为0
  • 就需要设置忽略单步异常

1593228840821

  • 忽略单步异常 在SEH处理函数下断点,F9运行
  • 调试器在单步和未忽略单步异常的情况下会自动设置TF为0

1593228952642

  • 这里修改了Context.Eip,retn后直接到了卸载SEH异常处理器的代码

4 INT 2D

原理

INT 2D原为内核模式中触发断点异常的指令,在用户模式下也会触发异常.

但在调试程序时,仅仅忽略下一条指令的第一个字节而不会触发异常

也就是调试模式下,线程的SEH不会触发,通过这个进行反调试

示例:DynAD_INT2D.exe

  • 0x40100下断点

1593241327786

  • 首先在0x40100B处安装了SEH异常处理程序
  • 在0x40101E处有INT 2D指令
  • 正常情况下,执行INT 2D应该触发异常,进入SEH,但调试器下并不会
  • SEH代码:

1593241486334

  • 修改了CONTEXT.Eip值为00401044,然后返回0

  • 正常情况下从SEH出来将会直接从00401044继续执行,而跳过了00401021处的

    MOV DWORD PTR SS:[EBP-4],1

  • 而下面有判断会比较此处

1593241700502

  • 进而造成两种代码执行路径,达到反调试目的

破解之法:

​ 修改SS:[EBP-4]处的值

​ 或者int 2D 改为int 3

​ 或修改跳转等

选择改为int 3,要让调试器忽略此处的异常

1593241894658

就可以进入SEH

1593241926684

5 0xCC探测

原理

0xCC 是 int 3的机器码,调试下断点其实就是将 该位置的指令修改为0xCC

可以通过探测异常的0xCC 指令,来判断程序是否处于调试状态

有以下两种方法

  • 探测API断点

调试过程中为了方便经常会用到API断点

获取某某API起始第一个字节是否为0xCC 即可判断是否处于反调试状态

破解之法:避开在API的第一条指令下断点

  • 比较校验和

记录下某个地址区域指令的校验和,然后进行比较

如: DynAD_Checksum.exe

1593243286452

  • 这里ECX=401070-401000 , 从ESI= 401000 开始循环,每循环一次ESI加1,按字节计算从401000到401070的校验和
  • 校验和储存在EAX,与记录的校验和比较

1593245880702

  • 这里因为在校验区域下了断点.前后不一致

1593246053097

  • 若不相等,SS:[LOCAL.1]会被设置为1,在后面进行条件判断 跳转

  • 破解:修改jmp指令等方法 如:

1593246176349

猜你喜欢

转载自blog.csdn.net/weixin_45551083/article/details/106984536