Kmd教程2-服务

转自:http://www.gomb.cn/?uid-2-action-viewspace-itemid-1449

在这里下载本文的源代码

2. 服务

※ 和本节对应的例子代码见KmdKit/examples/simple/Beeper

    读者也许有点疑惑:用户模式的服务关内核模式的驱动程序什么事呀?事实上,两者的确风马牛不相及,但是如果我们要和设备驱动程序通讯的话,我们必须首先安装它,启动它,而和设备驱动程序通讯的界面刚好和服务通讯的界面是类似的。

2.1 Windows服务

    Windows NT使用某种机制来启动进程,并让它们不和某个具体的交互式的用户界面相关联,这些进程就被称为服务(service),服务的一个很好的例子就是Web服务器,这些Web服务都没有用户界面,服务是唯一以这种方式运行的应用程序(注:指没有用户界面,当然,严格地说病毒、木马以及所有不想见光的程序也是这样的~~),服务可以在系统启动的时候自动启动,也可以被手工启动,从这一点来看,设备驱动程序和服务是类似的。
    Windows NT还支持驱动程序服务,只要使用的时候遵循设备驱动程序协议就可以了,这和用户模式的服务类似,所以,"服务"一词既可以指用户模式的服务进程或者内核模式的设备驱动程序,微软不知何故没有明确地区分两者的概念,所以下面的叙述可能看起来有点让人疑惑。可能有的地方我会说到"driver"一词,但在其他的文章中可能说到"service"一词,但既然这篇教程讲的是如何编写内核设备驱动程序,那么我们就约定无论说到"service"还是"driver",我们的意思都是指"驱动程序",当的确需要提及"服务"的时候,我会明确地指出来的。
    另外,请读者时刻记得,文档中关于服务管理的函数其实是叙述得相当含糊的,因为这些函数既能用于驱动程序也能用于服务,在下面的文章中,我们只强调它们在驱动方面的用途和忽略服务方面的用途。
    Windows NT中有三个组件和服务管理相关:
◎ 服务控制管理器(Service Control Manager/SCM)--用于启动服务以及和它通讯
◎ 服务控制程序(Service Control Program/SCP)--用于和SCM进行通讯,告诉它何时启动或者停止服务

(咦!第三个哪里去了,我也不知道,原文就这么两个呀,可能后面会提到吧~~)

    服务程序中包含可执行代码,这两个组件对服务和驱动程序的处理方式是相同的。我们先来看看前面两个组件,在后面再讲述驱动程序。

2.2 服务控制管理器(SCM)

    SCM的代码位于/%SystemRoot%/System32/Services.exe中,当系统启动的时候,SCM被WinLogon进程启动,然后它扫描注册表中HKLM/SYSTEM/CurrentControlSet/Services键下的相关内容,根据这些内容创建一个服务数据库,数据库中包括所有服务的相关参数,如果服务或者驱动被标为自动启动的,那么启动它们并检测启动中是否出错。
    为了更深入一步,我们可以用注册表编辑器regedit.exe来打开并观察注册表中的 HKLM/SYSTEM/CurrentControlSet/Services/下面的内容。
    想要查看系统中安装了哪些服务(注意不是驱动),可以在控制面板中选择"管理工具",再打开"服务"来查看。
    要查看系统中安装了哪些驱动,可以在控制面板中选择"管理工具",再打开"计算机管理",在"系统信息"下的"软件环境"中,你可以看到所有驱动的列表,但是不幸的是,在Windows XP中,这个功能被取消了。
    仔细对比一下上面三个地方的内容,我们可以发现这些内容是很一致的。
    HKLM/SYSTEM/CurrentControlSet/Services/下面有很多子键,表示一个服务的内部名称,每个子键下包含了和这个服务相关的参数。
    现在来考察一下安装一个服务所需的最低数量的参数,我们拿beeper.sys来举例,以后再来讨论这个驱动本身。



图2.1 beeper.sys驱动的注册表键值

    这些参数的含义如下:

◎ DisplayName--用户程序访问服务时使用的名称,如果为空,那么注册表的键名会被作为它的名称
◎ ErrorControl--如果SCM启动服务的时候驱动报错,这个值决定了SCM如何对付这个错误,我们对两种取值有点兴趣:
· SERVICE_ERROR_IGNORE (0)--I/O管理器忽略这个错误,不作记录
· SERVICE_ERROR_NORMAL (1)--如果驱动被装入的时候报错,系统将给用户显示一个告警框,并将错误记录到系统日志中

    你可以在控制面板中的"管理工具"中选择"事件查看器"来查看系统日志,例如,beeper.sys驱动在初始化的时候做完了所有该做的事(这个例子会让喇叭发声音,但是发声功能是在初始化函数DriverEntry中做的,初始化函数执行完,后面就没什么事了),所以它就返回一个错误,系统就会将它从内存中卸载。但是这里的ErrorControl参数等于SERVICE_ERROR_IGNORE,所以系统日志中并没有错误记录。

