内核中使用 字符串

1.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++特性的编码者则容易忽略这个问题。因为常常使用std::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的内核是使用Uincode编码的。ANSI_STRING仅仅在某些碰到窄字符的场合使用。而且这种场合非常罕见。

UNICODE_STRING并不保证Buffer中的字符串是以空结束的。因此,类似下面的做法都是错误的,可能会会导致内核崩溃:
UNICODE_STRING str;
…
len = wcslen(str.Buffer);           // 试图求长度。
DbgPrint(“%ws”,str.Buffer);       // 试图打印str.Buffer。

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


 

1.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 = RTL_CONSTANT_STRING(L“my first string!”);

 

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

 

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

 

用本小节的方法初始化的字符串,不用担心内存释放方面的问题。因为我们并没有分配任何内存。

 

1.3 字符串的拷贝

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

下面举一个例子。
UNICODE_STRING dst;         // 目标字符串
WCHAR dst_buf[256];             // 我们现在还不会分配内存,所以先定义缓冲区
UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);

// 把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串。
RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src);    // 字符串拷贝!

以上这个拷贝之所以可以成功,是因为256比L” My source string!”的长度要大。如果小,则拷贝也不会出现任何明示的错误。但是拷贝结束之后,与使用者的目标不符,字符串实际上被截短了。

我曾经犯过的一个错误是没有调用RtlInitEmptyString。结果dst字符串被初始化认为缓冲区长度为0。虽然程序没有崩溃,却实际上没有拷贝任何内容。
在拷贝之前,最谨慎的方法是根据源字符串的长度动态分配空间。在1.2节“内存与链表”中,读者会看到动态分配内存处理字符串的方法。

 

1.4 字符串的连接

UNICODE_STRING不再是简单的字符串。操作这个数据结构往往需要更多的耐心。读者会常常碰到这样的需求:要把两个字符串连接到一起。简单的追加一个字符串并不困难。重要的依然是保证目标字符串的空间大小。下面是范例:

 

NTSTATUS status;
UNICODE_STRING dst;         // 目标字符串
WCHAR dst_buf[256];             // 我们现在还不会分配内存,所以先定义缓冲区
UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);

// 把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src);    // 字符串拷贝!

status = RtlAppendUnicodeToString(
        &dst,L”my second string!”);
if(status != STATUS_SUCCESS)
{
    ……
}
NTSTATUS是常见的返回值类型。如果函数成功,返回 STATUS_SUCCESS。否则的话,是一个错误码。RtlAppendUnicodeToString在目标字符串空间不足的时候依然可以连接字符 串,但是会返回一个警告性的错误STATUS_BUFFER_TOO_SMALL。
另外一种情况是希望连接两个UNICODE_STRING,这种情况请调用RtlAppendUnicodeStringToString。这个函数的第二个参数也是一个UNICODE_STRING的指针。

1.5 字符串的打印

字符串的连接另一种常见的情况是字符串和数字的组合。有时数字需要被转换为字符串。有时需要把若干个数字和字符串混合组合起来。这往往用于打印日志的时候。日志中可能含有文件名、时间、和行号,以及其他的信息。

熟悉C语言的读者会使用sprintf。这个函数的宽字符版 本为swprintf。该函数在驱动开发中依然可以使用,但是不安全。微软建议使用RtlStringCbPrintfW来代替它。 RtlStringCbPrintfW需要包含头文件ntstrsafe.h。在连接的时候,还需要连接库ntsafestr.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,L”file path = %wZ file size = %d \r\n”,
    &file_path,file_size);
// 这里调用wcslen没问题,这是因为RtlStringCbPrintfW打印的
// 字符串是以空结束的。
dst->Length = wcslen(dst->Buffer) * sizeof(WCHAR);

RtlStringCbPrintfW在目标缓冲区内存不足的时候依 然可以打印,但是多余的部分被截去了。返回的status值为STATUS_BUFFER_OVERFLOW。调用这个函数之前很难知道究竟需要多长的缓 冲区。一般都采取倍增尝试。每次都传入一个为前次尝试长度为2倍长度的新缓冲区,直到这个函数返回STATUS_SUCCESS为止。

值得注意的是UNICODE_STRING类型的指针,用%wZ打印可以打印出字符串。在不能保证字符串为空结束的时候,必须避免使用%ws或者%s。其他的打印格式字符串与传统C语言中的printf函数完全相同。可以尽情使用。
另外就是常见的输出打印。printf函数只有在有控制台输出的情况下才有意义。在驱动中没有控制台。但是Windows内核中拥有调试信息输出机制。可以使用特殊的工具查看打印的调试信息(请参阅附录1“WDK的安装与驱动开发的环境配置”)。
驱动中可以调用DbgPrint()函数来打印调试信息。这 个函数的使用和printf基本相同。但是格式字符串要使用宽字符。DbgPrint()的一个缺点在于,发行版本的驱动程序往往不希望附带任何输出信 息,只有调试版本才需要调试信息。但是DbgPrint()无论是发行版本还是调试版本编译都会有效。为此可以自己定义一个宏:
#if DBG
    KdPrint(a) DbgPrint##a
#else
    KdPrint (a)
#endif

不过这样的后果是,由于KdPrint (a)只支持1个参数,因此必须把DbgPrint的所有参数都括起来当作一个参数传入。导致KdPrint看起来很奇特的用了双重括弧:

 

// 调用KdPrint来进行输出调试信息
status = KdPrint ((
    L”file path = %wZ file size = %d \r\n”,
    &file_path,file_size));

 

这个宏没有必要自己定义,WDK包中已有。所以可以直接使用KdPrint来代替DbgPrint取得更方便的效果。

 

 

 

转载于:https://www.cnblogs.com/DreamOfGalaxy/articles/4242294.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值