C/C++

内核编程 —— 入口函数详解

zeronohacker · 4月5日 · 2020年 151次已读

0x00 内核入口函数

具有 windows 应用层(用户态)开发经验的朋友应该很清楚,windows 应用程序有统一的 WinMain 入口函数,类似于应用层,内核驱动也有一个统一的入口函数,名字叫做 DriverEntry,DriverEntry 函数的原型如下:

NTSTAUS DriverEntry(PDRIVER_OBJECT pDriverObject,PUNICODE_STRING pRegistryPath )

第一个参数为 pDriverObject,表示一个驱动对象指针,一个驱动文件(.sys)运行之后,操作系统在内存中为该驱动分配一个类型为 DRIVER_OBJECT 的数据结构,用于记录该驱动的详细信息,结构定义如下:

typedef struct _DRIVER_OBJECT 
{
    CSHORT Type;
    CSHORT Size;

    // The following links all of the devices created by a single driver
    // together on a list, and the Flags word provides an extensible flag
    // location for driver objects.
    
    PDEVICE_OBJECT DeviceObject; // 设备链 
    ULONG Flags;
    
    // The following section describes where the driver is loaded. The count
    // field is used to count the number of times the driver has had its
    // registered reinitialization routine invoked.
   
    PVOID DriverStart;
    ULONG DriverSize;
    PVOID DriverSection;
    PDRIVER_EXTENSION DriverExtension; // 驱动扩展,对于 WDM 程序比较重要 
 
    // The driver name field is used by the error log thread
    // determine the name of the driver that an I/O request is/was bound.
    
    UNICODE_STRING DriverName; // 驱动名称 

    // The following section is for registry support. This is a pointer
    // to the path to the hardware information in the registry
    
    PUNICODE_STRING HardwareDatabase; // 设备的硬件数据库名称 
   
    // The following section contains the optional pointer to an array of
    // alternate entry points to a driver for "fast I/O" support. Fast I/O
    // is performed by invoking the driver routine directly with separate
    // parameters, rather than using the standard IRP call mechanism. Note
    // that these functions may only be used for synchronous I/O, and when
    // the file is cached.
    
    PFAST_IO_DISPATCH FastIoDispatch; // 文件驱动程序中的快速 IO 请求函数地址 

    // The following section describes the entry points to this particular
    // driver. Note that the major function dispatch table must be the last
    // field in the object so that it remains extensible.
    
    PDRIVER_INITIALIZE DriverInit;
    PDRIVER_STARTIO DriverStartIo; // DriverStartIo 派发函数的地址 
    PDRIVER_UNLOAD DriverUnload; // 卸载函数指针 
    PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1]; // 30 个分发函数
 } DRIVER_OBJECT; 
typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;

以上就是驱动对象结构体信息,在这只提比较重要字段,第一,DeviceObject(设备对象),每个驱动程序都会有一个或多个设备对象,所有设备对象以链表的形式串联起来,驱动对象中的设备对象字段指的是所有设备对象的第一个对象,通过它可以遍历所有设备对象;第二,DriverName(驱动名称),表示的该驱动的名称,采用 UNICODE 编码,该字符串的一般形式为 \Driver[驱动程序名称];第三,DriverUnload(驱动卸载函数),指向该驱动程序的卸载函数地址;第四,MajorFunction(派遣函数指针数组),该数组的每个指针成员指向一个响应的处理 IRP 的派遣函数。第二个参数 pRegistryPath 是一个类型为 UNICODE_STRING 的指针,表示当前驱动所对应的注册表位置。UNICODE_STRING 是内核中表示字符串的结构体,对应定义如下:

// Unicode strings are counted 16-bit character strings. If they are 
// NULL terminated, Length does not include trailing NULL. 
// 
typedef struct _UNICODE_STRING 
{
    USHORT Length;
    USHORT MaximumLength; 
#ifdef MIDL_PASS[size_is(MaximumLength / 2), length_is((Length) / 2) ] USHORT * Buffer; 
#else // MIDL_PASS_Field_size_bytes_part_opt_(MaximumLength, Length) PWCH Buffer; 
#endif // MIDL_PASS 
} UNICODE_STRING; 
typedef UNICODE_STRING *PUNICODE_STRING; 
typedef const UNICODE_STRING *PCUNICODE_STRING;

其中 Buffer 为一个指针,指向一个 UNICODE 类型的字符串缓冲区;MaximumLength 表示 Buffer 所指向缓冲区的总空间大小,一般等于 Buffer 被分配时的内存大小,单位为字节;Length 表示 Buffer 所指向缓冲区中字符串的长度,单位也是字节。

