第54章 调试练习1:服务
本文最后更新于:2022年5月27日 下午
第54章调试练习1:服务
从现在开始,我们将用多种示例程序练习调试,并通过这些调试练习进一步提高各位的调试水平。第一个调试示例是Windows服务程序,本章主要学习其调试方法。
服务程序比较难调试,有时,即使是逆向分析经验丰富的入调试起来也并非易事。本章主要学习服务程序的调试方法,帮助各位掌握。
54.1 服务进程的工作原理
服务(Service)程序由SCM(Service Control Manager,服务控制管理器)管理。运行服务程序时,需要由服务控制器(Service Controller)执行启动命令。服务控制器向SCM提出服务控制请求,SCM向服务程序传递控制命令,并接收其返回的值(参考图54-1)。
服务控制器无法直接向服务程序下达命令,必须通过SCM传达。
54.1.1 服务控制器
Windows默认提供了服务控制器,在“控制面板”中单击“管理工具”,打开“管理工具”窗口,双击“服务”图标即可运行服务控制器,如图54-3所示(也可以直接在控制台窗口输入 “services.msc” 命令)。
图54-3列出了设置在系统中的所有服务列表,在服务列表中选择想要控制的服务即可(启动/停止/暂停/重启)。选中指定服务,单击“启动服务”按钮,弹出“服务控制”窗口,显示正在启动指定服务信息,如图所示。
服务进程正常启动后,图54-4中的服务控制窗口消失,服务状态变为“已启动”。下面详细了解服务启动过程。
54.1.2服务启动过程
图54-5大致描述了服务程序的启动过程。
所有服务程序都是由外部(服务控制器)调用StartService() API启动的(参考:若服务为自启动服务,则由SCM调用StartService()启动)。
服务进程启动过程
(1)服务控制器调用StartService()
服务控制器调用StartService()时,SCM会创建相应服务进程,然后执行服务进程的EP代码。
(2)服务进程调用StartServiceCtrlDispatcher()
为了以服务形式运行,必须在服务进程内部调用StartServiceCtrlDispatcher() API来注册服务主函数SvcMain()的地址。调用StartServiceCtrlDispatcher()时,返回服务控制器的StartService()函 数。SCM调用服务进程的服务主函数SvcMain()。
(3)服务进程调用SetServiceStatus()
虽然已经创建了服务进程,但尚未以服务形式运行。当前状态仍为SERVICE_START_PENDING。在服务主函数SvcMain()内部调用SetServiceStatus(SERVICE_RUNNING)API后,才正式以服务进程形式运行(此时图54-4中的服务控制状态窗口消失)。
综上所述,服务程序先由SCM创建进程,然后控制转移给SvcMain()函数,调用SetServiceStatus
(SERVICE_RUNNING)API,这样才能以服务进程的形式运行。对服务进程尚不熟悉的朋友,请
认真阅读上面关于服务进程启动过程的描述。下面通过一个简单的示例程序(DebugMe1.exe)来
帮助各位进一步了解服务进程的工作原理。
示例程序在Windows XP/7(32位)中正常运行
54.2 DebugMe1 .exe 示例讲解
DebugMe1.exe进程以2种形式运行,一种为常规运行形式,负责服务的安装与删除;另一种
由SCM以服务形式运行。常规运行时需要接收运行参数(install或者uninstall),以服务形式运行
时则不需要。
54.2.1安装服务
为了将示例程序安装为服务,我们先将其复制到合适的文件夹,然后运行图54-6所示的命令。
服务安装成功后,可以在服务列表中看到,如图54-7所示。
从服务列表可以看到,服务名为SvcTest。选中它,在菜单栏中依次选择“操作-属性”菜单,打开属性窗口即可查看有关该服务的更多信息(也可以选择服务,在鼠标右键中选择“属性”菜单),如图54-8所示。
弹出服务属性对话框,如图54-9所示。
服务属性对话框的“常规”选项卡中包含了所有服务相关信息。SvcTest服务程序的可执行文件路径为:C:\Users\admin\Desktop\test\DebugMe1.exe(示例程序的安装路径),并且服务的“启动类型”为“手动”(需要运行时要手动启动),当前的“服务状态”为“已停止”。
服务启动类型大致可分为手动与自动这2种。为了方便,上述示例程序采用了手动启动方式。若启动类型为自动方式,则系统启动时即运行服务。
54.2.2 启动服务
下面开始启动服务,选中SvcTest服务,单击“启动服务”按钮,如图54-10所示。
服务成功启动后,服务进程(DebugMe1.exe)也就运行起来,如图54-11所示。
图54-11中需要注意的是,SvcTest服务的进程(DebugMe1.exe)是以services.exe进程的子进程(Child)形式运行的。其实,所有服务进程都以该形式运行。Services.exe进程就是SCM。示例程序DebugMe1.exe的功能相当简单,它经过一定时间间隔输岀调试字符串(调用OutputDebugString() API)。使用DebugView实用工具可以查看输出的调试字符串,如图54-12所示。
Windows Vista以上的系统中需要用管理员权限来运行DebugView,在菜单栏中依次选择Capture-Capture Global Win32菜单才能捕获输出的调试字符串,如图54-13所示。
54.2.3 源代码
下面看看DebugMe1.exe的源代码(DebugMe1.cpp)。
#main()
1 |
|
从main()函数代码可以看到,根据有无运行参数,程序可分别以服务模式(无参数)或常规模式(有参数)运行。以服务模式运行时会调用StartServiceCtrlDispatcher() API,启动服务主函数(SvcMain());以常规模式运行时,根据所给参数的种类,分别调用InstallService()/UninstallService()函数,它们分别用来安装或卸载服务(由于这2个函数比较简单,此处不再详细说明,请各位参考示例源码以及MSDN分析)。
#SvcMain()
1 |
|
以上是SvcMain()函数代码。先调用RegisterServiceCtrlHandler() API来注册服务处理函数(SvcCtrlHandler),然后调用SetServiceStatus() API将服务状态修改为SERVICE_RUNNING(此时图54-4中的服务控制窗口消失)。最后在while循环中每隔3秒调用1次OutputDebugString()函数,输出调试字符串。本示例服务(SvcTest)的功能非常简单,仅用来输岀调试字符串。
54.3 服务进程的调试
要想准确调试服务程序,就不能像对待普通程序一样直接用调试器启动并调试,而需要将调试器附加到SCM运行的服务进程上。这正是调试服务进程的困难之处,但理解了服务的工作原理后,解决起来就变得相当简单。
54.3.1 问题在于SCM
服务进程由SCM运行,这是服务进程调试的核心所在。
□服务进程由SCM运行。
□服务核心代码主要存在于服务主函数(SvcMain())中。
□服务主函数(SvcMain())由SCM正常调用。
我们要调试的是服务主函数(SvcMain()),但使用调试器打开服务程序的可执行文件并开始调试时,服务主函数并不运行,所以调试时需要将SCM运行的服务进程附加到调试器。
54.3.2 调试器无所不能
使用调试器打开服务可执行文件无法直接调试服务主函数(SvcMainO)代码。原因在于,SCM不会调用服务主函数(因非由SCM运行,故不能运行它)。但这并不意味着没有解决方法。因为调试器拥有被调试进程的强大权限,所以可以先将调试位置强制指定为服务主函数(如:OllyDbg的New origin here菜单),然后再调试。使用这种方法调试服务主函数不会有什么大问题,如果这种方法有效,建议各位使用。不过,使用这种方法必须拥有强大的调试器权限才行,服务进程行为比较复杂时,使用该方法就可能无法顺利完成调试。
54.3.3 常用方法
调试服务最常用的方法是,先将SCM运行的服务进程附加到调试器后再调试。思路很简单,
但执行方法可能有问题。因为SCM运行服务后再进行附加操作的话,此时的核心代码(服务主函
数)已开始运行。因此,需要在SCM创建服务进程并运行EP代码前附加到调试器,这需要一定
的调试技巧,后面的练习示例中将介绍。
54.4 服务调试练习
下面以DebugMe1.exe服务程序为例练习服务调试。
54.4.1 直接调试:强制设置EIP
首先,使用调试器直接打开服务程序,学习服务程序的调试方法。分析调试服务程序的EP代码与main()函数代码时,采用的调试方法与调试普通应用程序没有什么不同。但一般而言,服务程序的主要代码存在于服务主函数(SvcMain())与服务处理函数(SvcHandler())中。
由调试器而非SCM运行的服务进程不会调用SvcMain()与SvcHandler()函数。所以需要先得到这两个函数的地址,然后再将调试位置移动到那里。在OllyDbg调试器中打开DebugMe1.exe程序,调试运行到main()函数处显示代码,如图54-14所示。
在40106C地址处可以看到StartServiceCtrlDispatcher() API。对于EXE文件形态的Windows服务程序而言,必须在其EP代码内部调用StartServiceCtrlDispatcher() API,将服务主函数(SvcMain())的地址通知给SCM。所以,查找该API即可获得SvcMain()地址。
对于DLL文件形式的Windows服务而言,服务主函数(默认为ServiceMain)为导出函数,SCM会调用运行导出函数,所以不需要另外调用StartServiceCtrlDispatcher()API(若想把服务主函数的名称修改为其他名称(非ServiceMain),只要向相关注册表注册即可)。
StartServiceCtrIDispatcher() API 的 pServiceTable参数为 SERVICE_TABLE_ENTRY结构体指针。跟踪该结构体即可得到服务名称字符串(“SvcMain”)与服务主函数(SvcMain())的地址。
1 |
|
图54-14中,调试运行到40106C地址处的CALL DWORD PTR DS:[StartServiceCtrlDispatcher()]指令后,查看栈,如图54-15所示。
pServiceTable(12FD24)的第一个成员(40A9CC)为 “SvcHost” 字符串,第二个成员(401320)为SvcMain()函数的地址。
在图54-14中可以看到,从401038地址开始为设置SERVICE_TABLE_ENTRY结构体的代码。
使用OllyDbg调试器中的Ctrl+G命令,转到SvcMain()函数地址处(401320),SvcMain()函数如图54-17所示。
为了从401320地址开始调试,需要先将调试位置(准确地说,是被调试进程的EIP值)修改到此处。单击鼠标右键,在弹出菜单中选择New origin here选项,调试位置即被修改为服务主函数(401320),除EIP寄存器外,其他值(栈、除EIP外的寄存器)都保持不变。
现在开始调试SvcMain()即可。
采用以上方式调试服务主函数(SvcMain())时需要注意:由于服务进程不是由SCM 正常启动运行的,所以调用与服务相关的部分API时可能引发异常。比如,在图54-17中执行40132A 地址处的 CALL DWORD PTR DS:[RegisterServiceCtrlHandlerW]指令,就会发生EXCEPTION_ACCESS_VIOLATION(0xC0000005)异常。为了 避免这种异常,可以在调试器中强制跳过对相关API的调用,也可以像图54-20一样设置调试器选项。
若在OllyDbg调试器中复选Memory access violation项,就会忽略内存非法访问异常,调试可以继续。
以上方法虽然不是什么硬性规定,但大多数情况下用来调试服务主函数是不会有什么大问题的。当然,有时上面的方法也会失灵,无法正常帮助我们完成调试,此时可以尝试接下来要讲解的方法。
54.4.2 服务调试的常用方法:“附加”方式
根据不同情况,我们有时需要将SCM正式运行的服务进程附加到调试器调试。这一过程需要应用一些简单的调试技术。为了帮助各位更好地理解该过程,下面用图54-21简单描述调试技术的具体操作步骤(以调试EP代码为准)。
以上操作流程的核心是,将服务进程附加到调试器前要进入无限循环,使服务进程的重要代码无法运行。原理非常简单,但具体实施时要充分考虑Service Start Timeout(服务启动超时)这一因素,确保上述操作在规定时间内(默认为30秒)完成。
启动服务后,SCM会在一定时间内(Service Start Timeout)等待服务状态变为STATUS_RUNNING。若规定时间内服务状态未改变,SCM就会引发ERROR_SERVICE_REQUEST_TIMEOUT错误,然后终止相关服务进程。
也就是说,将服务进程附加到调试器后的30秒内,必须把服务进程的状态变更为STATUS_RUNNING。而要更改服务状态,必须调用位于服务主函数的SetServiceStatus() API。但30秒内完成以上操作流程相当困难,所以具体操作前需要增加服务启动超时时间。
(1)安装服务
首先将示例程序(DebugMe1.exe)安装为Windows服务(参考图54-6)。
(2)增加服务启动超时时间
运行注册表编辑器(regedit.exe),创建ServicesPipeTimeout注册表项(DWORD类型),如下所示(参考图54-22)。
注意注意,这里书中写的是ServicePipeTimeout,少了一个s,这样的话是不起作用的
[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control] ServicesPipeTimeout
ServicesPipeTimeout并不存在默认值,所以需要创建新值,单位为毫秒(Millisecond),这里
设置其值为60 x 60 x 24 x 1000=86400000(24小时),这个时间足够了。设置好后重启系统,使之
生效,然后就有足够时间调试了。
设置ServicesPipeTimeout值会对系统中的所有服务产生影响,尽量不要在重要的电脑中设置它,建议各位在调试专用电脑中设置。
(3)修改文件:设置无限循环
接下来,开始向服务可执行文件(EXE或者DLL)的EP地址覆写无限循环(Infinite Loop)代码。使用Stud_PE实用工具,查看DebugMe1.exe文件的EP地址(RVA/RAW),如图54-23所示。
EP的文件偏移(FileOffset=RAW)为C24。然后使用HxD实用工具转到该地址处,如图54-24所示。
原EP代码的前2个字节为0xEB、0xC0(希望各位记住)。在调试器中查看该位置,如图54-25所示。
0xEB、0xC0是CALL指令的一部分,把它们分别修改为0xEB、0xFE,如图54-26所示。
使用OllyDbg调试器查看修改后的EP代码,如图54-27所示。
虽然可以背诵0xEB、0xFE,但尽量理解其原理会比较好。从IA-32用户手册上可知,操作码0xEB是近距离(Short Distance)JMP指令,带有1个字节大小的值,该值为Signed Value(有符号数),指的是“与Next EIP的相对距离”,计算时有如下公式:
Jump Address=Next EIP(401826)+0xFE(-2)=401824
许多JMP/CALL指令都使用上面这样的“相对距离”,请各位熟记上述计算方法。关于IA-32指令的详细说明请参考第49章。
(4)启动服务
启动SvcTest服务(参考图54-10)。使用Process Explorer查看SvcTest服务进(DebugMe1.exe),
可以发现服务进程陷入无限循环,CPU占有率升至近100%,如图54-28所示。
实际测试被限定在了50%
若系统为Windows 7,启动服务进程时会弹出警告信息框,如图54-29所示。
错误1503就是ERROR_SERVICE_REQUEST_TIMEOUT错误,因为终止的不是服务进程,所以可以继续。
- 若向注册表添加ServicesTimeOut之前出现上述错误,服务进程就会终止执行。
- 在Windows XP系统下向注册表添加ServicesTimeOut后,不会出现上述信息框。
(5)附加至调试器
在OllyDbg调试器的菜单栏中依次选择File-Attach菜单,选择DebugMe1.exe进程,将其附加到调试器后,调试器在系统库区域(ntdll.DbgBreakPoint)暂停,如图54-31所示。
使用 Ollydbg2.0 后 暂停的地方是死循环的地点,即原本的EntryPoint
(6)修改进程:删除无限循环
使用Ctrl+G命令转到DebugMe1.exe进程的EP地址处(VA: 401824)(参考图54-23、图54-25、 图54-27)。
先按F2键在EP地址处设置断点,然后按F9键运行,如图54-32所示,控制在EP地址处停下来。 在401824地址处使用OllyDbg的编辑功能(Ctrl+E),将指令恢复为原来的指令代码(0xE8、0xC0),如图54-33所示。
从图54-34中可以看到,指令代码已经被修改为原来的指令代码。
接下来只要调试目标代码就可以了。
图54-34的状态并非服务正常运行的状态,需要在服务主函数中调用SetServiceStatus() API将服务状态更改为 SERVICE_RUNNING状态才行。
在图54-35中继续调试,调用完401357地址处的SetServiceStatus() API后,SvcTest服务进程(DebugMe1.exe)的状态就变为“启动”状态,如图54-36所示。
54.5小结
本章讲解了服务进程的工作原理及调试方法。虽然没有什么特别难的内容,但如果不理解服务工作原理,就无法准确调试服务主函数。因此,不要只是背下服务进程的调试技术,而要把学习重点放在理解其工作原理上。