背景
被问到调试服务进程各种问题,附加了就退出,没办法用调试器直接启动服务进程。。。。 其实这些问题每一个都有坑,索性直接写篇文章来描述一下吧,这篇先说下服务进程怎么启动时挂载调试器或崩溃时启动JIT调试器调试,下一篇说一下其他的调试解决方案。
其实这个问题我刚开始工作的时候也很苦恼,其实微软也知道这东西不好调,对于调试服务专门给出了解决方案的,但是微软的资料吧,太多,很难找,而且很多现象没有前置的知识不知道为什么,这篇文章我系统的总结一下。
时间不多,我只截了2个图,文章中图比较小,点开看。
前置知识
进程启动时挂载调试器
如何在一个进程启动时就附加到调试器上呢?Windows系统的强大调试机制本身就提供了解决方案,只要在下面的注册表路径下添加新项:
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
这个项用来修改应用程序启动的一些选项,包括GFlags的配置,启动时附加调试器等等。
这个项是按进程名来区分的所以要调试前最好给进程取一个独特的名字(别是什么svchost.exe就行,不然会很惨),例如:要调试的EXE的名字为die.exe,那么就在这个路径添加下面的项:
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\die.exe
然后在这个项中添加一个名叫Debugger
的REG_SZ的值,值为调试器路径,最后的效果就是:
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\die.exe] "Debugger"="C:\\WinDDK\\7600.16385.1\\Debuggers\\windbg.exe"
上述代码可以直接保存成reg文件,双击注册表添加后,启动die.exe的进程前都会windbg附加。
题外话:
1.启动die.exe会被windbg附加,那如果die.exe是explorer.exe,这个调试器是一个恶意程序路径会有什么效果呢?
2.如果自己想写一个任务管理器,替换掉系统的任务管理器,有什么想法?
但是如果添加的是一个服务进程的话,那可就麻烦了,你会惊奇的发现,windbg弹不出来,且过一会儿进程就会退掉。这个是正常的,后面会解释具体原因和解决办法。
进程崩溃时实时启动调试器
这个功能有一个名字,叫注册JIT调试器。
熟悉的背影
安装vs后,会发现程序崩溃不是再弹出Windows错误报告界面,转而弹出让选择VS的调试器的界面,这个界面其实就是注册了JIT调试器。
每种调试器都有对应的参数或者设置可以将自己设置为JIT调试器,例如,Windbg设置如下:
Windbg.exe -I
上面的命令,在命令行里执行,就可以将Windbg设置为即时调试器,当程序崩溃时就会直接被Windbg附加。
同理,x64dbg等等都可以被设置,如果界面上有,那么就在界面上设置,如果界面上没有,那么请看下面的原理部分。
原理
其实执行完上面的命令后,Windbg只是修改了注册表的一个键值:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
这个项里面有一个REG_SZ类型的值,名字为Debugger
,后面就是Windbg的路径加参数
默认情况下,是没有这个值的,这时,如果没有禁用Windows错误报告,那么程序崩溃就会调用系统的错误报告那个程序。上面说到的安装VS后,VS也会替换这个位置的值为:
"C:\Windows\system32\vsjitdebugger.exe" -p %ld -e %ld -j 0x%p
同理,上面执行Windbg.exe -I
后,这个值会变成下面的值
"C:\WinDDK\7600.16385.1\Debuggers\windbg.exe" -p %ld -e %ld -g
题外话:
Windbg会设置另外一个Auto的值,这个值的含义如下:
0:系统会弹出一个对话框,可以选择调试器 1:系统会自动调用默认调试器
好了,JIT调试器的知识到这里就够用了,下面说说服务为什么跟别的进程不一样。
服务的特点
高权限
服务一般是SYSTEM进程跑的,所以低权限的调试器肯定是没办法调试系统服务的,一定得管理员或者SYSTEM权限的调试器。
Session隔离
首先是Session隔离,服务进程运行在Session 0, 普通用户的桌面进程在用户的session,这可以有多个,每个用户登录后,都只能看到自己session的窗口。而系统调用JIT调试器或者启动时挂载调试器的情况都是系统在服务的Session调用起来的。这就导致了界面没有在用户的session,也就是窗口弹是弹了,但是看不到。
题外话
Win7是可以给开启允许服务与桌面交互,然后用调试器在另外一个桌面调,我只在Win7见过,Win10我没试过,这个后面说。
不响应服务状态,进程会退出
这个点就得讲一下怎么写服务程序了,我用C++比较多,一个正常的服务程序大概长这样(这个代码转载自MSDN):
// // Purpose: // Sets the current service status and reports it to the SCM. // // Parameters: // dwCurrentState - The current state (see SERVICE_STATUS) // dwWin32ExitCode - The system error code // dwWaitHint - Estimated time for pending operation, // in milliseconds // // Return value: // None // VOID ReportSvcStatus( DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint) { static DWORD dwCheckPoint = 1; // Fill in the SERVICE_STATUS structure. gSvcStatus.dwCurrentState = dwCurrentState; gSvcStatus.dwWin32ExitCode = dwWin32ExitCode; gSvcStatus.dwWaitHint = dwWaitHint; if (dwCurrentState == SERVICE_START_PENDING) gSvcStatus.dwControlsAccepted = 0; else gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; if ( (dwCurrentState == SERVICE_RUNNING) || (dwCurrentState == SERVICE_STOPPED) ) gSvcStatus.dwCheckPoint = 0; else gSvcStatus.dwCheckPoint = dwCheckPoint++; // Report the status of the service to the SCM. SetServiceStatus( gSvcStatusHandle, &gSvcStatus ); } VOID WINAPI SvcMain( DWORD dwArgc, LPTSTR *lpszArgv ) { // Register the handler function for the service gSvcStatusHandle = RegisterServiceCtrlHandler( SVCNAME, SvcCtrlHandler); if( !gSvcStatusHandle ) { SvcReportEvent(TEXT("RegisterServiceCtrlHandler")); return; } // These SERVICE_STATUS members remain as set here gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; gSvcStatus.dwServiceSpecificExitCode = 0; // Report initial status to the SCM ReportSvcStatus( SERVICE_START_PENDING, NO_ERROR, 3000 ); // Perform service-specific initialization and work. SvcInit( dwArgc, lpszArgv ); } // // Purpose: // Entry point for the process // // Parameters: // None // // Return value: // None, defaults to 0 (zero) // int __cdecl _tmain(int argc, TCHAR *argv[]) { // If command-line parameter is "install", install the service. // Otherwise, the service is probably being started by the SCM. if( lstrcmpi( argv[1], TEXT("install")) == 0 ) { SvcInstall(); return; } // TO_DO: Add any additional services for the process to this table. SERVICE_TABLE_ENTRY DispatchTable[] = { { SVCNAME, (LPSERVICE_MAIN_FUNCTION) SvcMain }, { NULL, NULL } }; // This call returns when the service has stopped. // The process should simply terminate when the call returns. if (!StartServiceCtrlDispatcher( DispatchTable )) { SvcReportEvent(TEXT("StartServiceCtrlDispatcher")); } }
其中关键的就是ReportSvcStatus函数,这个函数的实现其实就是设置服务的状态,完整代码请参考我文章下面的链接,如果程序运行后一段时间(默认大概是30秒)没有向系统反馈服务的状态,那么就会被系统干掉。所以要调试服务,调试器附加后要尽快让它跑起来。
Windbg的远程调试
其实windbg和cdb用起来是一样,用cdb和windbg做服务端都可以。我这里用windbg使用TCP协议通信,也可以用其他的,管道、1394什么的。
先运行服务端,然后再运行客户端连接服务端,具体参数如下:
服务端:
windbg.exe -server tcp:port=5005 要调试的EXE
客户端:
windbg.exe -remote tcp:Port=5005
这样就可以在客户端的windbg输入命令,调试服务端的程序了。
启动挂载调试器或JIT调试器调试服务的方法
完整的调试服务,我知道的有下面三种方法:
1.远程调试
2.内核调试用户程序
3.允许服务与桌面交互
本篇文章只讲解远程调试的方式,远程调试有一个要求,就是手速要快。为什么上面一小节讲过了。如果想我一样老年人手速,那么就按照下面的方法:
微软给的方法是修改注册表:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
说明:
在此键下,找到或创建名为 ServicesPipeTimeout 的 DWORD 数据值。将此条目设置为您希望服务在超时之前等待的时间量(以毫秒为单位)。例如,值 60,000 表示 1 分钟,而值 86,400,000 表示 24 小时。如果未设置此注册表值,则默认超时约为 30 秒。 此值的意义在于,当每个服务启动时,clock 开始运行,当达到 timeout 值时,附加到该服务的任何 debugger 都将终止。因此,您选择的值应长于从启动服务到调试会话完成之间经过的总时间。 此设置适用于在注册表编辑完成后启动或重新启动的每项服务。如果某些服务崩溃或挂起,并且此设置仍然有效,则 Windows 不会检测到该问题。因此,应仅在调试时使用此设置,并在调试完成后将注册表项恢复为其原始值。
上面设置后,应该是要重启机器,配置才能生效。
其实,上面已经把知识点讲的差不多了,综合一下就可以了,在设置启动挂载调试器或者JIT调试器时加上远程调试参数就可以,这个调试器就是服务端。
例如,如果是JIT调试器,那么就这么设置:
具体设置的内容如下:
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\die.exe] "Debugger"="\"C:\\WinDDK\\7600.16385.1\\Debuggers\\windbg.exe\" -server tcp:port=5005"
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug] "UserDebuggerHotKey"=dword:00000000 "Debugger"="\"C:\\WinDDK\\7600.16385.1\\Debuggers\\windbg.exe\" -server tcp:port=5005 -p %ld -e %ld -g" "Auto"="1"
最后
这篇先写到这里吧,码字太费时间了,下次找时间说说怎么从内核调试应用程序,还有就是开启允许服务与桌面交互的情况下在win7上调试服务。