◎ ImagePath--指驱动文件的全路径文件名,如果该参数没有指定路径,那么系统会在/%SystemRoot%/Drivers目录下查找
◎ Start--指明何时装载驱动,这里我们关心的也是两个取值
· SERVICE_AUTO_START (2)--驱动在系统启动的时候装载
· SERVICE_DEMAND_START (3)--驱动由SCM根据用户要求装载

    如果驱动的Start参数为SERVICE_AUTO_START (2),那么SCM会在系统启动的时候就装载它,这样的驱动被称为自动启动的服务,如果驱动的执行依赖于其他的驱动,SCM也会把其他的驱动也启动起来(要控制设备驱动被装载的顺序,可以使用Group、Tag和DependOnGroup等参数值;要控制服务被装载的顺序,可以使用Group和DependOnService参数)。Start参数还有其他的取值,如SERVICE_BOOT_START (0),但这个参数只能供设备驱动程序使用,I/O管理器将在用户模式的进程启动之前把装载这些驱动程序,这时SCM还没有启动呢!

◎ Type--用于指定服务的类型,既然我们这里讲的是KMD的编程,那么我们只对一个取值感兴趣,那就是SERVICE_KERNEL_DRIVER (1)

    仔细观察图2.1后,你对beeper.sys有什么要说的吗?好的,我们看到beeper这个内核模式驱动程序位于C:/masm32/Ring0/Kmd/Article2/beeper目录下,它的名称为"Nice Melody Beeper",由用户控制启动,出错信息不被记录。
    Path前面的"/??"前缀的含义你下面就会知道!
    如果我们要启动SCM数据库中不存在的驱动程序,那么可以在任何时刻在SCP的帮助下动态装入(也许称为DCP/device control program更为贴切,但是微软的术语库中并没有这个词)。

2.3 服务控制程序(SCP)

    从名称理解,服务控制程序(service control program/SCP)可以控制服务或者设备驱动程序,这些功能是在SCM的管理下,通过调用适当的函数来完成的,这些函数位于/%SystemRoot%/System32/advapi.dll (Advanced API)中。
    这里是一段关于使用SCP来控制beeper.sys驱动的代码例子

;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;  Service Control Program for beeper driver
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                             I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include /masm32/include/windows.inc
include /masm32/include/kernel32.inc
include /masm32/include/user32.inc
include /masm32/include/advapi32.inc
includelib /masm32/lib/kernel32.lib
includelib /masm32/lib/user32.lib
includelib /masm32/lib/advapi32.lib
include /masm32/Macros/Strings.mac
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
start proc
local hSCManager:HANDLE
local hService:HANDLE
local acDriverPath[MAX_PATH]:CHAR
    invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    .if eax != NULL
        mov hSCManager, eax
        push eax
        invoke GetFullPathName, $CTA0("beeper.sys"),sizeof acDriverPath,addr acDriverPath,esp
        pop eax
        invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), /
                SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, /
                SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
        .if eax != NULL
            mov hService, eax
            invoke StartService, hService, 0, NULL
            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), /
                            NULL, MB_ICONSTOP
    .endif
    invoke ExitProcess, 0
start endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end start

2.3.1 建立到SCM的连接

    在上面的例子中,我们首先要做的事情是使用OpenSCManager函数来建立到SCM的连接,以便在指定的计算机上打开服务数据库。

OpenSCManager proto lpMachineName:LPSTR, lpDatabaseName:LPSTR, dwDesiredAccess:DWORD

    函数使用的参数说明如下:
◎ lpMachineName--指向需要打开的计算机名字符串,字符串以NULL结尾,如果参数指定为NULL,表示连接到本机上的SCM
◎ lpDatabaseName--指向以NULL结尾的包含SCM数据库名称的字符串,字符串应该指定为"ServicesActive",如果参数指定为NULL,则默认打开"ServicesActive"

.const
szActiveDatabase db "ServicesActive", 0
SERVICES_ACTIVE_DATABASE equ offset szActiveDatabase

    现在我们要打开的就是这个当前被激活的数据库,所以我们使用了NULL参数

