内核的基本操作,多线程和数据结构
内核的基本操作
UNICODE_STRING
为什么字符串很重要
- 大型工程中10%~20%的代码都与字符串操作有关
- 面试里面也有很多字符串相关问题
- 字符串涉及到指针操作
- 字符串是编程第二个重要关口
字符串分两类:0结尾和非零结尾
char *
在c语言中使用,以‘\0'结尾
BSTR
在win32编程中使用,前4个字节表示字节长,后面以'\0'结尾,可以理解为是char*
和UNICODE_STRING
的综合
UNICODE STRING
内核函数多以这种字符串作为输入参数,不是以'\0'结尾
定义:
/// unicode编码字符
typedef struct UNICODE_STRING{
USHORT Length; ///< 字节数,不是字符数,而是数据的字节数,字节数 = 字符数 *sizeof(WCHAR)
USHORT MaximumLength; ///< 字节数,告诉系统函数最多有多少内存可用
PWSTR Buffer; ///< 只是一个指针,一旦定义之后并没有内存,需要正确初始化之后,让Buffer含有真正的数据,才拥有内存.**非零结尾,中间也可能含有0**,PWSTR等价于WCHAR
}UNICODE_STRING,*PUNICODE_STRING;
/// 多字节编码字符
typedef struct _STRING{
USHORT Length; ///< 其他参数同上
USHORT MaximumLength; ///< 其他参数同上
PCHAR Buffer; ///< 其他参数同上
}ANSESTRING,*PANSI_STRING;
/// Buffer不是以零结尾,中间也可能含有零,wcscpy/wcscmp等操作其中的Buffer不可靠,
///(如果中间UNICODE_STRING.Buffer或者_STRING.Buffer中间也可能含有零,则会提前截断,如果中间不含有零,则会溢出)
对比:
typedef struct XXX{
USHORT Leng;
...
WCHAR Buffer[MAX_PATH]; ///< 字符数组,一旦定义就拥有内存
}XXX,*PXXX
初始化方式:
用字符串常量对其初始化
UNICODE_STRING uStr={0};///< 直接定义,Buffer没有内存(Buffer = NULL),必须正确初始化,让Buffer含有真正的数据。
/// 在栈上的局部变量未初始化,调试的时候内存显示的是"烫烫烫"
/// 在堆上的局部变量未初始化,调试的时候内存显示的是"屯屯屯"
/// L"Hello,world!"字符串常量存放在静态区的.rdata
WCHAR *szHello = L"Hello,world!";
/// buffer是浅拷贝(只把常量字符串的地址拷贝到buffer中),buffer直接指向L"Hello,world!"
/// Length:wcslen(szHello)*sizeof(WCHAR);
/// MaximumLength:(wcslen(szHello)+1)*sizeof(WCHAR);
RtllnitUnicodeString(&ustrTest,szHello);
上面两句等价于:DECLARE_CONST_UNICODE_STRING(ustrTest,L"Hello,world!);
/// buffer直接指向静态常量L"Hello,world!",常量区内存不可被修改,下面的操作必然会出错。
RtlCopyUnicodeString(&ustrTest,&uStr2);
RtlAppendUnicodeToString(&ustrTest,Str1);
RtlAppendUnicodeStringToString(&ustrTest,&uStr2);
RtlAnsiStringToUnicodeString(&ustrTest,&aStr1,FALSE);
用栈上的buffer或静态区的内存对其初始化
/// 定义并初始化一个UNICODE_STRING字符串
UNICODE_STRING ustrTest=(0};
/// 如果把SZHello定义在函数内部就是在栈上
/// 如果把SZHello定义在全局变量就是在静态区.bss
WCHAR SZHello[512]=L"Hello,world!"
/// 用栈上的buffer或静态区的内存对其初始化,
ustrTest.Buffer = szHello;
ustrTest.Length = wcslen("Lhello,world")*sizeof(WCHAR);
ustrTest.MaximumLength*sizeof(szHello);
用从堆上分配一个内存对其初始化
/// 定义并初始化一个UNICODE_STRING字符串
UNICODE_STRING ustrTest = {0};
/// 设置有效长度
ULONG ulLength =wcslen(L"Hello world")*sizeof(WCHAR);
/// @param MAX_PATH*sizeof(WCHAR)表示待分配的内存的大小(是字节数,在驱动中UNICODE_STRING一般用来表示设备对象名,符号链接,文件路径,所以**字符数**不会超过MAX_PATH)MAX_PATH宏是260(个),表示在windows系统中一个文件的路径最大个**字符数**。(除去一个盘符,一个':',一个'\',一个'\0',所以实际上文件路径剩下可修改的是256)
/// 为Buffer分配一个堆上的内存
ustrTest.Buffer = ExAllocatePooMWithTag(PagedPool,MAX_PATH*sizeof(WCHAR),'POCU');
/// 如果分配失败,则return
if (ustrTest.Buffer ==NULL)
{
return;
}
/// 分配成,则对Buffer的指向的堆上内存初始化为0
RtlZeroMemory(ustrTest.Buffer,MAX PATH*sizeof(WCHAR));
/// 把L"Hello,world"拷贝到Buffer指向的内存中去,是深拷贝
memcpy(ustrTest.Buffer,L"Hello,world",ulLength);
/// 设置有效长度
ustrTest.Length =ulLength;
/// 设置最大长度
ustrTest.MaximumLength MAX_PATH*sizeof(WCHAR);
DbgPrint("%wZ\n",&ustrTest);
/// 堆上分配的内存需要手动把它释放掉
ExFreePool(ustrTest.Buffer);
/// 如果下面还需要使用ustrTest,一定要把它设为NEULL
/// 如果执行完ExFreePool(ustrTest.Buffer);函数返回了,就不需要设置为NULL
/// ustrTest.Buffer = NULL;
问题代码:如果文件超过130个字符,就会出问题,文件路径不全。
UNICODE STRING uPath={0};
uPath.Buffer=ExAllocatePooMWithTag(
PagedPool,
MAX_PATH,
'MLFC'
);
uPath.Length=wcslen(L"c:\doc\1.txt")*sizeof(WCHAR);
uPath.MaximumLength=MAX_PATH;
常用对字符串处理的API
初始化
/// 是浅拷贝(只把字符串str1的地址拷贝到uStr1.buffer中),buffer直接指向字符串str1
RtlInitUnicodeString(&uStr1,str1);
浅拷贝只是拷贝的一个地址。在用堆上的内存对UNICODE_STRING初始化时,应该使用memcpy(ustrTest.Buffer,L"Hello,world",ulLength);``ustrTest.Length =ulLength;``ustrTest.MaximumLength MAX_PATH*sizeof(WCHAR);
来赋值,要注意不要使用RtllnitunicodeString(ustrTest,L"Hello,world")
来赋值(会有风险)
/// eg:UNICODE_STRING初始化不当导致的蓝屏
UNICODE_STRING ustrTest = {0};
ustrTest.Buffer = ExAllocatePoolWithTag(PagedPool,
(wcslen(L"Hello,world"))*sizeof(WCHAR),'POCU');
if (ustrTest.Buffer == NULL)
{
return;
}
RtlZeroMemory(ustrTest.Buffer,
(wcslen(L"Hello,world"))*sizeof(WCHAR));
/// Buffer原来执行的堆上的内存,执行完之后,Buffer指向了静态区的常量字符串
RtllnitunicodeString(ustrTest,L"Hello,world");
DbgPrint("%wZIn"&ustrTest);
/// 这里释放Buffer所指向的内存,不是堆上内存,内存泄漏了,同时释放的是静态区内存,系统自我保护蓝屏了
ExFreePool(ustrTest.Buffer);
拷贝
/// 深拷贝(copy值,不是地址),很明显名字中有copy字样
RtlCopyUnicodeString(&uStr1,&uStr2);
拼接
/// @param str2 是简单以'\0'结尾的C语言中的字符串
RtlAppendUnicodeToString(&uStr1,str2);
/// @param ustr2 本身就是UNICODE_STRING的字符串
RtlAppendUnicodeStringToString(&uStr1,&uStr2);
/// eg:
/// 用栈上的buffer或静态区的内存对其初始化
UNICODE_STRING uStr1 = {0};
WCHAR buff[100] = "Hello";
uStr1.Length = 10;
uStr1.Buffer = buff;
/// str1是简单以'\0'结尾的C语言中的字符串
WCHARstr *str1 = L"world";
/// 用str1对uStr2初始化
UNICODE_STRING uStr2 = {0};
uStr2.Length=10:
uStr2.Buffer=str1;
///如果直接拼接一个简单以'\0'结尾的C语言中的字符串则使用下面这个函数
RtlAppendUnicodeToString(&uStr1,str1);
///如果直接拼接一个UNICODE_STRING字符串则使用下面这个和函数
RtlAppendUnicodeStringToString(&uStr1,&uStr2);
拆分
比较
/// @param TRUE/FALSE 是否忽略大小写
/// @return 0表示相等,负数表示uStr1 < uStr1,正数则表示uStr1 > uStr1
RtlcompareUnicodeString(&uStr1,&uStr1
TRUE/FALSE);
编码转换
/// 把多字节字符串转换成宽字节字符串
/// @param TRUE/FALSE 转换的过程中内存的大小会发生变化,涉及内存分配和计算,TRUE表示交给系统去计算内存的大小和分配内存,FALSE则表示程序员自己去计算和分配内存
RtlAnsiStringToUnicodeString(&uStr1,&aStr1,
TRUE/FALSE);
/// 如果前面设置为TRUE,系统帮忙计算和分配内存,用完之后一定要释放掉,否则会造成内存泄漏
/// 实际上很少会遇到字符串编码的转换,因为内核中用的都是UNICODE_STRING
/// DbgPrint在打印unicode中文的话,在debug view里面是看不到的,这种情况下就需要把unicode中文转化成多字节编码才能看到
RtlFreeUnicodeString(&uStr1);
安全函数
/// unicode编码字符
typedef struct UNICODE_STRING{
USHORT Length; ///< 字节数,不是字符数,而是有效数据的字节数
USHORT MaximumLength; ///< 字节数,告诉系统函数最多有多少内存可用
PWSTR Buffer; ///< 只是一个指针,一旦定义之后并没有内存,需要正确初始化之后,让Buffer含有真正的数据,才拥有内存.**非零结尾,中间也可能含有0**,PWSTR等价于WCHAR
}UNICODE_STRING,*PUNICODE_STRING;
在UNICODE_STRNG的类型定义中可以发现,是存在UNICODE_STRING能存储大字符长度是655(USHORT取值范围是0-(2^16-1)
),能表示65535/2 = 32,767
个字符,而在上述的对字符串操作的函数中,都没有溢出检测的,存溢出风险
//安全函数,溢出检测
#include <ntstrsafe.h>
/// str1,&uStr2超过了32767,或者uStr1+uStr2超过了32767,函数就会返回失败
RtlUnicodeStringInit(&uStr1,str1);
RtlUnicodeStringCopy(&uStr1,&uStr2);
RtlUnicodeStringCat(&uStr1,&uStr2);
/// 不能操作32767个字符
#define NTSTRSAFE UNICODE_STRING_MAX_CCH(Oxffff / sizeof((wchar t))
自己实现所有c语言中与字符串相关的库函数,再与微软的对比
初始化
拷贝
拼接
拆分
比较
文件
文件的表示
- 应用层:
"c\\doc\\hi.txt"
- 内核:
L"\\??\\c:\\hi.txt"
-->"\\device\\harddiskvolume3\\hi.txt
-
"\\device\\harddiskvolume3\\hi.txt
表示在设备对象上有hi.txt文件,设备对象名"\\device\\harddiskvolume3\\hi.txt
是由内核驱动创建的,然后在根据设备对象名创建符号链接。"\\device\\harddiskvolume3\\hi.txt
-
\\??\\c:
代表卷设备对象的符号链接,也称为盘符。
-
-
设备对象的表示
- 应用层:
-设备名:L"\\\\.\\xxxDrv"
其中xxxDrv
代表符号链接名,把设备对象当作一个特殊的文件打开,打开得到一个句柄。 - 内核层:
- 设备名:`"\device\xxxDrv"``
- 符号链接名:
"dosdevices\\xxxDrv"
等价于\\??\\xxxDrv"
文件操作
应用层
打开文件获得handle -> 基于handle读写删除查询 -> 关闭
创建文件/文件夹
读/写
拷贝
移动
删除
属性访问与设置
内核层
每个API都有对应的Irp,但复制、粘贴、移动没有对应的Irp,因为这三个动作本质是读和写
ZwCreateFile
-
ZwCreateFile
打开文件 - 接口说明
/**
* @brief DriverEntry NtCreateFile 例程创建一个新文件或打开一个现有文件。
* @param[out] FileHandle 指向接收文件句柄的 HANDLE 变量的指针。
* @param[in] DesiredAccess 指定一个ACCESS_MASK值,该值确定对对象的请求访问权限。
* @param[in] ObjectAttributes 文件路径
* @param[out] IoStatusBlock 操作的结果,指向IO_STATUS_BLOCK结构的指针,该结构接收最终完成状态和有关所请求操作的其他信息
* @param[in] AllocationSize 指向LARGE_INTEGER的指针,该LARGE_INTEGER包含创建或覆盖的文件的初始分配大小(以字节为单位)。
* 如果"分配大小"为 NULL,则未指定分配大小。如果未创建或覆盖任何文件,则忽略分配大小。
* @param[in] FileAttributes 指定一个或多个FILE_ATTRIBUTE_XXX 标志,这些标志表示在创建或覆盖文件时要设置的文件属性。
* 调用方通常指定FILE_ATTRIBUTE_NORMAL,从而设置默认属性。
* @param[in] ShareAccess 创建/打开这个文件的共享访问的类型,指定为零或以下标志的任意组合。共享读|共享写|共享删除,如果设为0,则进程以独占的方式打开这个文件,
* 其他进程没办法再打开它,也就没办法删除它,但还是有其他办法强删的
* @param[in] CreateDisposition 指定在文件存在或不存在时要执行的操作。FILE_OPEN_IF,存在则打开这个文件,不存在则创建这个文件
* 360在处理文件创建拦截的时候,曾经在FILE_OPEN_IF出现过漏洞,流氓软件生成仿造系统软件的图标诱导
* 用户点击,给网站导流获取收益。因为当时360考虑到大部分文件都是以FILE_OPEN、FILE_OPEN_IF方式打开的,
* 如果监控会拖慢系统性能,后来还是把这个标志加入到监控中来了
* @param[in] CreateOptions 指定驱动程序创建或打开文件时要应用的选项。同步操作的标志、普通文件标志、文件夹标志
* @param[in] EaBuffer 对于设备和中间驱动程序,此参数必须是 NULL 指针。
* @param[in] EaLength 对于设备和中间驱动程序,此参数必须为零。
* @return ntStatus NtCreateFile 在成功时返回 STATUS_SUCCESS,或在失败时返回相应的 NTSTATUS 错误代码。在后一种情况下,
* 调用方可以通过检查 IoStatusBlock 参数来确定失败的原因。
* @see https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntcreatefile
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-24 22:09
*/
ZwCreateFile(
_Out_ PHANDLE FileHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_opt_ PLARGE_INTEGER AllocationSize,
_In_ ULONG FileAttributes,
_In_ ULONG ShareAccess,
_In_ ULONG CreateDisposition,
_In_ ULONG CreateOptions,
_In_reads_bytes_opt_(EaLength) PVOID EaBuffer,
_In_ ULONG EaLength
);
ZwWriteFile
-
ZwWriteFile
写文件,是相对于应用层来说,数据流向:r3->R0
ZwReadFile
-
ZwReadFile
读文件,是相对于应用层来说,数据流向:r0->R3
ZwQuerylnformationFile
-
ZwQuerylnformationFile
查询文件信息
ZwQueryFullAttributesFile
-
ZwQueryFullAttributesFile
查询文件完整信息
ZwSetinformationFile
-
ZwSetinformationFile
设置文件信息 ->irp_mj_set_information
- 在这里还有两个重要的Irp(重命名和删除,Major)
ZwClose
-
ZwClose
关闭文件
ZwQueryDirectoryFile
-
ZwQueryDirectoryFile
遍历文件夹-
./
当前目录 -
..
父目录
-