Windowsx86内核(三)

Windowsx86内核(三)

从现在开始内核编程的基础知识。
对于理解Windows内核,主要有两个障碍:
一是Windows没有提供源码,所以有时候不得不看汇编语言;
二是对内核的C语言编程方法不熟悉。

注意以下代码运行和调试都要安装WDK,并包含WDK相应文件头,并使用WinDbg调试。
现在先分析下内核程序的编程方法。

一、字符串的处理

1.使用字符串结构

常常使用传统的C语言的程序员比较喜欢用如下的方法定义和使用字符串:

char *str={"my first string"};              #Ansi 字符串
wchar_t *wstr={L"my first string"};         #Unicode 字符串
size_t len = strlen(str);                   #Ansi 字符串求长度
size_t wlen = wcslen(wstr);                 #Unicode 字符串求长度
printf("%s %ws %d %d",str,wstr,len,wlen);   #打印两种字符串

但实际上这种字符串相当的不安全,很容易导致缓冲区溢出漏洞。这是因为没有任何地方确切地表明一个字符串的长度,仅仅用一个“\0”字符来标明这个字符串的结束。一旦碰到根本就没有空结束的字符串(可能是攻击者恶意的输入,或者是编程错误导致的意外),程序就可能陷入崩溃。

如果使用的是C++特性的编码者则容易忽略这个问题,因为常常使用string和CString这样高级的类,不哦那个去担忧字符串的安全性。

在驱动开发中,一般不在用空来表示一个字符串的结束,而是定义了如下的一个结构:

typedef struct _UNICODE_STRING {
    USHORT Length;                    //字符串的长度         
    USHORT MaximumLength;             //字符串缓冲区的长度
    PWSTR Buffer;                     //字符串缓冲区
}UNICODE_STRING, *PUNICODE_STRING

以上是Unicode字符串,一个字符为双字节。与之对应的还有一个Ansi字符串,Ansi字符串就是C语言中常用的单字节表示一个字符的窄字符串。

typedef struct _STRING {
        USHORT Length;
        USHORT MaximumLength;
        PSTR Buffer;
}ANSI_STRING, *PANSI_STRING

在驱动开发中到处可见的是Unicode字符串,因此可以说:Windows的内核是使用Unicode编码的。ANSI_STRING仅仅在某些碰到窄字符的场合使用,而且非常罕见。
所以:
Windows 内核使用Unicode编码。内核中所见的大多数字符串大多为UNICODE_STRING结构

然而UNICODE_STRING并不保证Buffer中的字符串是以空结束的。因此,类似下面的做法都是错误的,可能会导致内核崩溃:

UNICODE_STRING str;
....
len = wcslen(str.Buffer);           //试图求长度
DbgPrint("%ws",str.Buffer);         //试图打印str.Buffer

如果要使用以上的方法,必须在编码中保证Buffer始终以空结束,但这又是一个麻烦的问题。所以,使用微软提供的Rtl系列函数来操作字符串,才是正确的方法。
下面将逐步地阐述这个系列函数的使用。

2.字符串的初始化

回顾之前的UNICODE_STRING结构。我们可以注意到,这个结构并不含有字符串缓冲的空间。这是一个初学者常见的出问题的来源。
举个内核会崩溃的例子:

UNICODE_STRING str;
wcscpy(str.Buffer,L"my first string!");
str.Length=str.MaximumLength=wcslen(L"my first string!") *sizeof(WCHAR)

以上代码定义了一个字符串并试图初始化它的值。但是非常遗憾,这样做是不对的。因为str.Buffer只是一个未初始化的指针,它并没有指向有意义的空间。相反,以下方法是正确的:

UNICODE_STRING str;
str.Buffer=L"my first string!";
str.Length=str.MaximumLength=wcslen(L"my first string!") *sizeof(WCHAR)
...

实际上,明显的初始化如下:

UNICODE_STRING str ={
        sizeof(L"my first string!") - sizeof((L"my first string!")[0]),
        sizeof(L"my first string!"),
        L"my first string!"
};

然而这样定义一个字符串实在是太繁琐了,但在头文件ntdef.h中有一个宏方便这种定义。使用这个宏后,我们就可以简单地定义一个常熟字符串如下:

#include <ntdef.h>
UNICODE_STRING str= PTR_CONSTANT_STRING(L"my first string!");

这个只能在定义这个字符串的时候使用。为了随时初始化一个字符串,可以使用RtlInitUnicodeString。示例如下:

UNICODE_STRING str;
RtlInitUnicodeString(&str,L"my first string!");

用这种方法初始化的字符串,不用担心内存释放的问题,因为并没有分配任何内存。

3.字符串的拷贝

因为字符串不再是空结束,所以使用wcscpy来拷贝字符串是不行的。UNICODE_STRING可以用RtlCopyUnicodeString来拷贝,在进行这种拷贝的时候,最需要注意的一点是:拷贝目的字符串的Buffer必须有足够的空间,如果Buffer的空间不足,字符串会拷贝不完全。
下面举一个例子:

