逆向工程核心原理笔记-5

0x00 反调试技术

 反调试技术对调试器与OS有着很强的依赖性(Dependency)。即,有些反调试技术仅能在特定版本OS下正常工作,而且不同种类调试器应用的反调试技术也略有不同。本书主要介绍针对XP与WIN7的反调试。

 反调试技术分为动态与静态。运用静态技术时,只要在开始破解1次,即可解除全部反调试限制。而运用动态技术时,要一边调试一边破解。因此,动态反调试技术使得破解更困难。如下表所示:

image-20230504120905116

静态反调试技术主要用来探测调试器,如果探测到,那么程序就无法正常运行。动态反调试会扰乱调试器跟踪(类似于逐行调试)的功能,使我们无法查看程序中的代码与数据。

0x01 静态反调试技术

方法1:PEB

 PEB结构体中有一些成员与反调试技术密切相关,如下所示:

image-20230504123014338

1
2
3
4
5
6
7
8
9
10
1. BeingDebugged
利用方法:IsDebuggerPresent API获取 PEB.BeingDebugged 的值来判断进程是否处于被调试状态。
2. Ldr
成员作用:是一个指向_PEB_LDR_DATA结构的指针,存储当前进程加载的所有模块的信息,包括模块的基地址、入口点、导入表、导出表等。
利用方法:调试进程时,堆内存会出现很多0xFEEEFEEE,表示未使用过的堆内存。而_PEB_LDR_DATA是在堆内存中创建的,所以查看PEB.Ldr,看里面是否有0xFEEEFEEE。若有,则代表进程正在被调试。
3. ProcessHeap
成员作用:指向HEAP结构体
利用方法:HEAP结构体中由Flags(+0xC)与ForceFlags(+0x10)两个成员,当被调试时,这两个成员被设置为特定的值(正常情况下Flags为0x2,ForceFlags为0x0,仅在XP中有效)。Heap成员可以直接从PEB中获取,也可以通过GetProcessHeap() API获取。
4. NtGlobalFlag
利用方法:调试进程时,PEB.NtGlobalFlag会设置为0x70,正常应该是0。(然而如果是attach进程则此方法不管用)

注:经过实验,在WIN11中,IsDebuggerPresentNtGlobalFlag是有用的,而LdrProcessHeap是没用的。

方法2:NtQuerylnformationProcess

 通过NtQuerylnformationProcessAPI可以获得与进程调试相关的信息,函数定义如下所示:

image-20230504132453270

 其中,ProcessInformation输出人们想要的信息,而ProcessInformationClass是一个枚举类型,来向函数说明自己想要哪些信息。ProcessInformationClass中与调试相关的枚举值为:ProcessDebugPort(0x7)ProcessDebugObjectHandle(0x1E)ProcessDebugFlags(0x1F)

ProcessDebugPort利用

 利用方法:进程处于调试状态时,系统就会为它分配1个调试端口(Debug Port)。ProcessInformationClass参数的值设置为ProcessDebugPort(0x7)时,调用NtQueryInformationProcess就能获取调试端口。若进程处于非调试状态,则变量dwDebugPort的值设置为0;若进程处于调试状态,则变量dwDebugPort的值设置为0xFFFFFFFF。如下代码所示:

image-20230504132950468

 有更简单的方法,CheckRemoteDebuggerPresent API调用了NtQueryInformationProcess(ProcessDebugPort) API,因此直接调用CheckRemoteDebuggerPresent即可。

ProcessDebugObjectHandle利用

 利用方法:设置参数为ProcessDebugObjectHandle时,会获得调试对象的句柄,如果进程处理与调试状态,句柄就存在,反之就不存在。代码如下所示:

image-20230504133328581

ProcessDebugFlags利用

 利用方法:设置参数为ProcessDebugFlags时,返回值若为0,则处于调试状态,否则不处于调试状态。代码略。

注:在WIN11中,上述三种利用都可以正常进行。

 如何破解反调试,破解上述3种利用?若只是调用几次API,则可以在调试器中手动操作输出值。相反,若函数被反复调用,则需要使用API钩取技术。

 在下面的练习中,我们使用ollyDbg的汇编命令手动设置钩取代码

1.确定钩取函数位置。将钩取代码设置在代码节区中最后一个Null padding的位置,本例子中为0x407E00

2.修改要钩取的API的代码。我们要修改NtQuerylnformationProcess的代码,源代码如下:

image-20230504134723177

 修改后代码如下:

image-20230504135407416

 注:修改的JMP指令长度为5字节,正好是CALL EDXRETN 14的长度。钩取API时,一般要在原 API起始地址处设置JMP 指令,但是这里却将JMP命令设置在略微偏下的地址,这是为了回避某些PE保护器的API钩取探测功能。有的PE保护器会检测NtQueryInformationProcess起始地址的第一个字节,若非B8,则认为该API被钩取,就会执行某些非正常运行的行为(也算是一种调试器探测技术)。

