re-core-principle-5
逆向工程核心原理笔记-5
0x00 反调试技术
反调试技术对调试器与OS有着很强的依赖性(Dependency)。即,有些反调试技术仅能在特定版本OS下正常工作,而且不同种类调试器应用的反调试技术也略有不同。本书主要介绍针对XP与WIN7的反调试。
反调试技术分为动态与静态。运用静态技术时,只要在开始破解1次,即可解除全部反调试限制。而运用动态技术时,要一边调试一边破解。因此,动态反调试技术使得破解更困难。如下表所示:
静态反调试技术主要用来探测调试器,如果探测到,那么程序就无法正常运行。动态反调试会扰乱调试器跟踪(类似于逐行调试)的功能,使我们无法查看程序中的代码与数据。
0x01 静态反调试技术
方法1:PEB
PEB结构体中有一些成员与反调试技术密切相关,如下所示:
1 | 1. BeingDebugged |
注:经过实验,在WIN11中,IsDebuggerPresent
、NtGlobalFlag
是有用的,而Ldr
、ProcessHeap
是没用的。
方法2:NtQuerylnformationProcess
通过NtQuerylnformationProcess
API可以获得与进程调试相关的信息,函数定义如下所示:
其中,ProcessInformation
输出人们想要的信息,而ProcessInformationClass
是一个枚举类型,来向函数说明自己想要哪些信息。ProcessInformationClass
中与调试相关的枚举值为:ProcessDebugPort(0x7)
、ProcessDebugObjectHandle(0x1E)
、ProcessDebugFlags(0x1F)
。
ProcessDebugPort利用
利用方法:进程处于调试状态时,系统就会为它分配1个调试端口(Debug Port)。ProcessInformationClass
参数的值设置为ProcessDebugPort(0x7)
时,调用NtQueryInformationProcess
就能获取调试端口。若进程处于非调试状态,则变量dwDebugPort
的值设置为0;若进程处于调试状态,则变量dwDebugPort
的值设置为0xFFFFFFFF
。如下代码所示:
有更简单的方法,CheckRemoteDebuggerPresent
API调用了NtQueryInformationProcess(ProcessDebugPort)
API,因此直接调用CheckRemoteDebuggerPresent
即可。
ProcessDebugObjectHandle利用
利用方法:设置参数为ProcessDebugObjectHandle
时,会获得调试对象的句柄,如果进程处理与调试状态,句柄就存在,反之就不存在。代码如下所示:
ProcessDebugFlags利用
利用方法:设置参数为ProcessDebugFlags
时,返回值若为0,则处于调试状态,否则不处于调试状态。代码略。
注:在WIN11中,上述三种利用都可以正常进行。
如何破解反调试,破解上述3种利用?若只是调用几次API,则可以在调试器中手动操作输出值。相反,若函数被反复调用,则需要使用API钩取技术。
在下面的练习中,我们使用ollyDbg的汇编命令手动设置钩取代码。
1.确定钩取函数位置。将钩取代码设置在代码节区中最后一个Null padding的位置,本例子中为0x407E00
。
2.修改要钩取的API的代码。我们要修改NtQuerylnformationProcess
的代码,源代码如下:
修改后代码如下:
注:修改的JMP指令长度为5字节,正好是CALL EDX
与RETN 14
的长度。钩取API时,一般要在原 API起始地址处设置JMP 指令,但是这里却将JMP命令设置在略微偏下的地址,这是为了回避某些PE保护器的API钩取探测功能。有的PE保护器会检测NtQueryInformationProcess
起始地址的第一个字节,若非B8
,则认为该API被钩取,就会执行某些非正常运行的行为(也算是一种调试器探测技术)。
3.编写钩取函数。
1 | CALL EDX |
首先,可以看到我们要写的代码在CALL EDX
与RETN 14
之间。注意PUSH EAX
,SS:[ESP+0xC]
指的是ProcessInformationClass
参数(第2个),而SS:[ESP+0x10]
指的是ProcessInformation
参数(第3个)。此程序功能为:ProcessInformationClass
参数(DWORD PTR SS:[ESP+C]
)值为0x7
、0x1E
、0x1F
之一时,则将ProcessInformation
参数(DWORDPTR SS:[ESP+10]
)地址所指的返回值分别修改为0
、0
、1
。
方法3:NtQuerySysteminformation
基于调试环境检测的反调试技术。之前的方法通过探测调试器来判断自己的进程是否处于被调试状态,非常直接。除此之外,还有间接探测调试器的方法,即:检测调试环境。
ntdll!NtQuerySystemInformation
是一个系统函数,用来获取当前运行的多种OS信息。其定义如下(看上去和NtQuerylnformationProcess
差不多,SystemInformationClass
是一个枚举类型,返回SystemInformation
。):
SystemKernelDebuggerlnformation(0x23)利用
SystemInformationClass
设置为SystemKernelDebuggerlnformation(0x23)
,即可判断OS是否在调试模式下运行。相关代码如下:
如何进行反调试破解?以正常模式启动OS,而不是以调试模式启动OS。
方法4:NtQueryObject
思想:系统中的某个调试器调试进程时,会创建1个调试对象类型的内核对象。检测该对象是否存在即可判断是否有进程正在被调试。
ntdll!NtQueryObject
用来获取系统各种内核对象的信息,其定义如下:
和之前的类似,ObjectInformationClass
是枚举类型,输出为ObjectInformation
。使用ObjectAllTypesInformation
作为ObjectInformationClass
的值,来获取系统所有对象信息,然后从中检测是否存在调试对象。由于输出的ObjectInformation
是一个数组,因此需要一个一个比较对象是否是调试对象。相关代码如下:
如何进行反调试破解?在调用ntdll!ZwQueryObject
的地方,将此时的栈中第2个参数ObjectAllTypesInformation
的值由0x03改为0x00即可。
方法5:ZwSetlnformationThread
此方法利用ZwSetlnformationThread
,可以强制分离(detach)被调试者与调试器,从而达到反调试的目的。其定义如下:
该函数拥有2个参数,第1个参数ThreadHandle
用来接收当前线程的句柄,第2个参数ThreadInformationClass
表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11)
,调用该函数后,调试进程就会被分离出来(其原理就是:将线程隐藏起来,调试器就接收不到信息,从而无法调试。)。ZwSetInformationThread
不会对正常运行的程序产生任何影响,但若运行的是调试器程序,调用该API将使调试器终止运行,同时终止自身进程。
如何进行反调试破解?在调用ZwSetInformationThread
之前将第2个参数ThreadInformationClass
由0x11变为0x00即可。也可以用API钩取自动化这些操作。
再介绍一个API:DebugActiveProcessStop
,它用来分离调试器和被调试进程,从而停止调试。而ZwSetInformationThread
则用来隐藏当前线程,使调试器无法再收到该线程的调试事件,最终停止调试。
TLS回调函数
TLS回调函数是程序最初运行的一些代码。在TLS中,经常使用某些反调试技术(例如IsDebuggerPresent
)来判断是否被调试。
ETC
反调试的目的:防止其他人来调试我们的程序,因此,我们就会有很多很多种方法。最常用的方法是:判断当前系统是否为逆向分析的系统,若是,则直接停止程序。这样,我们就能从系统中轻松获取各种信息(进程、文件、窗口、注册表、主机名、计算机名、用户名环境变量等),通过判断这些,来判断这是不是逆向分析的系统。
1 | 1. 检测OllyDbg窗口-FindWindow |
总结
1.反调试技术有很多,这里只是介绍了几种常见的。
2.反调试技术对OS有很强的依赖性。有的反调试方法对于不同的OS可能没有用。
3.使用调试器的插件,可以有效的绕过反调试技术。但是不是万能的。
0x02 动态反调试技术
动态反调试技术可以不断阻止对程序代码的跟踪调试,与静态反调试技术相比,动态反调试技术难度更高,破解难度更大。
利用异常来进行动态反调试
异常(Exception)常用于反调试技术。
SEH(实际用的非常多)
可以利用如下特性来进行反调试:正常运行的进程发生异常时,在SEH机制的作用下OS会接收异常,然后调用进程中注册的SEH处理。但是,若进程(被调试者)在调试运行中发生异常,调试器就会接收处理。
书中给出了一个DynAD_SEH.exe
的例子,源代码如下所示:
上图红框是安装SEH函数,上图绿框则是出发了异常。其代码执行流如下图:
可以看到,如果处于调试状态,则无法跳转到正常代码。直接jmp 0xFFFFFFFF
处。实际情况下,jmp 0xFFFFFFFF
可能是及其冗长的代码,会让我们头昏脑胀。
如果运行SEH,则程序跳转到0x40102C
,之后运行MOV EAX,DWORD PTR SS:[ARG.3]
,它会将pContext
放到EAX中,之后通过MOV DWORD PTR DS:[EAX+0xB8], EBX
修改pContext
中EIP
的值,最终通过XOR EAX, EAX
返回0,表示不用执行下一个SEH函数。之后,程序会运行到0x401040
,通过POP DWORD PTR FS:[0]
来删除SEH函数,接着输出No debugging
。
SetUnhandledExceptionFilter
进程中发生异常时,若SEH未处理或注册的SEH不存在,此时会调用执行系统的kernel32!UnhandledExceptionFilter
,该函数内部会运行系统的最后一个异常处理器(SEH函数):Top Level Exception Filter
或Last Exception Filter
。这个SEH函数通常会弹出消息框,然后中止进程的运行。
kernel32!UnhandledExceptionFilter
内部调用了ntdll!NtQueryInformationProcess
(静态反调试),以判断是否正在调试进程(在win7中没有发现这一点,但是确实检测了进程是否在调试中)。若进程正常运行,则运行系统最后的异常处理器;若进程处于调试中,则将异常发送给调试器。
通过kerel32!SetUnhandledExceptionFilter
可以修改系统最后的异常处理器,函数定义如下:
只要将新的Top Level Exception Filter
给这个函数就好,返回值是旧的Top Level Exception Filter
的地址。
如何使用它来进行反调试?先特意触发异常,然后在新注册的Top Level Exception Filter
内部判断进程正常运行还是调试运行,并根据判断结果修改EIP值。
跟着书进行实验,与书实验不同的是(P560),并没有发现ntdll!NtQueryInformationProcess
的调用,但是确实检测了进程是否被调试,如果被调试就把控制权交还给调试器,这样就会陷入一个循环。经过分析,发现下图红框位置检测了是否被调试。
只要将此时的EAX转为0,就可以正常运行到我们新注册的Top Level Exception Filter
,如下所示:
可以看到,首先0x401009
则是恢复了之前的UnhandledExceptionFilter
,之后0x40100F-0x401015
则是调用pContext
并将eip加4,最后返回EAX=0xFFFFFFFF
,代表程序运行到最后一个SEH。最终,程序将在报异常的下一条命令开始执行。
那么此程序的逻辑就是:
(1)若没有进行调试,会首先调用SetUnhandledExceptionFilter
设置UnhandledExceptionFilter
。那么运行到异常代码后,之后运行这个UnhandledExceptionFilter
,并直接运行到出错程序的下一条。
(2)若进行了调试,会首先调用SetUnhandledExceptionFilter
设置UnhandledExceptionFilter
。那么运行到异常代码后,会交给调试器,如果调试器忽略了,之后就会交给UnhandledExceptionFilter
。在运行UnhandledExceptionFilter
之前,程序先做一个静态判断,判断是否是调试器,如果是(需要手动修改),则控制权交还给调试器;如果不是,则运行UnhandledExceptionFilter
,并直接运行到出错程序的下一条。
如何破解这类反调试?这类反调试结合了动态反调试&静态反调试技术,只要使得程序能够运行到正常代码即可。
Timing check进行动态反调试
思想:在调试器中逐行跟踪程序代码比程序正常运行耗费的时间要多出很多。因此可通过计算运行时间的差异来判断进程是否处于被调试状态。由于在模拟器中运行速度也很慢,因此此技术也可以探测是否在模拟器中运行。
实际操作中,该反调试技术通常与其他反调试技术并用,导致反调试的破解过程变得困难。
由于要测量时间间隔,有两类方法:(1)利用CPU计数器计时(例如读取时间戳计数器RDTSC:Read Time Stamp Counter)。(2)利用系统实际时间计时(Time)。下面是两种方法分别对应的API:
注:计数器的准确程度由高到低排列如下:RDTSC > NtQueryPerformanceCounter > GetTickCount
。NtQueryPerformanceCounter
与GetTickCount
使用相同硬件,但二者准确程度不同。而RDTSC是CPU内部的计数器,其准确程度最高。
下面介绍基于RDTSC的反调试技术。
x86 CPU中存在一个名为TSC(Time Stamp Counter,时间戳计数器)的64位寄存器。CPU对每个Clock Cycle(时钟周期)计数,然后保存到TSC。RDTSC
是一条汇编指令,用来将TSC值读入EDX:EAX
寄存器。
随书示例中代码的汇编如下:
其中个人感觉MOV DWORD PTR SS:[EBP-4],EAX
作用不大。可以看到,上述代码,如果时间差值小于0xFFFFFF
,就会认为是正常程序(未进行调试)。
如何破解这类反调试?
1 | 1. 直接F9越过相关代码。 |
TF位进行动态反调试
EFLAGS的第9比特位为Trap Flag(TF),也叫陷阱位。TF值设置为1时,CPU将进入单步执行模式。单步执行模式中,CPU执行1条指令后即触发1个EXCEPTION_SINGLE _STEP
异常,然后TF会自动变为0。EXCEPTION_SINGLE_STEP
异常可以与SEH结合,用于探测调试器。
相关汇编代码如下:
解释如下:发生异常时,若程序进程非调试运行,则运行SEH执行正常代码。若程序进程处于调试中,则无法转到SEH,继续执行0x40102F
地址处的指令,然后执行0x401034
地址处的JMP EAX(0XFFFFFFF)
指令,进程非正常终止。程序的运行就像这样被分为正常运行与调试运行。
如何破解这类反调试?设置ollydbg忽略EXCEPTION_SINGLE_STEP
异常,将其交给被调试程序处理即可。
还有一种指令,INT 2D
也可以用于动态反调试。INT 2D
为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。但程序调试运行时不会触发异常,只是忽略。这种在调试与正常运行中不同的表现可以很好地应用于反调试技术。
INT 2D
指令有几个有趣的特征:
1 | 1. INT 2D 执行完后,之后的第一个字节将会被跳过(便于代码混淆)。然后顺序执行,而正常调试则是转到 SEH 函数 |
注:
如何使用
ollydbg
中调试到SEH
?设置options IgnoreEXCEPTION_SINGLE_STEP
异常。其次,对INT 2D
与SEH
函数打断点,之后,运行到INT 2D
时,将TF
位改为1,表示进入单步模式,然后再按F7,就会跳到SEH
函数的开头。(原因:猜测应该是ollydbg
的单步调试只是对下一条指令的开头设置断点所致。)软件断点 vs 硬件断点:(1)软件断点通过修改程序代码,而硬件断点是修改CPU的调试寄存器。(2)软件断点适用于在代码中设置断点,而硬件断点适用于在数据读写设置断点。(3)软件断点数目通常比硬件断点多,因为硬件断点的数量受CPU调试寄存器的限制,而软件断点则只受内存空间的限制。
0xCC探测
思想:软件断点对应的x86指令为0xcc
,若检测到该指令,则可以判断程序是否处于调试状态。(不太可靠,因为0xcc
可能是立即数,会有误报)
- 很多情况下,我们会在windows API的起始地址打断点,通过调用API时堆栈里存储的返回地址来确定调用API的代码,并进行分析。因此,只要探测windows API的起始地址,就可以判断是否在调试。如何破解?尽量不要在API开头设断点啦。
- 比较特定代码区域的校验和也是一种方法,用来检测代码是否被修改,从而判断代码是否被调试。如何破解?修改计算校验和的语句,或者修改比较的语句。
0x03 高级反调试技术
很多PE保护器使用了高级反调试技术,例如垃圾代码、条件分支、循环、加解密、复杂的调用树等。
垃圾代码
大量无意义的代码。
注:
XCHG
指令,交换两个操作数的值。HLT
指令,用于停止CPU的执行,将CPU进入休眠状态。当有中断或复位信号到来时,CPU会重新启动,并按照指定的程序继续执行。movzx
指令,用于将一个操作数的低位字节(8位或16位)复制到目标操作数的高位字节(16位或32位),并将目标操作数的其余位清零。
扰乱代码对齐
加密/解密
- 普通的加密解密。
- 代码重组。代码对之后的代码进行更改,如下所示:
如果在要被修改的代码上打断点(0xcc
),那么重组之后的代码可能就完全不一样,也避免了反调试。
Stolen Bytes (Remove OEP)
思想:将部分源代码(主要是OEP
)转移到保护器创建的内存区域运行。
此技术的优点:
1 | 1. 转储进程内存时,一部分OEP代码会被删除,转储的文件无法正常运行反转储技术。 |
示例:stolen_bytes.exe
,跟书进行操作。
首先,使用PESpin
保护器对stolen_bytes.exe
进行操作,开启Remove OEP
选项,得到stolen_bytes_pespin.exe
。但是它给的示例和书中不符。
API重定向
对于一些逆向分析人员,他们喜欢在API上打断点,此时堆栈中就存有返回地址,接下来只需要在返回地址快乐的调试就好了。API重定向则让他们不再快乐。具体而言,保护器这样做的:(1)将部分API代码复制到其他内存区域;(2)分析要保护的目标进程代码;(3)修改调用API的代码,来运行自己复制的API代码。这样,即使在原API
地址处设置断点也没用。
下面分析api_redirection1.exe
,但是一直达不到书中的效果。(运行不到解压缩阶段,太菜了呜呜呜,感觉也有一部分懒的因素)
再分析api_redirection2.exe
,放弃达到书中的效果,仅记录知识点:
对于
ASProtect
,每次调试时其代码形态均不同,因为ASProtect
的混淆代码生成器每次都会生成新的垃圾代码。我们把这种能产生相同结果而又具有不同形态的代码称为多态代码(Polymorphic Code)。ASProtect
并未改变API
的地址,没看出来API
重定向体现在哪里,感觉只是ASProtect
的保护流程很麻烦、很繁琐、很难进行破解。API
重定向技术在结构上与API
钩取技术有很多类似的地方:都不直接调用原API
,而是添加自身代码并执行后再调用(理解了ASProtect
的API
重定向体现在哪儿)。二者最大的不同在于,它们的目的是不一样的:API
重定向用来增加代码调试的难度,而API
钩取则用来在API
调用前/后添加另外的功能。
Debug Blocker(Self Debugging)
思想:在调试模式下运行自身进程。Debug Blocker
是自我创建技术(以子进程形式运行自身进程)的推广。自我创建技术中,子进程负责执行原代码,父进程负责创建子进程、修改内存(代码)数据 、更改 EP 地址等。所以仅调试父进程将无法转到OEP代码处,这样能起到很好的反调试效果。但调试时若用附加命令将子进程附加到调试器,这种反调试手法就会失去作用。Debug Blocker的出现弥补这一不足。
Debug Blocker
的优点:
1 | 1. 防止代码调试。子进程运行的原代码且已处于调试之中,原则上就无法再使用其他调试器进行附加操作。 |
常规反调试 vs Debug Blocker
常规反调试(例如SEH
反调试)中,SE Handler
代码位于相同的进程内存空间;但Debug Blocker技术中,SE Handler
代码位于调试进程。所以,为了调试子进程,必须先断开与已有调试器的连接,但这样子进程又无法正常运行。
Nanomite技术
在Debug Blocker
上发展而来,更牛逼的技术(PESpin
支持此技术)。
将所有条件跳转指令修改为INT3(0xCC)
指令。并且,调试器内部有表格,含有被修改的Jcc
指令的位置以及要跳转的地址。执行被调试者内部修改后的指令就会触发异常,控制权即被转交给调试器。调试器通过发生异常的地址从表格中获取要跳转的地址,然后通知被调试者。
0x04 调试练习1:服务
历时近3个月,终于到达最后一大章节,此书打算于2023.05.19
前看完。此书的知识丰富且不过时,我自觉没有全部掌握,比较惭愧。
留言
- 文章链接: https://wd-2711.tech/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!