64位内核开发第三讲,内核中字符串编程注意事项

windows内核下字符串操作详解

一丶内核字符串简介

1.1 简介

在Windows内核下 和 应用程序一样,驱动程序需要经常和字符串打交道。 其中包括了 ASCII 字符串丶宽字符串,还有DDK定义的 ANSI_STRING STRING UNICODE_STRING 等结构。下面就详细说明。

二丶 ASCII字符串和宽字符串简介

2.1 ASCII字符串和宽字符串类型和使用

在应用程序中,往往 使用两种字符串,一种是char 类型的字符串,这种类型字符串一般都是一个 char 数组指针,指针负责记录 ANSI的字符集。每个 char 类型的变量都为一个字节 字符串是以 0 标志字符串结尾。 如下:

char *pszName = "HelloWorld";
char pszName[] = "HelloWorld";

另一种是 wchar_t 类型宽字符串 它是负责描述 unicode 字符集的字符串,同第一种方式一样,唯一区别就是 wchar_t类型的字符大小占用两个字节,字符串结尾同样以0标志结尾

wchar_t 类型的字符串在使用的时候 我们必须 在字符串前边加 L

wchar_t *pszName = "HelloWorld";
wchar_t pszName[] = "HelloWorld";

在驱动程序开发中 DDK将 char 和 wchar_t 分别重定义为了 CHAR WCHAR 类型 并且分别为其提供了指针类型 PCHAR PWCHAR

所以在使用的时候我们可以进行如下使用

WCHAR *pszName = L"HelloWorld";
CHAR * pszName = L"HelloWorld";
PWCHAR pszName = L"HelloWorld";
PCHAR  pszName = L"HelloWorld";

2.2 ASCII字符串和宽字符串的操作

操作ASCII字符串和宽字符串 都可以使用 C语言标准库中提供的函数

如针对ASCII字符串可以使用

strchr
strcoll
strlen
strncat
strncmp
strncpy
strcpy
strtok
strstr

等等,详情可以看一下 string.h 头文件中定义的函数 都是可以用的。

宽字符则如下(只列出常用的)

wcscat
wcschr
wcscmp
wcscpy
wcslen
wcsncat
wcsncmp

等等

其中标准中的 内存操作函数也是可以用的

memmove
memset
memcpy

三丶 ANSI_STRING STRING 与 UNICODE_STRING

3.1 简介与结构

在DDK编程中,它并不想让程序员使用 C语言字符串,主要原因就是标准的C字符串处理函数容易导致缓冲区溢出等错误。如果程序员在使用字符串的时候不进行校验那么就很容易出现错误导致蓝屏。

从而 DDK定义了结构来表示字符串。

其中 STRING ANSI_STRING 是一个定义只不过名字不一样。

我们看下三个成员都是怎么定义的吧。

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    USHORT * Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;
typedef struct _STRING {
    USHORT Length;
    USHORT MaximumLength;
    PCHAR Buffer;
} STRING;
typedef STRING *PSTRING;
typedef STRING ANSI_STRING;

其实它们本质都是一个结构体,总共三个成员。

成员1: 记录字符串的长度

成员2: 记录字符串的最大长度

成员3: 记录字符串指针。

请注意成员三记录的地址里面存储了字符串,但是并不是以0结尾的。字符串长度需要根据成员1来决定。

3.2 字符串的格式化输出

在内核中我们可以使用 DbgPrint 以及 KdPrint 来进行输出。可以参考 C 语言的 printf函数

如果要对 ANSI_STRING 进行输出的话那么例子如下:

ANSI_STRING ansiStr;
KdPrint(()"%Z \n",&ansiStr);

而针对 UNICODE_STRING 则为如下

UNICODE_STRING ustr;
KdPrint(("%wZ\n",&ustr));

如果是在调试状态下 KdPrint 是不会输出内容的。而 KdPrint是个宏 所以我们括号要使用两个。

四丶字符串的初始化与销毁

4.1 ANSI_STRING 与 UNICODE_STRING 的初始化

  • Rtl函数初始化方法

针对这两个结构 要进行初始化 DDK也提供了函数来给我们使用

分别为如下:

 RtlInitAnsiString
 RtlInitEmptyAnsiString
 RtlInitEmptyUnicodeString
 RtlInitUnicodeString
 RtlUnicodeStringInit

RtlUnicodeStringInit 需要包含 ntstrsafe.h 头文件。 它的实现在 Ntstrsafe.lib 库中。

这几个的参数都很简单,不懂得查询下API得使用即可。