3.编写钩取函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CALL EDX
PUSH EAX
CMP DWORD PTR SS:[ESP+C], 7
JNZ SHORT 0x407E16
MOV EAX, DWORD PTR SS:[ESP+10]
MOV DWORD PTR DS:[EAX], 0
JMP SHORT 0x407E3A
CMP DWORD PTR SS:[ESP+C], 1E
JNZ SHORT 0x407E29
MOV EAX, DWORD PTR SS:[ESP+10]
MOV DWORD PTR DS:[EAX], 0
JMP SHORT 0x407E3A
CMP DWORD PTR SS:[ESP+C], 1F
JMP SHORT 0x407E3A
MOV EAX, DWORD PTR SS:[ESP+10]
MOV DWORD PTR DS:[EAX], 0
POP EAX
RETN 14

image-20230504142225344

 首先,可以看到我们要写的代码在CALL EDXRETN 14之间。注意PUSH EAXSS:[ESP+0xC]指的是ProcessInformationClass参数(第2个),而SS:[ESP+0x10]指的是ProcessInformation参数(第3个)。此程序功能为:ProcessInformationClass参数(DWORD PTR SS:[ESP+C])值为0x70x1E0x1F之一时,则将ProcessInformation参数(DWORDPTR SS:[ESP+10])地址所指的返回值分别修改为001

方法3:NtQuerySysteminformation

 基于调试环境检测的反调试技术。之前的方法通过探测调试器来判断自己的进程是否处于被调试状态,非常直接。除此之外,还有间接探测调试器的方法,即:检测调试环境

ntdll!NtQuerySystemInformation是一个系统函数,用来获取当前运行的多种OS信息。其定义如下(看上去和NtQuerylnformationProcess差不多,SystemInformationClass是一个枚举类型,返回SystemInformation。):

image-20230504150139916

SystemKernelDebuggerlnformation(0x23)利用

SystemInformationClass设置为SystemKernelDebuggerlnformation(0x23),即可判断OS是否在调试模式下运行。相关代码如下:

image-20230504150521370

image-20230504150533460

 如何进行反调试破解?以正常模式启动OS,而不是以调试模式启动OS。

方法4:NtQueryObject

 思想:系统中的某个调试器调试进程时,会创建1个调试对象类型的内核对象。检测该对象是否存在即可判断是否有进程正在被调试。

ntdll!NtQueryObject用来获取系统各种内核对象的信息,其定义如下:

image-20230504151115591

image-20230504151126032

 和之前的类似,ObjectInformationClass是枚举类型,输出为ObjectInformation使用ObjectAllTypesInformation作为ObjectInformationClass的值,来获取系统所有对象信息,然后从中检测是否存在调试对象。由于输出的ObjectInformation是一个数组,因此需要一个一个比较对象是否是调试对象。相关代码如下:

image-20230504152215260

image-20230504152229035

 如何进行反调试破解?在调用ntdll!ZwQueryObject的地方,将此时的栈中第2个参数ObjectAllTypesInformation的值由0x03改为0x00即可。

方法5:ZwSetlnformationThread

 此方法利用ZwSetlnformationThread,可以强制分离(detach)被调试者与调试器,从而达到反调试的目的。其定义如下:

image-20230504153004946

 该函数拥有2个参数,第1个参数ThreadHandle用来接收当前线程的句柄,第2个参数ThreadInformationClass表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11),调用该函数后,调试进程就会被分离出来(其原理就是:将线程隐藏起来,调试器就接收不到信息,从而无法调试。)。ZwSetInformationThread不会对正常运行的程序产生任何影响,但若运行的是调试器程序,调用该API将使调试器终止运行,同时终止自身进程。

 如何进行反调试破解?在调用ZwSetInformationThread之前将第2个参数ThreadInformationClass由0x11变为0x00即可。也可以用API钩取自动化这些操作。

 再介绍一个API:DebugActiveProcessStop,它用来分离调试器和被调试进程,从而停止调试。而ZwSetInformationThread则用来隐藏当前线程,使调试器无法再收到该线程的调试事件,最终停止调试。

TLS回调函数

 TLS回调函数是程序最初运行的一些代码。在TLS中,经常使用某些反调试技术(例如IsDebuggerPresent)来判断是否被调试。

ETC

 反调试的目的:防止其他人来调试我们的程序,因此,我们就会有很多很多种方法。最常用的方法是:判断当前系统是否为逆向分析的系统,若是,则直接停止程序。这样,我们就能从系统中轻松获取各种信息(进程、文件、窗口、注册表、主机名、计算机名、用户名环境变量等),通过判断这些,来判断这是不是逆向分析的系统。

