I、 摘要
WIN NT下的服务就类似*NIX下面的守护进程一样,而且现在越来越多的软件开始设计成服务的形式,从XP推出之后,通过服务来实现多用户切换等就显得很有作用了。从安全角度来看待WIN的服务的话,也就因此有很多的话题,比如运行的权限、运行的时间等等。本文就从一些方面来介绍并谈谈WIN服务的一些东西,受水平限制,内容不精致。
II、 关于WIN2K的服务
WIN32服务由三部分组成:服务应用程序、服务控制程序(SCP)和服务控制管理器(SCM)。
一、服务控制管理器
服务控制管理器(Service Control Manager):在系统启动的时候开始,是WIN系统的一部分,它是一个远程过程调用(RPC)服务器。这也是WIN服务系统的核心。
SCM主要负责下面的东西:
·维护安装的服务数据库
·在系统启动或者有命令的时候开始服务和驱动服务
·枚举安装的服务和驱动
·维护运行着的服务和驱动的状态
·传输控制请求去运行服务
·锁定和解锁服务数据库
SCM维护着注册表中的服务数据库,位于:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services。其下的子键就是安装的服务和驱动服务。每个子键的名称就是服务名,当安装的时候由服务安全程序的 CreateService 函数指定。
当系统安装的时候,最初的数据库就被创建。这个数据库包含系统启动时候的设备驱动。数据库中的每个服务和驱动的信息包括:
·服务类型。服务执行时候是自己的进行还是同其他服务共享进行,是否是核心驱动还是文件系统驱动。
·启动类型。服务或者驱动服务是否是在系统启动的时候自动启动还是,是否是由SCM来接受控制请求来启动。启动类型也表明服务是否被禁止。
·错误控制等级。指明如果服务或者驱动服务启动失败的错误处理。
·执行文件的全路径。
·附加依赖信息决定启动的正确顺序。对于服务,这个信息包括在服务启动之前SCM需要先启动的指定服务,服务所属加载顺序组的名称,服务在组中启动顺序的标志符。对于驱动服务,这个信息包括驱动启动前需要启动的指定驱动。
·对于服务,还有附加的帐号名称和密码。如果没有指定帐号,服务就使用LocalSystem帐号。
·对于驱动,附加驱动对象名称,用于I/0系统加载设备驱动。如果没有指明对象名,I/O系统在驱动服务名称基础上创建一个默认的名称。
二、服务控制程序
服务控制程序(SCP)则是控制服务应用程序的功能块,也是服务应用程序同服务管理器(SCM)之间的桥梁。服务控制程序可以完成这些动作:
·如果服务启动类型为SERVICE_DEMAND_START,那么服务控制程序来启动服务
·发送控制请求给运行着的服务
·查询运行着的服务的当前状态
这些动作要求打开一个服务对象的句柄。
·服务启动
要启动一个服务,服务控制程序使用StartService 函数。如果数据库被锁定,那么StartService函数会失败。如果遇到这种情况,那么服务控制程序需要等待,并重新调用StartService。可以通过QueryServiceLockStatus来查询服务数据库的状态。
当服务控制程序开始一个服务的时候,可以通过StartService函数来指定传递给服务ServiceMain 函数的参数。当创建一个新的线程去执行ServiceMain后,StartService就返回了。服务控制程序可以通过QueryServiceStatus 函数来查询被启动的服务的状态。在SERVICE_STATUS结构初始化中dwCurrentState应该是SERVICE_START_PENDING,而dwWaitHint则是一个毫秒的时间间隔,表示服务控制程序在调用QueryServiceStatus应该等待的时间。当初始化完成,服务就会改变服务的状态dwCurrentState 为SERVICE_RUNNING。
如果服务在80秒,再加上最后的等待时间内没有改变它的状态,服务控制管理器确定服务已经停止响应,会记录事件并停止服务。
如果程序在启动驱动服务,StartService会在设备驱动初始化完成后返回。
·服务控制请求
服务控制程序通过ControlService来发送一个控制请求给运行着的服务。这个函数指定控制值传递给指定服务的HandlerEx 函数。这个控制值可以是用户自定义码,也可以是下面这些基本控制码:
·停止服务:SERVICE_CONTROL_STOP
·暂停服务:SERVICE_CONTROL_PAUSE
·恢复被暂停的服务:SERVICE_CONTROL_CONTINUE
·返回服务的更新状态信息:SERVICE_CONTROL_INTERROGATE
每个服务可以指定它接收和处理的控制值。要确定哪个基本控制值被服务接收,可以使用QueryServiceStatus 函数或者指定SERVICE_CONTROL_INTERROGATE 来调用ControlService 函数。SERVICE_STATUS结构中的dwControlsAccepted返回的是是否服务能被停止、暂停和恢复。所有的服务都能接收SERVICE_CONTROL_INTERROGATE QueryServiceStatus函数返回指定服务的最近状态,而不会获得服务本身更新的状态。使用SERVICE_CONTROL_INTERROGATE控制来调用ControlService函数可以确定状态是否是当前的信息。
三、服务应用程序
服务应用程序是一个服务的主体程序,它是一个或者多个服务的可执行代码。这将在服务的编程中详细解释。
III、 服务的启动和关闭的基本过程
当系统启动的时候,SCM会启动所有自动启动的服务以及这些服务依赖的服务。如果一个自动启动的服务所依赖的服务是“手动”(需要命令才启动)的服务,那么这个服务也会被自动启动。服务的加载顺序由下面这些方面来决定:
1.组的顺序
2.一个组中服务的加载顺序
3.每个服务所依赖的服务
当启动完成的时候,系统执行启动确认程序(由注册表的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
中的BootVerificationProgram值指定,默认情况下,这个值是没有的。)。当第一个用户登录后,系统会简单地报告启动成功。可以单独提供一个启动确认程序来检查系统问题和报告启动状态给SCM,使用 NotifyBootConfigStatus 函数。当系统成功启动后,系统就克隆保存一份数据库备份,作为last-known-good(LKG)配置。如果当前使用的数据库导致系统启动失败,那么可以用备份来恢复。备份的数据库就保存在:
HKEY_LOCAL_MACHINE\SYSTEM\ControlSetXXX\Services 中。
其中XXX值也被保存在:
HKEY_LOCAL_MACHINE\System\Select\LastKnownGood 中。
如果自动启动的服务自动的时候得出SERVICE_ERROR_CRITICAL错误,SCM就会重新启动机器,并使用LKG的配置,如果LKG的配置已经被使用了,启动就会失败。注册表中服务的ErrorControl值表示SCM如何处理服务错误。如果值为SERVICE_ERROR_IGNORE(0)或者没有指定,SCM只忽略错误并继续服务的启动,如果为SERIVCE_ERROR_NORMAL(1),就在事件日志中记录下错误原因。如果错误控制为SERIVCE_ERROR_SEVERE(2)或者SERIVCE_ERROR_CRITICAL(3),服务就报告启动错误。SCM记录事件日志,并调用函数ScreverToLastKnownGood,将系统注册配置切换到LKG的版本,然后调用NtShutDownSystem重新启动系统。如果系统已经使用LKG版本,就直接重新启动。
LKG版本的产生:SCM在系统启动阶段启动了所有自起服务之后,需要来决定这个LKG配置。缺省情况下,一次成功的启动包括所有服务的成功启动和一个用户的登录。如果在启动服务阶段存在服务的SERIVCE_ERROR_SEVERE(2)或者
SERIVCE_ERROR_CRITICAL(3)错误,那么这就是失败的启动。如果SCM成功完成服务的启动,当有用户登录的时候, Winlogon调用NotifyBootConfigStatus函数发送消息给SCM。在成功启动所有服务,并且收到NotifyBootConfigStatus的
登录信息,SCM就调用NtInitializeRegistry保存当前的启动配置信息。
第三方可以用自己的定义取代Winlogon的确认,这可以由注册表中:
KHLM\SYSTEM\CurrentControlSet\Control\BootVerificationProgam中的程序确定,可通过此加入对系统成功启动的定义。启动验证程序则通过设定HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Winlogon\ReportBootOK为0禁止Winlogon对NotifyBootConfigStatus的调用。这样,SCM启动完服务后,等待这个验证程序调用NotifyBootConfigStatus函数通知登录成功,然后才保存LKG配置。
SCM的执行文件是:WINN\System32\Service.exe,以控制台模式运行,Winlogon进程在系统启动早期启动SCM。SvcCtrlMain在紧接着屏幕变为空白时刻运行,在Winlogon加载图形化身份鉴定并显示登录界面GINA之前运行。
SvcCtrlMain首先创建一个nonsignaled初始化的名为SvcCtrlEvent_A3752DX的同步事件,在完成准备接受SCP的命令的各项步骤之后,SCM才设定此事件为signaled状态。SCP通过OpenSCManager函数来确认SCM,这个函数通过等待
SvcCtrlEvent_A3752DX为signaled来防止SCP在SCM初始化完成之前接触SCM。
SvcCtrlMain然后调用ScCreateServiceDB函数,建立SCM的服务数据库。它先读取注册表中HKLM\system\CurrentControlSet\Control\ServicegroupOrder\list内容,列出服务组名称和它们的启动顺序,然后再搜索HKLM\SYSTEM\currentControlSet\Services的内容,为每一条主键在服务的数据库中创建一个条目。SCM本身属于自起服务和设备驱动,并且标记为引导启动和系统启动驱动的启动错误,也就是,所有标记为引导驱动和系统启动驱动将在
SCM启动前被加载,在用户模式进程执行前,I/O管理器就会加载这些启动。ScCreateServiceDB读取服务的组键值来确定服务所属组,并同先前建立的组列表联系起来。该函数还通过DependOnGroup和DependOnService函数来查询服务和组的依赖关系。
在服务启动的时候,SCM可能需要调用Lsass,SCM会等待Lsass在其初始化结束时的LSA_RPC_SERVICE_ACTIVE同步事件通知,Winlogon也会启动Lsass进程,Lsass和SCM的初始化是同步的,但初始化结束顺序不确定。SvcCtrlMain会调用
ScGetBootAndSystemDriverState来遍历服务数据库查询引导启动和系统启动的设备驱动,该函数通过查询对象管理器中的名字域目录\Driver中的名字来确定驱动是否成功启动。当设备驱动被成功加载,I/O管理器就把驱动器对象插入名字域。如果驱动没有被加载,SCM在PnP_DeviceList函数返回的驱动列表中查询其名字,SvcCtrlMain记录没有启动的驱动名称,并作为ScFailedDrivers列表中当前配置文件的一部分。
在启动自启服务之前,SCM创建一个管道作远程过程调用Pipe\Ntsvcs,并且创建一个线程来监听SCP的消息,然后通知它的初始化结束事件SvcCtrlEvent_A3752DX。SCM通过由RegesterServiceProcess注册一个控制台应用程序关闭事件处理并向WIN32子系统注册来为系统关闭作准备。
SvcCtrlMain调用SCM的ScAutoStartService来启动自启动的服务,算法是阶段性执行的,每个阶段有一个服务组构成。当一个阶段开始时,ScAutoStartService把所有属于该组的服务作标记,然后循环所有标志的服务,检测是否每个服务能够启动。检测内容包括服务组的依赖关系,如果存在依赖关系,服务所依赖的组必须先完成初始化,并且这个组中至少有一个服务已经成功启动了。如果组启动顺序中服务的依赖组比服务所在组靠后,SCM标记circular dependency错误给服务。对于一个服务(非驱动),还回检测服务的依赖关系,并且所依赖的服务是否已经启动。同样,也会标记circular dependency错误,这时就不启动服务,如果服务依赖的是本组的服务,并且还没有被启动,那么这个服务的执行就被跳过。
依赖关系检测通过后,在服务启动前,ScAutoStartService还要检测此服务是否是当前引导配置的一部分。比如安全模式启动,在HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot中列举,分为最小配置和网络支持配置需要启动的服务和驱动。
当确定要启动服务,SCM就调用ScStartService函数。当ScStartService启动一个WIN32服务的时候,会读取服务注册主键的ImagePath来确定运行服务进程的文件,然后检查服务类型是否为SERVICE_WIN32_SHARE_PROCESS,SCM确保服务进程与已启动服务使用相同的帐号登录,服务的注册键值中包含有帐号信息。这时,SCM调用ScLogonAndStartImage启动服务进程,如果是其他帐号,就使用Lsass函数登录帐号。调用LsaLogonUser指派服务的登录类型,Lsass在注册表Security下Secrets下的_SC_中找到密码,登录成功后,LsaLogonUser返回调用者访问句柄,代表用户的安全权限。
登录成功后,如果没有装载帐户信息,SCM调用LoadUserProfile函数装载帐号信息。对于交互服务必须打开WinSta0。
然后服务还没有被启动,ScLogonAndStartImage函数会继续启动服务进程。SCM用CreateProcessAsUser函数以挂起状态启动进程,然后创建一个命名管道来同服务进程通信:\PipeNetNetControlPipeX(X是每一个新管道叠加的),SCM
通过ResumeThread来恢复服务进程和等待服务同SCM管道连接。注册表中:
HTLM\SYSTEM\CurrentControlSet\Control\ServicePipeTimeout的值决定了这个等待时间,默认为30秒。
如果服务顺利同SCM连接上,SCM就发送启动命令给服务,如果在超时内,如果没有对响应启动命令,SCM就放弃并开始启动下一个服务,并在系统日志中记录下错误。
ScAutoStartServices循环组内的所有服务直到服务被启动或者发生依赖关系错误为止。循环是按照服务的依赖关系自动排列服务的次序。SCM会先循环所依赖的服务。SCM结束所有服务组后,执行那些列表中没有列出的组的服务,最后执行不属于任何组的服务。
当系统开始关闭的时候,Win32ExitWindowsEx函数发送消息给系统进程Csrss,调用Csrss的关闭例程。Csrss遍历所有进程同志它们系统正在关闭。在通报下一个进程前,Csrss等待除SCM以外的每个系统进程退出,等待时间为:
HKLM\.DEFAULT\ControlPanel\Desktop\WaitToKillAppTimeout,缺省为20秒。当Csrss遇到SCM进程时,也通知SCM系统正在关闭,并等待为SCM指定的超时。在系统初始化的时候,SCM通过RegisterServicesProcess函数向Csrss注册它的进程ID,Csrss通过使用SCM的进程ID来识别SCM。SCM的超时值为:
HKLM\SYSTEM\CurrentControlSet\Control\WaitToKillServiceTimeout,缺省为20秒。
SCM的关闭处理程序发送关闭通知给所有SCM初始化时申请需要关闭通知的服务。SCM的ScShutdownAllServices遍历SCM数据库寻找那些请求关闭通知的服务,并发送关闭通知,同时记录等待延时。发送关闭通知后,SCM等待通知的服务退
出或者等待超时为止。如果服务超时没有退出,SCM测定一个或者多个等待退出的服务是否发送一个消息给SCM,这个消息是来告诉SCM服务在关闭过程中取得的进展。如果至少一个服务有进展,SCM就在延时等待范围内再等待一次。SCM持续该等待循环,直到所有服务退出,或者在等待延时范围内没有收到服务的进展信息为止。
当SCM通知服务关系并且等待服务退出的时候,Csrss等待SCM退出。如果Csrss等待超时,而SCM还没有退出,Csrss就继续关闭过程,所以,在系统关闭时,没有在规定时间内成功关闭的服务只是简单地同SCM一起执行。
IV、 服务的编程
服务程序是一个或者多个服务的可执行代码。SERVICE_WIN32_OWN_PROCESS类型创建的服务只能是一个服务的执行程序。服务可以配置使用本地、主域或者信任域的帐号关系执行。SERVICE_WIN32_SHARE_PROCESS类型的服务代码中可以包
含多个服务。
一个服务必须包含 main 、ServiceMain 和 控制处理函数
·服务的main函数
服务通常是控制台程序,入口点就是main函数,main函数从注册表中服务的ImagePath值中获得参数。当SCM开始服 务程序的时候,等待调用StartServiceCtrlDispatcher 函数。规则为:
·SERVICE_WIN32_OWN_PROCESS 类型的服务会从主线程立刻调用 StartServiceCtrlDispatcher 函数。可以在服务
启动后完全初始化。
·SERVICE_WIN32_SHARE_PROCESS类型的服务,在程序中进行公共初始化,可以在StartServiceCtrlDispatcher函
数调用前在主线程中完成初始化,只要花费的时间少于30秒。否则,当主线程调用StartServiceCtrlDispatcher的时候
必须创建另外一个线程去完成公共初始化。可以在 ServiceMain 函数中去完成每个服务单独的初始化。
StartServiceCtrlDispatcher函数为在进程中的每个服务获得一个SERVICE_TABLE_ENTRY 结构。每个结构指定服务
名和服务的入口点。如果StartServiceCtrlDispatcher函数调用成功,调用线程不会返回,直到所有运行服务的进程都
终止。SCM通过命名管道控制这个线程的请求。这个线程就象发报机(调度器),完成下面任务:
·当新的服务开始时,创建一个新的线程去调用适当的入口
·调用适当的句柄函数去操作服务控制请求
当SCM启动一个服务进程的时候,就会调用StartServi