re-core-principle-6
逆向工程核心原理笔记-6
此章节就是给多种示例程序,进行调试练习。
0x00 windows 服务程序的调试
服务(Service)程序由SCM(Service Control Manager,服务控制管理器)管理。运行服务程序时,需要由控制器(Service Controller)执行启动命令。控制器向SCM提出服务控制请求,SCM向服务程序传递控制命令,并接收其返回的值。如下图所示:

注意:控制器无法直接向服务程序下达命令,必须通过 SCM 传达。下图是windows的服务控制器:

服务启动过程

 所有服务程序都是由服务控制器调用SartService() API启动的,若服务为自启动服务,则由SCM调用StartService启动。
启动过程如下:
1  | (1)服务控制器调用StartService  | 
DebugMe1.exe 讲解
 DebugMe1.exe进程以2种形式运行,一种为常规运行形式,负责服务的安装与删除;另一种由SCM以服务形式运行。执行完DebugMe1.exe install后发现存在一个SvcTest服务。之后启动服务,发现服务进程DebugMe1.exe开始运行。注意到:SvcTest服务的进程(DebugMe1.exe)是以services.exe进程的子进程形式运行的。所有服务进程都以services.exe进程的子进程形式运行。Services.exe进程就是SCM。
 DebugMe1.exe的作用是:经过一定时间间隔输出调试字符串。源代码&注释如下所示:
1  | 
  | 
补充:
1  | Windows 服务:扩展服务和标准服务  | 
服务进程调试
服务程序的调试与普通程序不同,我们需要将调试器附加到SCM运行的服务进程上。
 为什么?(1)服务进程由SCM运行;(2)服务核心代码主要存在于服务主函数(SvcMain)中;(3)服务主函数(SvcMain)由SCM正常调用。我们要调试的是服务主函数(SvcMain),但使用调试器打开服务程序的可执行文件并开始调试时,服务主函数并不运行,所以调试时需要将SCM运行的服务进程附加到调试器。由于此时的服务主函数(SvcMain)可能已开始运行。因此,需要在SCM创建服务进程并运行EP代码前附加到调试器(具体怎么说后面说)。服务程序的主要代码存在于服务主函数(SvcMain)与服务处理函数(SvcHandler)中。
 另一种解决方法:将调试位置强制指定为服务主函数(SvcMain)(如:OllyDbg的New orign here),然后再调试。
调试位置强制指定
跟着书做就可以。
 对于EXE文件形态的Windows服务程序而言,必须在其EP代码内部调用StartServiceCtriDispatcher,将服务函数(SvcMain)的地址通知给SCM。对于DLL文件形式的 Windows 服务而言,服务主函数(SvcMain)为导出函数,SCM会调用运行导出函数,所以不需要另外调用StartServiceCtrlDispatcher。
需要注意,由于服务进程不是由 SCM正常启动运行的,所以调用与服务相关的部分 API时可能引发异常。为了避免这种异常可以设置调试器选项,从而忽略某些异常。
将调试器附加到SCM运行的服务进程
跟着书做就可以。
 下图是调试流程图,其中EP代码指的是SvcMain:

 以上操作流程的核心是,将服务进程附加到调试器前要进入无限循环,使服务进程的重要代码无法运行。但是,有超时时间。启动服务后,SCM会在一定时间内等待服务状态变为STATUS_RUNNING。若规定时间(一般是30s)内服务状态未改变,SCM就会引发ERROR_SERVICE_REQUEST_TIMEOUT错误,然后终止相关服务进程。
先安装服务程序。
30s内我们要附加到调试器并恢复EP代码,时间不够用。所以把30s改了。
 改完之后,开始设置无限循环。0xEB 0xFE是无限循环指令,为什么?操作码0xEB是近距离JMP 指令,带有1个字节大小的值,该值为有符号数,指的是与Next EIP 的相对距离,计算时有如下公式:
1  | JumpAddress = Next_EIP + 0xFE(-2)  | 
启动服务程序。SCM会主动找我们改过的服务程序,并陷入循环。我的win7出现了书中的情况,就算改了30s,系统也会自动杀掉,所以没有实际跟进后续实验流程。
0x01 自我创建程序的调试
有些应用程序在运行过程中可以将自身创建为子进程运行,这种方式称为自我创建。相同的可执行文件在以父进程运行和以子进程运行时分别表现出不同的行为特征。
 实验DebugMe2.exe,父进程用来在控制台窗口中输出字符串,并运行子进程。而子进程在消息窗口中输出字符串。
工作原理

1  | (1)创建子进程。  | 
源代码&注释如下:
1  | 
  | 