1
2
3
4
5
1. 检测OllyDbg窗口-FindWindow
2. 检测OllyDbg进程-CreateToolhelp32Snapshot
3. 检查计算机名称是否为TEST、ANALYSIS等-GetComputerName
4. 检查程序运行路径中是否存在TEST、SAMPLE等名称-GetCommandLine。
5. 检测虚拟机是否处于运行状态(查看虚拟机特有的进程名称-VMWareService.exe、VMWareTray.exe、VMWareUser.exe等)。

总结

1.反调试技术有很多,这里只是介绍了几种常见的。

2.反调试技术对OS有很强的依赖性。有的反调试方法对于不同的OS可能没有用。

3.使用调试器的插件,可以有效的绕过反调试技术。但是不是万能的。

0x02 动态反调试技术

 动态反调试技术可以不断阻止对程序代码的跟踪调试,与静态反调试技术相比,动态反调试技术难度更高,破解难度更大。

利用异常来进行动态反调试

异常(Exception)常用于反调试技术。

SEH(实际用的非常多)

 可以利用如下特性来进行反调试:正常运行的进程发生异常时,在SEH机制的作用下OS会接收异常,然后调用进程中注册的SEH处理。但是,若进程(被调试者)在调试运行中发生异常,调试器就会接收处理。

 书中给出了一个DynAD_SEH.exe的例子,源代码如下所示:

image-20230504162252436

 上图红框是安装SEH函数,上图绿框则是出发了异常。其代码执行流如下图:

image-20230504162339441

 可以看到,如果处于调试状态,则无法跳转到正常代码。直接jmp 0xFFFFFFFF处。实际情况下,jmp 0xFFFFFFFF可能是及其冗长的代码,会让我们头昏脑胀。

 如果运行SEH,则程序跳转到0x40102C,之后运行MOV EAX,DWORD PTR SS:[ARG.3],它会将pContext放到EAX中,之后通过MOV DWORD PTR DS:[EAX+0xB8], EBX修改pContextEIP的值,最终通过XOR EAX, EAX返回0,表示不用执行下一个SEH函数。之后,程序会运行到0x401040,通过POP DWORD PTR FS:[0]来删除SEH函数,接着输出No debugging

SetUnhandledExceptionFilter

 进程中发生异常时,若SEH未处理或注册的SEH不存在,此时会调用执行系统的kernel32!UnhandledExceptionFilter,该函数内部会运行系统的最后一个异常处理器(SEH函数):Top Level Exception FilterLast Exception Filter。这个SEH函数通常会弹出消息框,然后中止进程的运行。

kernel32!UnhandledExceptionFilter内部调用了ntdll!NtQueryInformationProcess(静态反调试),以判断是否正在调试进程(在win7中没有发现这一点,但是确实检测了进程是否在调试中)。若进程正常运行,则运行系统最后的异常处理器;若进程处于调试中,则将异常发送给调试器。

 通过kerel32!SetUnhandledExceptionFilter可以修改系统最后的异常处理器,函数定义如下:

image-20230504164236213

 只要将新的Top Level Exception Filter给这个函数就好,返回值是旧的Top Level Exception Filter的地址。

如何使用它来进行反调试?先特意触发异常,然后在新注册的Top Level Exception Filter内部判断进程正常运行还是调试运行,并根据判断结果修改EIP值。

 跟着书进行实验,与书实验不同的是(P560),并没有发现ntdll!NtQueryInformationProcess的调用,但是确实检测了进程是否被调试,如果被调试就把控制权交还给调试器,这样就会陷入一个循环。经过分析,发现下图红框位置检测了是否被调试。

image-20230504171837614

 只要将此时的EAX转为0,就可以正常运行到我们新注册的Top Level Exception Filter,如下所示:

image-20230504171959188

 可以看到,首先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:

image-20230504183512625

注:计数器的准确程度由高到低排列如下:RDTSC > NtQueryPerformanceCounter > GetTickCountNtQueryPerformanceCounterGetTickCount使用相同硬件,但二者准确程度不同。而RDTSC是CPU内部的计数器,其准确程度最高。

 下面介绍基于RDTSC的反调试技术。

x86 CPU中存在一个名为TSC(Time Stamp Counter,时间戳计数器)的64位寄存器。CPU对每个Clock Cycle(时钟周期)计数,然后保存到TSC。RDTSC是一条汇编指令,用来将TSC值读入EDX:EAX寄存器。

 随书示例中代码的汇编如下:

image-20230504184455594

 其中个人感觉MOV DWORD PTR SS:[EBP-4],EAX作用不大。可以看到,上述代码,如果时间差值小于0xFFFFFF,就会认为是正常程序(未进行调试)。

如何破解这类反调试?