◎ dwDesiredAccess--指定访问SCM的权限,这个参数告诉SCM我们需要进行什么样的操作,常用的取值有三个:
· SC_MANAGER_CONNECT--允许连接到SCM,这个取值是默认值,它的定义就是0
· SC_MANAGER_CREATE_SERVICE--允许创建服务
· SC_MANAGER_ALL_ACCESS--允许进行所有的操作

    我们可以使用下面的代码连接到SCM:
    invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    .if eax != NULL
        mov hSCManager, eax

    如果OpenSCManager函数执行成功,那么返回值就是被连接的SCM的句柄,我们在以后使用其他函数在对SCM数据库进行操作的时候会用到这个句柄。
    另外,忘了提醒大家,安装内核模式驱动程序需要超级用户的权限,为了安全起见,普通权限的用户没有被授权的话是无法执行特权代码的。当然,本文的例子总是假设你是有超级用户权限的。

2.3.2 安装一个新的驱动

    打开SCM后,我们可以用CreateService函数将驱动添加到服务数据库中,这里是该函数的原型,CreateService函数远不止三个参数,但不要害怕,这些参数都是很简单的:
CreateService proto hSCManager:HANDLE, lpServiceName:LPSTR, lpDisplayName:LPSTR, /
                    dwDesiredAccess:DWORD, dwServiceType:DWORD, dwStartType:DWORD, /
                    dwErrorControl:DWORD, lpBinaryPathName:LPSTR, lpLoadOrderGroup:LPSTR, /
                    lpdwTagId:LPDWORD, lpDependencies:LPSTR, lpServiceStartName:LPSTR, /
                    lpPassword:LPSTR

参数说明如下:

◎ hSCManager--不用说了吧?就是上一节中得到的SCM句柄
◎ lpServiceName--指向一个以0字符结尾的表示服务名称的字符串,字符串的最大长度是256个字符,名称中不允许使用/或者/字符(因为这些字符会和注册表的路径表示方式冲突),这个值和注册表中的键名是相对应的
◎ lpDisplayName--指向一个以0字符结尾表示服务名称的字符串,这个名称是供用户界面程序识别函数时使用的,同样,它的最大长度也是256个字符。这个值和注册表中的DisplayName键的值是相对应的
◎ dwDesiredAccess--指定需要访问服务的操作,可以有以下取值:
· SERVICE_ALL_ACCESS--可以进行所有操作
· SERVICE_START--允许调用StartService函数来启动服务
· SERVICE_STOP--允许调用ControlService函数来停止服务
· DELETE--允许调用DeleteService函数来删除服务

    在这里我们只需要做两件事情:启动驱动和删除驱动,所以例子中使用了SERVICE_START和DELETE,我们不需要停止服务的操作,因为上面已经说过,这个驱动在初始化的时候就会返回错误(所以它不会有已经启动的状态)。
◎ dwServiceType--服务的类型,我们的教程中只用得到SERVICE_KERNEL_DRIVER,这个值和注册表中的Type键的值是相对应的
◎ dwStartType--表示在什么时候启动服务,如果我们需要手动启动驱动的话,那么使用SERVICE_DEMAND_START参数,如果驱动程序需要在系统启动的时候就被启动,那么使用SERVICE_AUTO_START参数,这个取值和注册表中的Start键的取值是相对应的
◎ dwErrorControl--表示当驱动初始化的时候出错该如何处理,取值SERVICE_ERROR_IGNORE表示忽略错误,取值SERVICE_ERROR_NORMAL表示将错误记录到系统日志中去,这个取值和注册表中的ErrorControl键值是相对应的
◎ lpBinaryPathName--指向以0结尾的表示驱动程序文件名的字符串,这个值和注册表中的ImagePath的键值是相对应的
◎ lpLoadOrderGroup--指向以0结尾的表示组名称的字符串,表示该驱动属于哪个组,既然我们的例子程序不属于任何组,那么这里就用NULL好了
◎ lpdwTagId--指向一个32位的缓冲区,用来接收驱动在lpLoadOrderGroup参数指定的组中的唯一的标识,我们的例子中不需要用到这个表示,所以参数指定为NULL
◎ lpDependencies--对于驱动程序来说,这个参数没什么用途,设置为NULL好了
◎ lpServiceStartName--指向一个以0结尾的表示帐号名称的字符串,用于指定服务允许在哪个帐号下运行,如果服务类型是SERVICE_KERNEL_DRIVER的话,该帐号就是系统装入服务的模块名称,我们在这里使用NULL,表示由默认的模块装入
◎ lpPassword--对于驱动程序来说,这个参数没什么用途,设置为NULL好了

    现在来总结一下,最后的5个参数总是设置为NULL,我们就把它抛到脑后去好了,第一个参数是SCM句柄,而dwDesiredAccess参数也是很好理解的,剩下的参数是什么?聪明的你一定已经猜到了--它们实际上就是和注册表里面的键一一对应的!看看下表就明白了:
CreateService函数的参数   注册表
-----------------------   -------------
lpServiceName             键名
lpDisplayName             DisplayName
dwServiceType             Type
dwStartType               Start
dwErrorControl            ErrorControl
lpBinaryPathName          ImagePath