请注意,Buffer 指向的字符串,并不要求以 ‘\0’ 作为结束,在大多数情况下,Buffer 指向的字符串没有以 ‘\0’ 结尾。

请注意,Buffer 指向的字符串,并不要求以 ‘\0’ 作为结束,在大多数情况下,Buffer 指向的字符串没有以 ‘\0’ 结尾。

那如何来验证上面这一事实呢?实践才是验证真理唯一途径。这里我以我写好的一个驱动程序(FirstDriver.sys)作为一个例子来调试,不多说,直接上 windbg,设置好环境后,在虚拟机里以管理员方式运行 cmd,输入如下命令(已禁用驱动强制签名,= 后面有一个空格):

以上就表示创建服务成功了,接下来便是启动服务,在启动服务之前,先对驱动入口函数下个断,如下:

kd> bu FirstDriver!DriverEntry

下断之后,输入 g 命令把控制权转给操作系统,下面就启动服务,把 DebugView 开着,如下:

sc start FirstDriver

启动服务后,中断到 windbg,输入 p 命令单步运行起来,让 pRegistryPath 压入栈中,好了后,输入 kp 查看函数参数,如下:

从上图可看出,地址位于 0x9336c00,接下来,查看结构体内容,输入如下:

kd> dt nt!_UNICODE_STRING 0x00000000`9336c000

从上图可看出 Buffer 字符串长度为 59,又因为为宽字符类型,所以占用 118 字节,用十六进制表示为 0x76,正好为上面 Length 和 MaximumLength 大小,这说明此时的 Buffer 字符串没有 \0 结尾。

pRegistryPath 表示的是这个驱动所对应的注册表位置,我们知道,内核驱动是作为 windows 系统服务存在的,不同服务是通过服务名来识别的。一个 sys 驱动文件需要运行,首先会把该服务信息写入到注册表中,以服务名作为一个注册表的键名,如上图所示。

关于返回值,DriverEntry 的返回值类型为 NTSTATUS,定义如下:

typedef _Return_type_success_(return >= 0) LONG NTSTATUS;

所以说 DriverEntry 的返回值类型实际为 LONG 类型,windows 操作系统规定 DriverEntry 返回 STATUS_SUCCESS 表示成功,返回其他值表示失败。实际上 STATUS_SUCCESS 是一个宏定义,如下:

#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth

内核驱动作为 windows 服务运行,在执行具体代码前,驱动 SYS 文件首先会被映射到内核地址空间,作为内核的一个驱动模块,接着系统对这个驱动模块执行导入表初始化、修正重定位表中对应的数据偏移操作,最后系统会调用该驱动模块的 DriverEntry 入口函数,如果这个入口函数返回 STATUS_SUCCESS,系统认为这个驱动初始化成功;如果这个入口函数返回除 STATUS_SUCCESS 以外的其他值,系统认为驱动初始化失败,系统执行一系列的清理工作,并把驱动模块从内核空间中删除,从用户角度看,就是服务启动失败。

关于驱动卸载函数,当一个内核驱动被要求停止时,DriverObject->DriverUnload 指向的函数就会被系统调用,开发者可以在这个函数中执行一些清理相关工作。DriverUnload 函数非常重要,但重要并不等于必须,DriverUnload 函数是可选的,开发者可以不提供 DriverUnload 函数,这样做的结果是该驱动不支持停止,也就是说,只要开发者不提供 DriverUnload 函数,这个驱动对应的服务一旦启动后,再也无法停止。该特性被很多安全软件利用,刻意不提供 DriverUnload 函数,避免驱动被恶意停止。

提示:驱动初始化失败不会触发 DriverUnload 函数调用,DriverUnload 只有在驱动服务成功启动后,被要求停止才会触发。

0x01 一个基本的通用驱动程序框架

以下为一个基本的通用驱动程序框架:

#include <ntddk.h> 

VOID DriverUnload(PDRIVER_OBJECT pDriverObject) 
{
    if (pDriverObject != NULL)
    {
        DbgPrint("[%s]Driver upload, Driver Object Address: %p", __FUNCTION__, pDriverObject);
    }
    return;
} 

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) 
{
    DbgPrint("[%s]Hello Kernel World!\n", __FUNCTION__);
    if (pRegistryPath != NULL)
    {
        DbgPrint("[%s]Driver RegistryPath: %wZ\n", __FUNCTION__, pRegistryPath);
    }
    if (pDriverObject != NULL)
    {
        DbgPrint("[%s]Driver Object Address: %p\n", __FUNCTION__, pDriverObject);
        pDriverObject->DriverUnload = DriverUnload;
    }
    return STATUS_SUCCESS; 
}

(本文完)

0 条回应

必须 注册 为本站用户, 登录 后才可以发表评论!