1
2
3
4
1. 直接F9越过相关代码。
2. 操作第2个RDTSC的结果值,使之与第1个结果值相同。
3. 操纵条件分支指令,直接强制修改Flags的值。
4. 利用内核模式驱动程序使RDTSC失效,但是不知道原理。(OllyAdvanced PlugIn就用了此方法)

TF位进行动态反调试

 EFLAGS的第9比特位为Trap Flag(TF),也叫陷阱位。TF值设置为1时,CPU将进入单步执行模式。单步执行模式中,CPU执行1条指令后即触发1个EXCEPTION_SINGLE _STEP异常,然后TF会自动变为0。EXCEPTION_SINGLE_STEP异常可以与SEH结合,用于探测调试器。

 相关汇编代码如下:

image-20230504190048315

 解释如下:发生异常时,若程序进程非调试运行,则运行SEH执行正常代码。若程序进程处于调试中,则无法转到SEH,继续执行0x40102F地址处的指令,然后执行0x401034地址处的JMP EAX(0XFFFFFFF)指令,进程非正常终止。程序的运行就像这样被分为正常运行与调试运行。

如何破解这类反调试?设置ollydbg忽略EXCEPTION_SINGLE_STEP异常,将其交给被调试程序处理即可。

 还有一种指令,INT 2D也可以用于动态反调试。INT 2D为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。但程序调试运行时不会触发异常,只是忽略。这种在调试与正常运行中不同的表现可以很好地应用于反调试技术。

INT 2D指令有几个有趣的特征:

1
2
1. INT 2D 执行完后,之后的第一个字节将会被跳过(便于代码混淆)。然后顺序执行,而正常调试则是转到 SEH 函数
2. 在 ollydbg 中,执行完 INT 2D 之后,运行F8/F7会直接运行,而不会单步暂停

注:

  • 如何使用ollydbg中调试到SEH?设置options Ignore EXCEPTION_SINGLE_STEP异常。其次,对INT 2DSEH函数打断点,之后,运行到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位),并将目标操作数的其余位清零。

扰乱代码对齐

image-20230512181653909

加密/解密

  • 普通的加密解密。
  • 代码重组。代码对之后的代码进行更改,如下所示:

image-20230512185752973

 如果在要被修改的代码上打断点(0xcc),那么重组之后的代码可能就完全不一样,也避免了反调试。

Stolen Bytes (Remove OEP)

思想:将部分源代码(主要是OEP)转移到保护器创建的内存区域运行。

 此技术的优点:

1
2
1. 转储进程内存时,一部分OEP代码会被删除,转储的文件无法正常运行反转储技术。
2. 采用Stolen Bytes的文件再次经过保护器压缩后,会给逆向分析人员造成很大混乱。文件脱壳后,得到的不是熟悉的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,而是添加自身代码并执行后再调用(理解了ASProtectAPI重定向体现在哪儿)。二者最大的不同在于,它们的目的是不一样的:API重定向用来增加代码调试的难度,而API钩取则用来在API调用前/后添加另外的功能。

Debug Blocker(Self Debugging)

思想:在调试模式下运行自身进程。Debug Blocker是自我创建技术(以子进程形式运行自身进程)的推广。自我创建技术中,子进程负责执行原代码,父进程负责创建子进程、修改内存(代码)数据 、更改 EP 地址等。所以仅调试父进程将无法转到OEP代码处,这样能起到很好的反调试效果。但调试时若用附加命令将子进程附加到调试器,这种反调试手法就会失去作用。Debug Blocker的出现弥补这一不足。

Debug Blocker的优点:

1
2
1. 防止代码调试。子进程运行的原代码且已处于调试之中,原则上就无法再使用其他调试器进行附加操作。
2. 能够控制子进程(Debuggee,被调试者)。调试器-被调试者关系中,调试器具有很大权限,可以处理被调试进程的异常、控制代码执行流程等。

常规反调试 vs Debug Blocker

image-20230512212059207

 常规反调试(例如SEH反调试)中,SE Handler代码位于相同的进程内存空间;但Debug Blocker技术中,SE Handler代码位于调试进程。所以,为了调试子进程,必须先断开与已有调试器的连接,但这样子进程又无法正常运行。

Nanomite技术

 在Debug Blocker上发展而来,更牛逼的技术(PESpin支持此技术)。

将所有条件跳转指令修改为INT3(0xCC)指令。并且,调试器内部有表格,含有被修改的Jcc指令的位置以及要跳转的地址。执行被调试者内部修改后的指令就会触发异常,控制权即被转交给调试器。调试器通过发生异常的地址从表格中获取要跳转的地址,然后通知被调试者。

0x04 调试练习1:服务

 历时近3个月,终于到达最后一大章节,此书打算于2023.05.19前看完。此书的知识丰富且不过时,我自觉没有全部掌握,比较惭愧。

留言

© 2024 wd-z711

⬆︎TOP