前言
- 原先调试一个windows进程,习惯在内核模式的windbg中切换到目标进程上下文,加载调试符号和下断点。但是这样只能调试已经启动的、运行中的进程。最近要调试某个svchost服务的入口(下文以TermService服务为例),也就是需要调试该服务的
ServiceMain
函数,需要能在这个函数或者在该函数执行之前通过调试器中断下来。 - 经过一番网上冲浪,得到如下两种方法:
- 利用
Image File Execution Options
键,使用调试器启动服务,这个算比较正经的途径。在《使用Windbg&OllyDbg从头调试windows服务》这篇文章中提到结合桌面交互检测服务(Interactive Services Detection)进行本地调试,然而在Win10的较新版本中这个服务已经被移除了,因此本文采用的调试方式为远程调试。 - 将动态库的入口函数的开始部分字节替换成一条自循环的指令,之后启动服务时,就会先在这里卡住,这时可附加调试器,再将之替换为原指令即可。这样做不需要修改注册表,但要先把原来的dll文件替换掉。
- 利用
- 下文以TermService服务为例,实验验证上述两种方法。
实验环境
- 物理机:Windows 10 x64,已安装Windbg Preview。
- 虚拟机:Windows 10 x64,将在该系统上运行要调试的TermService服务
实验一:结合Image File Execution Options
键,使用调试器启动服务进程
搭建调试环境(windbg + ntsd/cdb)
- 电脑安装wdk后,其中的
Debuggers
目录下有全部跟调试有关的库和程序等(注入windbg
,cdb
,ntsd
,dbgsrv
)。我的虚拟机是x64的系统,故将Debuggers\x64
目录拷贝到虚拟机中。注意,必须拷贝整个目录,不能只拷贝单个程序,否则会因缺少库而导致各种问题。
- 关闭防火墙,以便使用tcp端口作为远程调试用的端口。
- 找到
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
项。我要调试termservice服务,其通过svchost.exe
启动,故找到svchost.exe
键,新建字符串类型值debugger
,填入调试器路径及启动参数:C:\Users\cmtest\Desktop\x64\ntsd.exe -server tcp:port=1234 -noio -y srv*C:\win_symbols*http://msdl.microsoft.com/download/symbols
。-y
参数指定符号路径。使用ntsd或cdb开启调试服务后,远程调试时调试器是在虚拟机本地搜索调试符号文件的。
- 在
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
键下,新建一个dword值ServicesPipeTimeout
,该值表示服务启动的超时时间(单位:毫秒)。为了保证有足够时间调试,设置值为86400000,表示24小时。注意,需重启电脑才能使这一设置生效。
- 打开服务管理器,找到要调试的服务,在
登录
项里选中允许服务与桌面交互
。如果前面设置了debugger
值而不设置这一项,则启动服务会提示无权限。
- 虚拟机中启动要调试的服务。
- 在宿主机中打开windbg preview,选择
Connect to remote debugger
,在Connection strings
中填写tcp:server=192.168.29.128,port=1234
,并点击OK
。
- 成功连接后,宿主机的windbg和虚拟机的情况分别如下二图。在下面第二张图中,可看到ntsd进程的使用的TCP 1234端口已建立了连接。
问题一:所有svchost服务启动时都会通过ntsd使用同一端口
- 这样一来,系统有时启动一些svchost服务,会出现很多新的ntsd进程,调试上比较麻烦(类似于后面的问题二所述)。
- 解决方法如下:
- 在system32目录,拷贝svchost文件,新文件命名为
svchost2.exe
。 - 修改
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TermService
键的ImagePath
值,改用svchost2
的文件路径。 - 注册表
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
下新建一个svchost2.exe
项,参照前面的填法填入调试器路径及参数。
- 在system32目录,拷贝svchost文件,新文件命名为
- 之后启动服务,可看到使用的是
svchost2
。并且不会再出现其他ntsd进程。
问题二:无法退出调试的服务
- 当服务退出时,windbg会再次中断,停留在
ntdll!NtTerminateProcess
函数的结束处(这也表明被调试的进程已经结束了)。但是退出调试器再连接此端口时,仍会中断于此。
- 最后只能通过process hacker将ntsd进程杀掉。暂无其他方法。
问题三:.reload
命令没能下载符号
- 在宿主机的windbg调试器中执行
.reload
命令时提示The system cannot find the file specified
。看了下符号路径,也没发现问题。无解。
- 目前的解决方法:先在虚拟机中启动termservice服务(需先把前面对
Image File Execution Options
项设置的debugger
去掉),然后ntsd -p <pid>
附加到此服务进程,设置sympath,再通过.reload /f
下载符号文件。
分析svchost.exe
,以确定作为服务载体的dll库被加载的时机
- 刚开始连上调试服务时,windbg停在启动进程时用的加载器函数
LdrInitializeThunk
这里。这时svchost.exe
已加载(即可使用调试符号),termsrv.dll
未加载。需要在termsrv.dll
被加载完后,才能到这个库的入口处下断点。
- 思路是在
svchost.exe
中找到对LoadLibrary
函数的调用,以确定加载目标dll的那段代码。用ida打开svchost.exe
,查看LoadLibrary
函数的交叉引用。
- 可以看到主要是
GetServiceMainFunctions
函数在调用LoadLibrary
函数,判断加载动态库的主要操作在此中,所以进GetServiceMainFunctions
函数中简单分析一下。 - 首先看到了对注册表项
System\\CurrentControlSet\\Services\\Parameters
下的ServiceDll
和ServiceManifest
等值的读取,明白这里要获取目标dll的文件路径。
- 获取的路径字符串存在了
a1
参数指向的某结构体中(a1 + 8
,即a1
处往后的第8个qword处)
- 之后便是调用
LoadLibrary
函数加载动态库。
- 之后又分别获取
SvchostPushServiceGlobals
,SvchostPushServiceGlobalsEx
以及dll主函数地址,分别存放到a2
,a3
,a4
参数中。
- 通过分析,确定
GetServiceMainFunctions
函数会完成dll的加载,并且不会将执行权转交给dll。因此,找到调用GetServiceMainFunctions
函数的地方,运行完该函数,即可完成termsrv.dll
的加载。 - ida中看到调用
GetServiceMainFunctions
函数的地方只有一处,因此在此处调用后面下断点,直接F5运行至此,完成termsrv.dll
库的加载。
- 既然
termsrv.dll
已被加载到内存中,那就能加载其调试符号,然后找到主函数并下断点了。
实验二:修改程序入口,迟滞进程的启动
- 前面提到,上面的方法有一个问题,就是不能通过在宿主机的windbg中执行
.reload /f
命令将所有相关的符号文件下载到本地。于是有了下面的测试。
实验2.1:将termsrv!DllMainCRTStartup
函数开头替换为自跳转指令
- 在ida中加载
termsrv.dll
文件,将DllMainCRTStartup
函数的开头两个字节替换成EB FE
,跳转到此短跳转指令自身,让程序在开始运行时陷入死循环。
- 导出打过补丁的
termsrv.dll
,替换虚拟机中的该文件,然后启动TermService服务。这时,即使没有通过调试器启动svchost,服务启动时也会卡着。
- 这时就可在本地(也就是虚拟机中)通过windbg附加到服务进程。在反汇编窗口可看到
DllMainCRTStartup
函数修改后的样子。这时候,可以执行.reload /f
将包括termsrv.pdb
在内的相关的符号文件下载到本地。然后就可以愉快地找函数下断点了。
- 之后再windbg中打开内存窗口,将前面的补丁字节改回来。
- 继续运行,服务便完成启动了。
实验2.1:将termsrv!DllMainCRTStartup
函数开头替换成int 3
- 现在再测一下将
termsrv!DllMainCRTStartup
函数第一个字节替换成CC
,以触发int 3
中断。
- 在虚拟机已配置好内核调试的情况下,服务一启动系统就会卡死。
- 这时在物理机挂上内核模式的windbg,可看到系统卡在svchost进程这,也就是上面设置的
int 3
中断处。这时可以通过.reload
命令将符号文件下载到物理机的文件系统中。之后同前文所述,下断点、将开头字节改回原字节,即可继续运行并调试了。