调试
对于attach程序来说,由于子进程是调试中新创建的,所以首先要调试父进程,查看子进程的EP被修改为哪一地址。
 这要求我们要从启动时就开始调试,可以在EP地址处设置无限循环来解决。还有一种解决办法,JIT(Just-In-Time)调试法。
可不可以在子进程创建的瞬间用调试器调试?不可以,因为此时子进程是挂起的,调试器无法调试被挂起的进程。
可不可以直接用调试器attach子进程?应该可以,没试过。
JIT调试
运行中的进程发生异常时,OS会自动运行调试器,附加发生异常的进程。由于可以从异常发生的位置开始调试,所以采用这种方式很容易把握出现异常的原因。
之后按照书上P626之前来就行。
0x02 PE映像切换调试
先运行某个进程,然后将其虚拟内存中的PE映像切换为另一个PE映像,这称为PE映像切换。
工作原理
 PE映像切换:先以挂起模式运行某个进程(A.exe),然后将完全不同的一个PE文件(B.exe)的PE映像映射到A.exe进程内存空间,并在A.exe进程的内存空间中运行。修改PE映像后,进程名称仍为原来的A.exe,但实际映射在进程内存中的PE映像为B.exe,所以最终会产生与原来 (A.exe)完全不同的行为动作。此时,A.exe为外壳进程,B.exe为内核进程。
补充:
1  | CUI:控制台程序  | 
 书中的实例中,给出了DebugMe3.exe、fake.exe、real.exe,fake.exe在命令行中输出字符串,real.exe在图形化窗口上输出字符串,运行DebugMe3.exe fake.exe real.exe,发现虽然运行着fake.exe,但是输出图形化窗口。
 大量的调试分析是逃不过的,这里为了加快速度,所以对于DebugMe3.exe的分析直接按书上的来了,但是要记住,这都是之前欠下的,以后还要还回去。
main函数的流程图如下:

 接下来分析SubFunc_1函数。经过分析,此函数是将real.exe读入内存,real.exe的内存地址叫做MEM_FILE_REAL_EXE。
 继续分析SubFunc_2函数。首先,此函数调用了GetThreadContext,获得了fake.exe的主线程上下文。之后,通过主线程上下文获得fake.exe的PEB,并调用ReadProcessMemory获得fake.exe的内存映射地址。代码如下所示:

注:由于当前fake.exe进程是以挂起模式创建的,处于暂停状态。进程被创建出来时,PE装载器就会将PEB结构体的地址设置给上下文中的EBX寄存器。
 接下来,获取real.exe文件的ImageBase地址。如下所示:


 EDI寄存器的值为MEM_FILE_REAL_EXE地址(real.exe的内存地址)。所以EDI+3C指的就是IMAGE_DOS_HEADER结构体的elfanew成员(NT头的偏移)。之后,EAX+EDI=elfanew+StartofPE是IMAGE_NT_HEADER结构体的起始地址,EAX+EDI+34指的是IMAGE_OPTION_HEADER.ImageBase成员。
 之后,比较fake.exe进程的实际映射地址与real.exe文件的ImageBase值。若两值相同,由于fake.exe的PE映像已经映射到某地址处,而此地址也是real.exe的PE映像要映射的地址。若将real.exe强行映射到该地址处,就会发生冲突,所以必须先卸载fake.exe的PE映像的映射。由于fake.exe进程处于挂起状态,所以卸载PE映像时不会发生错误。使用ZwUnmapViewOfSection来卸载,如下所示:

 若两值不同,不必非得卸载fake.exe的PE映像,可以先在fake.exe进程的虚拟内存空间(4G内存)中为real.exe的PE映像分配所需空间,然后将real.exe映射进去就可以了。接下来还要告知PE装载器,fake.exe进程的PE映像是real.exe的ImageBase地址。如下所示,调用WriteProcessMemory,将fake.exe进程的PEB.ImageBase值修改为real.exe文件的ImageBase值:

 至此,SubFunc_2分析完毕。
 接下来分析SubFunc_3函数。此函数负责把real.exe文件映射到fake.exe进程。首先,使用VirtualAllocEx,为real.exe的PE映像分配内存(之前读取的是real.exe文件,而不是映像)(分配的内存是real.exe的ImageBase地址)。其次,将real.exe映射到fake.exe进程:(1)映射PE文件头。具体来说,就是调用WriteProcessMemory,将real.exe的PE文件头写入到刚刚分配的内存区域。(2)映射PE节区。反复调用WriteProcessMemory,映射PE节区。循环结束后,real.exe文件被完全映射至real.exe的ImageBase。(3)修改EP。获取fake.exe的主线程上下文后,如下所示:

 如上图所示,eax存的值是fake.exe原来的EP地址,eip存的值为ntdll!RtlUserThreadStart的起始地址,因此,我们可以总结程序挂起后重新运行之后做的事:处于挂起状态的fake.exe进程恢复运行后,首先会调用ntdll!RtlUserThreadStart,跳转至EP地址处(Eax)。由于前面已经将fake.exe进程的PE映像替换为real.exe,所以需要将Eax修改为real.exe的EP地址(使用SetThreadContext)。最后,调用ResumeThread恢复进程。