UNICODE_STRING 用得比较多,那么列举一个例子。

NTSYSAPI
VOID
NTAPI
RtlInitUnicodeString(
    __out PUNICODE_STRING DestinationString,
    __in_z_opt __drv_aliasesMem PCWSTR SourceString
    );

参数1: 是你要初始化的 UNICODE_STRING结构的地址,此函数负责为你填充这个结构体。所以需要传地址。

参数2: 初始化的宽字符

例子:

 UNICODE_STRING ustr;
 RtlInitUnicodeString(&ustr, L"abc");
  • 内核DDK宏直接初始化
 ANSI_STRING ansi = RTL_CONSTANT_STRING("HelloWorld");
 UNICODE_STRING ustr = RTL_CONSTANT_STRING(L"HelloWorld");

RTL_CONSTANT_STRING 此宏可以进行在定义的时候直接初始化,初始化后 UN/AN 结构里面的Buffer指向的是常量字符串地址。

  • 初始化的本质

    ANSI/UNICODE 结构 里面都有一个pBuffer指针 这个指针是来指向字符串的。

    但是我们要了解本质。 初始化还分为 常量初始化 栈内存初始化 堆内存初始化. 其实本质是将pBuffer指针指向你初始化字符串的地址。 如果你字符串是一个常量字符串,那么自然指向的就是 常量区的地址, 如果你是用栈初始化的 那么自然指向你的栈内存,如果指向堆内存 那么自然指向的就是堆内存。

例子:

 1.常量初始化

UNICODE_STRING ustr = RTL_CONSTANT_STRING(L"HelloWorld");

本质是如下:(伪代码)

UNICODE_STRING ustr;
ustr.length = wcslen(L"HelloWorld");
ustr.MaxLength = wcslen(L"HelloWorld")+1;
ustr.pBuffer = &L"HelloWorld";

如果你想修改 ustr只能重新修改指向了。

  1. 堆栈初始化
 UNICODE_STRING ustr = {0};
 WCHAR szBuffer[100] = L"HelloWorld";
 RtlInitUnicodeString(&ustr, szBuffer);

看着代码很正常但是 ustr.pBuffer的指向指向的是 szBuffer

如果你修改szBuffer的内容那么ustr.pBuffer的内容也会更改

原理(伪代码)

ustr.pBuffer = szBuffer;

3.堆内存初始化

同堆栈初始化一样,只不过 buffer是我们申请的堆内存了。一般我们也建议这样使用。

因为修改堆内存之后 ustr.pbuffer才会更改

 PWCHAR szBuffer = (PWCHAR)ExAllocatePoolWithTag(NonPagedPool, 'abcd');
    if (szBuffer != NULL)
    {
        wcscpy(szBuffer, L"HelloWorld");
    }
    RtlInitUnicodeString(&ustr, szBuffer);

但是如果这样使用请一定注意要释放内存,否则就会内存泄漏。

五丶字符串操作常见函数

5.1 字符串复制

字符串复制函数针对 ANSI_STRING UNICODE_STRING 都分别提供了。

函数如下:

 RtlCopyString
 RtlCopyUnicodeString
 RtlUnicodeStringCbCopyStringN
 RtlUnicodeStringCbCopyN
 RtlUnicodeStringCbCopyNEx
 RtlUnicodeStringCbCopyStringNEx
 RtlUnicodeStringCopyString

其中最长使用的就是前两个函数 用于分别 拷贝 ANSI_STRING 或者 UNICODE_STRING

下面的 cb类型的函数都是可以拷贝 宽字符到 UNICODE_STRING中 下面的也会遇到 但主题说的是拷贝所以不在多说。如果想使用这类函数 一定要包含头文件 <ntstrsafe.h> 以及它对应的库文件。详情查询MSDN吧。

5.2 字符串比较

字符串比较同样提供一组函数

RtlCompareString()
RtlCompareUnicodeString()
RtlCompareUnicodeStrings()
RtlEqualString()
RtlEqualUnicodeString()

比较的时候是比较两个 结构的字符串是否相等

取一个举例子

LONG
NTAPI
RtlCompareUnicodeString(
    __in PCUNICODE_STRING String1,
    __in PCUNICODE_STRING String2,
    __in BOOLEAN CaseInSensitive
    );

参数1: 要比较的第一个字符串

参数2: 要比较的第二个字符串

参数3: 是否对大小写敏感

返回值: 比较结果 0 字符串相等 1 代表第一个字符串大于第二个字符串 小于0 代表第一个字符串小于第二个字符串

