驱动开发学习笔记(3-3)–Four-F的驱动开发教程-服务

在这里下载本文的源代码

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驱动的代码例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;  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:

1
2
3
   invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    .if eax != NULL
        mov hSCManager, eax

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

2.3.2 安装一个新的驱动

打开SCM后,我们可以用CreateService函数将驱动添加到服务数据库中,这里是该函数的原型,CreateService函数远不止三个参数,但不要害怕,这些参数都是很简单的:

1
2
3
4
5
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参数也是很好理解的,剩下的参数是什么?聪明的你一定已经猜到了–它们实际上就是和注册表里面的键一一对应的!看看下表就明白了:

1
2
3
4
5
6
7
8
CreateService函数的参数   注册表
-----------------------   -------------
lpServiceName             键名
lpDisplayName             DisplayName
dwServiceType             Type
dwStartType               Start
dwErrorControl            ErrorControl
lpBinaryPathName          ImagePath

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

好了,现在回过头来看看例子代码:

1
2
3
4
5
6
7
8
        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 卸载驱动

怎样卸载驱动呢?

1
2
3
4
5
6
         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的编程,那么我就不在这些宏上面做过多的解释了,但是后面的程序中有很多地方会用到它们。

原文链接:http://211.90.241.130:22366/view.asp?file=321

You may also like

发表评论

电子邮件地址不会被公开。 必填项已用*标注