UNICODE_STRING dst;                 //目标字符串
WCHAR dst_buf[256];                 //我们现在还不会分配内存,所以先定义缓冲区
UNICODE_STRING str=RTL_CONST_STRING(L"my first string!");
//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src); //拷贝

主义要调用RtlInitEmptyString,设置缓冲区长度,否则会复制不了任何内容。

4.字符串的连接

UNICODE_SRING 不再是简单的字符串,操作这个数据结构往往需要更多的耐心。
操作字符串,我们常常会遇到这样的问题:要把两个字符串连接在一起。
这并不难,重要的依然是保证目标字符串的空间大小。下面是范例:

NTSTATUS status;
UNICODE_STRING dst;
WCHAR dst_buf[256];
UNICODE_STRING src =RTL_CONST_STRING(L"my first string!");
//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
RtlInitEmptySring(dst,dst_buf.256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src);

status=RtlAppenUnicodeToString(&dst,L"my first string!");
if(status != STATUS_SUCCESS)
{... ...}       

NTSTATUS 是常见的返回值类型,如果函数成功,返回STATUS_SUCCESS;否则。是一个错误码。RtlAppendUnicodeToString在目标字符串空间不足的时候依然可以连接字符串,但是会返回一个警告性的错误STATUS_BUFFER_TOO_SMALL。

另外一种情况是希望连接两个UNICODE_STRING,这种情况调用RtlAppendUnicodeStringToStirng。这个函数的第二个参数也是一个UNICODE_STRING的指针。

5.字符串的打印

熟悉C语音的读者会使用sprintf,这个函数的宽字符版本为swprintf。该函数在驱动开发中依然可以使用,但是不安全,微软建议用RtlStringCbPrintfW来代替它。需要文件头ntstrsafe.h,在连接的时候,还需要加载链接库ntstrsafe.lib。

#include <ntstrsafe.h>

WCHAR buf[512]={0};
UNICODE_STRING dst;
NTSTATUS status;
//字符串初始化字符串。缓冲区长度为512*sizeof(WCHAR)
RtlInitEmptyString(dst,dst_buf,512*sizeof(WCHAR));

//调用RtlStringCBPrintfW来打印
status=RtlStringCBPrintfW(dst->Buffer,512*sizeof(WCHAR),L"file path=%wZ file size = %d \r\n",
&file_path,file_size);
//这里调用wcslen没问题,这是因为RtlStringCBPrintfW打印的
//字符串是以空结束的
dst->Length=wcslen(dst->BUffer)*sizeof(WCHAR);

RtltringCBPrintfW在缓冲区不足的时候依然可以打印,打算多余的部分会被截去。返回的status值为STATUS_BUFFER_OVERFLOW。

值得注意的是,UNICODE_STRING类型的指针,用%wZ打印可以打印出字符串。在不能保证字符串为空结束的时候,必须避免使用%ws或者%s.

驱动中调用DbgPring()函数来打印调试信息。这个函数的使用和pring基本相同,但是格式字符串要实验宽字符。

二、内存与链表

1.内存的分配与释放

内存泄漏是C语音一个臭名昭著的问题。传统C语言使用内存分配通常是malloc。在驱动中这个函数不再有效,最常被调用的是ExAllocatePoolWithTag。下面举一个字符串复制的例子(将一个字符串src拷贝到dst):

//定义一个内存分配标志
#define MEM_TAG 'MyTt'
//目标字符串,接下来它需要分配空间
UNICODE_STRING dst={0};
//分配一个空间给dst,根据源目标字符串的长度
dst.Buffer=
(PWCHAR)ExAllocatePoolWithTag(NonpagedPool,src->Length,MEM_TAG);
if(dst.Buffer==NULL)
{
    //错误处理
    status=STATUS_INSUFFICIENT_RESOURCES;
    .....
}
dst->Length=dst.MaximumLength=stc_Length;
status=RtlCopyUnicodeString(&dst,&src);//复制
ASSERT(status==STATUS_SUCCESS);

ExAllocatePoolWithTag的第一个参数NonpagePool表明分配的内存是锁定内存。
这些内存永远真实存在于物理内存上,不会被分页交换到硬盘上。第二个参数是长度。第三个参数是一个所谓的“内存分配标记”。
内存分配标记是用于检测内存泄漏。
内核中最常用的分配内存的方法是ExAllocatePoolWithTag。

此外,也可以分配可分页内存,使用PagedPool即可。

ExAllocatePoolWithTag分配的内存需要用ExFreePool来释放,如果没释放,则永远泄漏,并不会随着用户关闭进程而自动释放。即使驱动内存动态卸载也不能释放空间,唯一的办法就是重启。

ExFreePool只需提供所要释放的指针即可。

ExFreePool(dst.Buffer);
dst.Buffer=NULL;
dst.Length=dst.MaximumLength=0;

ExFreePool不能用来释放一个栈空间的指针,否则系统会立即崩溃。
像以下代码:

UNICODE_STRING src=RTL_CONST_STRING(L"My source string~@!");
ExFreePool(src.Buffer);

会导致立刻蓝屏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值