所以记住 返回0 是代表字符串相等

5.3 字符串转为大写

字符串的大小写转换也是很烦人的,索性 内核提供了 字符串转大写的函数

RtlUpcaseUnicodeChar()
RtlUpcaseUnicodeString()
RtlUpperString()
RtlUpperChar()

常用的就两个

RtlUpcaseUnicodeString RtlUpperString 一个是将UNICODE_STRING 转为大写,另一个自然就是将 ANSI_STRING 转为大写了 而提供了 两外两个函数则是将 字符 转为 大写字符

例子:

NTSYSAPI NTSTATUS RtlUpcaseUnicodeString(
  [in, out] PUNICODE_STRING  DestinationString,
  [in]      PCUNICODE_STRING SourceString,
  [in]      BOOLEAN          AllocateDestinationString
);

参数1: 目的字符串

参数2: 源字符串

参数3: 是否为目的字符串申请内存

返回值: 返回转换是否成功

要注意,UNICODE_STIRNG 转为大写多了一个参数,就是参数3.它的意思就是转换后要不要给参数一传递的UNICODE_STRING 申请空间。 如果要的话 那么你要注意使用

RtlFreeUnicodeString 来释放空间 而 ASCII版本没有这个参数,所以代表着你的参数一在传递的时候一定要自己预留出一个空间来。这样才能存放结果。

5.4 字符串与整形数字的相互转换

    1. 字符串转为整数

UNICODE_STRING 可以转换为整数。 那么自然整数也可以转化为 UNICODE_STRING

API如下:

NTSTATUS
NTAPI
RtlUnicodeStringToInteger (
    __in PCUNICODE_STRING String,
    __in_opt ULONG Base,
    __out PULONG Value
    );

参数1: 需要你进行转化的字符串

参数2: 转换的进制数,你要转化为多少进制,如 2 8 10 16

参数3: 转换后的结果数字

返回值: 指明是否转换成功

    1. 整数转为字符串

整数也可以转化为字符串

其中提供的函数有两个

NTSTATUS
NTAPI
RtlIntegerToUnicodeString (
    __in ULONG Value,
    __in_opt ULONG Base,
    __inout PUNICODE_STRING String
    );
NTSTATUS
NTAPI
RtlInt64ToUnicodeString (
    __in ULONGLONG Value,
    __in_opt ULONG Base,
    __inout PUNICODE_STRING String
    );

参数1: 你想转化的数值(注意用64的代表你要给一个64的值)

参数2: 转换的进制数(2 8 10 16)

参数3: 转换成功后存储结果的字符串(注意你要预留空间)

返回值: 是否成功

使用例子:

5.5 ANSI_STRING 与 UNICODE_STRING 字符串的互相转换

    1. UNICODE_STRING 转化为 ANSI_STRING
NTSYSAPI NTSTATUS RtlUnicodeStringToAnsiString(
  [in, out] PANSI_STRING     DestinationString,
  [in]      PCUNICODE_STRING SourceString,
  [in]      BOOLEAN          AllocateDestinationString
);
    1. ANSI_STRING 转化为 UNICODE_STRING
NTSYSAPI NTSTATUS RtlAnsiStringToUnicodeString(
  [in, out] PUNICODE_STRING DestinationString,
  [in]      PCANSI_STRING   SourceString,
  [in]      BOOLEAN         AllocateDestinationString
);

函数原理一样

参数1: 代表你需要被转化的字符串 目的字符串

参数2: 你要转化的源字符串

参数3: 是否为目的字符串申请内存

返回值: 是否转换成功

就上将源字符转化为目的字符串。 如果需要申请内存 请一定注意使用 RtlFreexxxString

5.6 字符串的拼接操作

字符串拼接 自然就是 cat的操作 DDK也为我们提供了函数

RtlStringCbCatA    // 
RtlStringCbCatNA
RtlUnicodeStringCat   // 追加一个UNICODE_STRING
RtlUnicodeStringCatEx 
RtlUnicodeStringCatString  //追加一个 WCHAR*数据
RtlUnicodeStringCbCatN     //按照大小追加

5.7 字符串的格式化-wsprintf..

 RtlUnicodeStringPrintf

例子:

 RtlUnicodeStringPrintf(&buffer,
                         L"OsrUsbFX2 RawPdo For Switch %d",
                         pDesc->SwitchNumber);

具体的话使用的时候在进行查询吧。

posted @ 2019-06-08 14:46  iBinary  阅读(1630)  评论(0编辑  收藏  举报