表2.1 参数和注册表键的对应关系

    好了,现在回过头来看看例子代码:
        push eax
        invoke GetFullPathName,$CTA0("beeper.sys"),sizeof acDriverPath,addr acDriverPath,esp
        pop eax
        invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), /
                SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, /
                SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
        .if eax != NULL
            mov hService, eax

    首先,我们调用GetFullPathName函数来获取全路径的驱动程序文件名,并把它传递给CreateService函数。
    然后CreateService函数将这个驱动程序加入到SCM的数据库中,并创建对应的注册表键,正如表2.1所示的,所有这些键将被CreateService函数加入到注册表中,如果你在源代码中把DeleteService一行去掉,将csp.asm重新编译并执行,就可以验证我说的了。
    不要认为使用RegXXX之类的函数将相同的信息写入注册表就可以达到相同的结果,这样操作的话,键值是写到注册表里面了,但是SCM的数据库里面可什么都没有哦!
    如果SCM数据库中指定的设备驱动程序已经存在,那么CreateService函数会返回一个错误,这时可以调用GetLastError函数获取具体原因,上例中会得到ERROR_SERVICE_EXISTS。如果CreateService函数成功地将驱动加入到了SCM数据库中,函数的返回值就是驱动的句柄,这个句柄在后面的驱动管理函数中将会被用到。

2.3.3 启动驱动程序

    下一步要调用的函数是StartService,它的原型申明如下:

StartService proto hService:HANDLE, dwNumServiceArgs:DWORD, lpServiceArgVectors:LPSTR

    参数说明如下:

◎ hService--就是上一小节中由CreateService返回的驱动的句柄
◎ dwNumServiceArgs--用于驱动程序的时候,这个参数总是设置为NULL
◎ lpServiceArgVectors--同上,也为NULL

    启动驱动的方法就是这样的:

    invoke StartService, hService, 0, NULL

    StartService函数的执行过程和装入用户模式的DLL的过程类似,驱动程序文件的映像被装入到系统的地址空间中,文件可以被装入到任何地址中,然后系统会根据PE文件中的重定位表对其进行重定位操作,这样驱动程序的内存映像就被准备好了,接下来系统调用驱动的入口函数,也就是DriverEntry子程序,和装入DLL不同的是,DriverEntry子程序的执行是在系统进程的上下文中进行的。
    StartService函数的调用是同步执行的,也就是说,只有驱动程序的DriverEntry过程返回后,函数才会返回(回想一下,如果函数不等人家执行完就直接返回了,那叫什么~~~那是异步!)。如果驱动初始化成功,那么DriverEntry过程应该返回STATUS_SUCCESS,这样StartService会返回一个非0值,这时,我们又回到了调用StartService的用户模式的上下文中了。
    在这个例子中,我们并不关心StartService函数的返回值,理由前面已经说过了,那就是beeper驱动程序在DriverEntry中进行了发声音功能的演示,并返回一个错误码,后面再没有什么功能要做的了。

2.3.4 卸载驱动

    怎样卸载驱动呢?
            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager

    现在我们需要将系统恢复到以前的状态,调用DeleteService函数就可以将驱动从SCM数据库中删除,比较奇怪的是,并不需要将SCM句柄传递给DeleteService函数。
    DeleteService函数的原型申明如下:

DeleteService proto hService:HANDLE

    参数hService就是需要被卸载的服务的句柄
    严格地说,这个函数并不真正将服务删除,它仅仅是将服务做了一个删除标志,只有当服务已经停止,并且服务的句柄被关闭后,SCM才真正将服务删除。调用了DeleteService函数后,我们还需要将服务的句柄保存以便在后面使用。如果再次调用DeleteService函数的话,函数会返回失败,这时用GetLastError得到的错误代码是ERROR_SERVICE_MARKED_FOR_DELETE。
    现在我们不再需要和驱动程序通讯了,所以需要使用CloseServiceHandle函数将句柄关闭:

CloseServiceHandle proto hSCObject:HANDLE

    参数hSCObject可以是服务或驱动的句柄,也可以是SCM数据库的句柄,驱动的句柄被关闭后,我们再次调用CloseServiceHandle函数来关闭SCM句柄。

2.4 字符串操作的宏

    最后来解释一下源代码中的$CTA0是什么东东--这是一个宏,用来在只读数据段中定义一个以0结尾的字符串,它可以在invoke宏指令中使用,这不是唯一用到的宏,在/Macros/Strings.mac文件中还包括很多其他有用的宏,这些宏都是用于定义字符串的,文件中也有怎样使用它们的详细的解释。既然本教程的重点是讲述KMD的编程,那么我就不在这些宏上面做过多的解释了,但是后面的程序中有很多地方会用到它们。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值