总结一下流程:

调试
 如何调试real.exe呢?
 将real.exe文件映射到fake.exe进程前,向real.exe的EP代码设置无限循环。此时,借助调试器的附加功能将其附加到调试器即可调试。
0x03 Debug Blocker调试
 Debug Blocker是一种反调试技术,指的是进程以调试模式运行自身或其他可执行文件的技术。父进程是调试器,子进程是被调试者,且程序在作为调试器与被调试者时分别执行不同的代码。
注:使用CreateProcess创建进程时,若选用了DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS参数,则创建出的父子进程会形成调试器与被调试者的关系。
Debug Blocker特征
调试器与被调试者关系中,调试进程与被调试进程是一种父子关系。
被调试进程不能被其他调试器调试。若想调试被调试进程,必须先切断原调试器与被调试者的关系。
终止调试进程的同时也终止被调试进程。当强制终止调试进程以切断调试器-被调试者关系时,被调试进程也会同时终止。
调试器操作被调试者的代码。调试器用来操纵被调试进程的运行分支,生成或修改执行代码等。因此,缺少调试进程的前提下,仅凭被调试进程无法正常运行。
调试器处理被调试进程中发生的异常。被调试进程中发生异常时,进程会暂停,控制权转移到调试进程。此时调试器可以修改被调试者的执行分支,此外也可以对被调试进程内部的加密代码解密,或者向寄存器、栈中存入某些特定值等。
调试
调试某个程序,程序逻辑是:父进程会在控制台窗口输出字符串,而子进程会在消息框输出字符串。
 程序首先使用CreateMutexW创建互斥体对象,之后调用GetLastError,并于0xB7比较,如果相同,则说明以子进程运行,否则程序以父进程运行。父进程调用CreateProcessW,该函数使用DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS参数(调试模式)创建自身进程。以Debug模式运行后,父进程为调试器,子进程为被调试者。
 Debug Blocker的程序的核心代码一般都在子进程中运行。我们在0xB7比较阶段更改分支,进入子进程。子程序第一行代码为lea eax, eax,由于lea的第2操作数一般为内存,所以会报非法指令异常。这样就会把控制权交给父进程,获得控制权后,父进程会处理子进程中发生的异常,同时还会做一些其他事情。我们可以猜想:父进程(调试器)获取控制权后可能会使用这些指令对加密的代码解密,或者将执行分支修改为其他地址。到这里,我们已经不能继续采用这种方式调试了。要继续调试程序,必须详细分析父进程(调试器)的运行代码。
 接下来继续调试父进程。发现父进程中调用了WaitForDebugEvent,等待被调试进程发生Debug事件。WaitForDebugEvent如下:

 若调用WaitForDebugEvent,将在wMilliseconds时间内等待被调试进程发生Debug。被调试进程发生异常时将返回WaitForDebugEvent,且相关异常信息就会填充到IpDebugEvent指针所指的DEBUG_EVENT结构体。此结构体定义如下:

然后会比较Debug码与1(Exception)的关系,我们只需要关注等于1的时候即可。具体过程见书中,比较复杂,笔记中并未记全。
补充:条件记录断点(Conditional Log Break Point:CLBP),满足某个条件,记录到日志中。
总结一下这个程序的执行流程:(子进程的两次指令异常,此时交还给父进程处理)


上述程序调试需要多次调试,子进程与父进程都调试。那如果我们需要直接调试它的子进程(被调试进程),该如何处理呢?
方法1:静态。详细分析调试过程,得到解码代码,然后直接修改程序的PE文件或进程内存,从而达到用OllyDbg调试器调试的目的。
方法2:动态。(1)使用OllyDbg调试父进程,调试运行到要分析的地方(或者子进程代码完成解码的地方);(2)在子进程中要分析的代码处设置无限循环;(3)将父进程从子进程中分离(Detach);(4)附加OllyDbg调试器到子进程。具体看书中P669中的内容。
我不是什么天才,但只要不断努力,照样能成为代码逆向分析高手。
送给自己。
留言
- 文章链接: https://wd-2711.tech/
 - 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!