Windows内核下的Rootkit开发技术学习。
用户数据都以文件的形式存储在本地磁盘上,Rootkit等恶意软件想要获取用户的隐私数据就需要有操作文件的功能,包括但不限于增、删、查、改。一般有三种文件管理的方式,一是基于导出的内核API直接操作文件,二是通过程序直接构造输入输出请求包(I/O Request Package,IRP)并发送IRP来操作文件,三是根据文件系统格式(New Technology File System,NTFS)来解析硬盘上的二进制数据。本文先从相对简单的内核API实现开始学习,内核API和用户模式中的WIN32 API有一定的类似。
编程环境:
内核API是一组从内核组件中输出的函数,大多数函数在内核本身模块(NtOskrnl.exe)中实现,少部分在其它模块中(如hal.dll)。内核API是大量C函数的集合,其中大多数名称含有一个前缀,这个前缀指示了实现该函数的模块。
常见的一些内核API前缀:
ExAllocatePool
KeAcquireSpinLock
IoCompleteRequest
ZwCreateFile
Zw前缀的内核API是原生API包装的,原生API指的是前缀为Nt的一系列函数。Nt系列函数(如NtCreateFile
)是Windows内核提供的原生API,它们直接与内核模式交互。但应用程序调用WIN32API时,这些API最终会转换成对应的Nt函数,执行实际的操作。
所谓的包装意味着Zw系列和Nt系列在实现上几乎相同,但它们通过不同的入口点进入系统。当内核模式调用Nt系列函数时,函数会检查调用上下文,以确定调用来自内核模式还是用户模式,如果来自用户模式,函数可能会进行一些安全检查或模式转换。而调用Zw系列函数,系统假设调用已经在内核模式下了,即直接将PreviousMode设置成 KernelMode(0)。
OBJECT_ATTRIBUTES
是Windows内核编程中用于描述操作系统对象(如文件、设备、事件、互斥体等)属性的结构体。该结构体通常在创建或打开内核对象时被使用,传递给如ZwCreateFile
、ZwOpenKey
等内核API。
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length; // 结构体的大小,以字节为单位。
HANDLE RootDirectory; //表示对象名称的根目录句柄。
PUNICODE_STRING ObjectName; //指向UNICODE_STRING结构体的指针,表示对象的名称。
ULONG Attributes; //指定对象的属性。
PSECURITY_DESCRIPTOR SecurityDescriptor; //指向安全描述符的指针。常设NULL。
PSECURITY_QUALITY_OF_SERVICE SecurityQualityOfService; //用于指定对象的安全服务质量属性,常设NULL。
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
上述字段中,如果RootDirectory
为NULL,则ObjectName
必须是一个完整的绝对路径名。否则,ObjectName
是相对与RootDirectory
目录的路径名。
Attributes
常见的属性标志:
OBJ_CASE_INSENSITIVE
:表示对象名称比较不区分大小写。OBJ_KERNEL_HANDLE
:指示句柄只能在内核模式下访问。OBJ_OPENIF
:如果对象存在,则打开它,而不是失败。OBJ_PERMANENT
:创建一个永久对象,不会自动删除。InitializeObjectAttributes
宏用于初始化 OBJECT_ATTRIBUTES
结构体,这俩者都同属于ntdef.h
。
宏定义如下:
#define InitializeObjectAttributes(p, n, a, r, s) { \
(p)->Length = sizeof(OBJECT_ATTRIBUTES); \
(p)->RootDirectory = r; \
(p)->Attributes = a; \
(p)->ObjectName = n; \
(p)->SecurityDescriptor = s; \
(p)->SecurityQualityOfService = NULL; \
}
p 指向 OBJECT_ATTRIBUTES
结构体的指针,字段如上述一样。
在内核层中的病毒木马要想创建或者释放植入程序,最先的是创建一个文件。ZwCreateFile
函数用于创建或打开文件对象,它是内核模式下访问文件系统的关键API。该函数同样可以用于创建或打开目录。
函数原型如下:
NTSTATUS ZwCreateFile(
PHANDLE FileHandle, //指向接收文件句柄的指针。
ACCESS_MASK DesiredAccess, //指定文件访问的类型,例如读、写、执行等。
POBJECT_ATTRIBUTES ObjectAttributes,//指向已经初始化的OBJECT_ATTRIBUTES结构体.
PIO_STATUS_BLOCK IoStatusBlock, //指向IO_STATUS_BLOCK结构的指针
PLARGE_INTEGER AllocationSize, //指定文件的分配大小。
ULONG FileAttributes, //指定了创建或覆盖文件时的属性
ULONG ShareAccess, //指定文件的共享模式。
ULONG CreateDisposition, //指定文件的创建方式,如文件已存在时应如何处理。
ULONG CreateOptions, //指定文件的创建方式,如文件已存在时应如何处理。
PVOID EaBuffer, //指向扩展属性(EA,Extended Attributes)缓冲区的指针,通常用于网络文件系统。
ULONG EaLength //EaBuffer的长度
);
ZwCreateFile
在使用时取决于字段CreateOptions
,如果该字段设置了FILE_DIRECTORY_FILE标志,表明这是创建一个目录。
在内核模式下创建文件的步骤:
UNICODE_STRING
结构,这是文件路径的表达形式。OBJECT_ATTRIBUTES
结构体,调用宏初始化。ZwCreateFile
函数。ZwClose
关闭文件句柄,释放资源。#include
void CreateFileTest()
{
NTSTATUS status;
HANDLE hFile;
OBJECT_ATTRIBUTES objAttr;
IO_STATUS_BLOCK ioStatusBlock;
UNICODE_STRING fileName;
//初始化UNICODE_STRING
RtlInitUnicodeString(&fileName, L"\\??\\C:\\Users\\example\\Desktop\\syswork\\MyFile.txt");
//初始化OBJECT_ATTRIBUTES
InitializeObjectAttributes(&objAttr, &fileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
//调用ZwCreateFile
status = ZwCreateFile(&hFile, GENERIC_WRITE | GENERIC_READ, &objAttr, &ioStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
//检查
if (NT_SUCCESS(status)) {
DbgPrint("File created or opened successfully.\n");
}
else {
DbgPrint("Failed to create or open file.Status: 0x%X\n", status);
}
//关闭文件句柄
if (hFile) {
ZwClose(hFile);
}
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
CreateFileTest();
return STATUS_SUCCESS;
}
需要注意的地方,初始化UNICODE_STRING
时:文件路径要包含\\??\\
,这是Windows内核中的一个符号链接,用于将内核模式的文件路径映射到用户模式的路径。
ZwCreateFile
的FILE_OPEN_IF
标志告诉内核,如果文件已经存在则打开它,如果文件不存在则创建它。
在虚拟机上加载驱动程序,要打开测试模式。还有visual studio生成解决方案时要用x64生成,使用x86生成的sys在win10上可能会报签名错误。
删除指定文件。函数声明如下:
NTSTATUS ZwDeleteFile(
_In_ POBJECT_ATTRIBUTES ObjectAttributes
);
唯一的参数,指向一个 OBJECT_ATTRIBUTES
结构的指针。
如果要单独写一个删除文件的sys,步骤和ZwCreateFile
是相同的,先初始化文件路径和OBJECT_ATTRIBUTES
结构体,然后调用就好。这里演示,直接将ZwDeleteFile加到上述代码中。
status = ZwDeleteFile(&objAttr);
if (NT\_SUCCESS(status)) {
DbgPrint("File deleted successfully.\\n");
} else {
DbgPrint("Failed to delete file. Status: 0x%X\\n", status);
}
报未找到ZwDeleteFile
的错误,可以添加一个#include
,注意要放在ntddk前面。
要对文件进行操作免不了要获取文件的大小,以便申请数据缓冲区。在用户层中,通过GetFileSize
函数获取文件大小;在内核模式下,要获取文件的大小,可以通过调用 ZwQueryInformationFile
函数,配合 FILE_STANDARD_INFORMATION
结构来完成。ZwQueryInformationFile
函数允许我们查询文件的各种属性,其中 FILE_STANDARD_INFORMATION
结构包含了文件的大小信息。
函数定义:
NTSYSAPI NTSTATUS ZwQueryInformationFile(
[in] HANDLE FileHandle, //文件或设备的句柄。
[out] PIO_STATUS_BLOCK IoStatusBlock, //指向一个 IO_STATUS_BLOCK 结构的指针,用于接收请求的状态信息和实际传输的字节数。
[out] PVOID FileInformation, //指向接收所请求信息的缓冲区。
[in] ULONG Length, //指定FileInformation缓冲区的大小(以字节为单位)。
[in] FILE_INFORMATION_CLASS FileInformationClass //一个枚举类型,指定要检索的文件信息类型。
);
其中字段FileInformationClass
常用的类型包括:
FileBasicInformation
: 基本文件信息,例如创建时间、上次访问时间。FileStandardInformation
: 标准文件信息,包括文件大小、分配大小、文件是否为目录等。FilePositionInformation
: 当前文件指针的位置。ZwQueryInformationFile
函数的返回值是一个NTSTATUS
代码,表示操作的成功与否。
FILE_STANDARD_INFORMATION
结构用于存储与文件或目录相关的标准信息。它是内核APIZwQueryInformationFile
常使用的一个信息结构,当该函数的FileInformationClass
参数设置为FileStandarInformation
时,这个结构会返回标准文件信息,包括文件大小、分配大小等。
结构定义:
typedef struct _FILE_STANDARD_INFORMATION {
LARGE_INTEGER AllocationSize; // 文件的分配大小
LARGE_INTEGER EndOfFile; // 实际大小
ULONG NumberOfLinks; // 指向文件的硬链接数
BOOLEAN DeletePending; // 文件是否已标记为删除
BOOLEAN Directory; // 是否是目录
} FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION;
ZwQueryInformationFile
的第一个参数是文件句柄,该句柄需要由ZwCreateFile
或其它函数提供。因此同删除文件类似,可以对第一个具体代码进行修改,在创建或打开文件后,获取文件的大小。
变量要添加一行FILE_STANDARD_INFORMATION fileInfo;
if (NT_SUCCESS(status)) {
DbgPrint("File opened successfully.\\n");
// 查询文件信息以获取文件大小
status = ZwQueryInformationFile(hFile, &ioStatusBlock, &fileInfo, sizeof(fileInfo), FileStandardInformation);
if (NT_SUCCESS(status)) {
// 输出文件大小信息
DbgPrint("File size: %llu bytes\\n", fileInfo.EndOfFile.QuadPart);
}
else {
DbgPrint("Failed to query file information. Status: 0x%X\\n", status);
}
// 关闭文件句柄
ZwClose(hFile);
}
else {
DbgPrint("Failed to open file. Status: 0x%X\\n", status);
}
在内核模式下,读写文件操作通常使用 ZwReadFile
和 ZwWriteFile
函数。它们同用户模式的ReadFile
和 WriteFile
函数类似,但是在内核模式中使用需要处理一些额外的事项,例如对象属性、I/O状态块等。
函数定义:
NTSTATUS ZwReadFile(
HANDLE FileHandle, //文件句柄。
HANDLE Event, //可选的事件句柄。
PIO_APC_ROUTINE ApcRoutine, //可选的APC例程,用于异步操作。
PVOID ApcContext, //可选的APC上下文
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer, //缓冲区。
ULONG Length, //要读取的字节数。
PLARGE_INTEGER ByteOffset, //从文件读取的位置。
PULONG Key //与文件关联的健,用于锁定操作。
);
ZwWriteFile
函数的各个参数同ZwReadFile
相同,除了Buffer
,对于读取操作来说,该参数是存储数据的缓冲区;对于写入操作来说,该缓冲区存储的是要被写入的数据。
这俩个函数同样需要一个文件句柄调用,该句柄一般由ZwCreateFile
提供。
先声明变量,HANDLE
和OBJECT_ATTRIBUTES
结构IO_STATUS_BLOCK
都需要声明俩个,分别用于读取和写入操作。
NTSTATUS status;
HANDLE hFileRead, hFileWrite;
OBJECT_ATTRIBUTES objAttrRead, objAttrWrite;
IO_STATUS_BLOCK ioStatusBlockRead, ioStatusBlockWrite;
UNICODE_STRING fileNameRead, fileNameWrite;
LARGE_INTEGER byteOffset;
CHAR buffer[1024]; // 读写缓冲区
ULONG bytesRead;
同上一样需要先初始化UNICODE_STRING
和OBJETC_ATTRIBUTES
结构,然后调用ZwCreateFile
打开一个文件,获得其文件句柄。再打开文件的基础下,再创建一个文件,之后从已打开的文件中读取数据写入到新的文件中。
关键代码:
// 读取文件数据并写入到新文件中
byteOffset.LowPart = byteOffset.HighPart = 0;
while (NT\_SUCCESS(status)) {
// 从读取文件中读取数据
status = ZwReadFile(hFileRead, NULL, NULL, NULL, &ioStatusBlockRead, buffer, sizeof(buffer), &byteOffset, NULL);
if (status == STATUS\_END\_OF\_FILE) {
DbgPrint("Reached end of file.\\n");
break;
}
if (NT\_SUCCESS(status)) {
bytesRead = (ULONG)ioStatusBlockRead.Information;
// 将读取的数据写入到写入文件中
status = ZwWriteFile(hFileWrite, NULL, NULL, NULL, &ioStatusBlockWrite, buffer, bytesRead, &byteOffset, NULL);
if (!NT\_SUCCESS(status)) {
DbgPrint("Failed to write data. Status: 0x%X\\n", status);
break;
}
byteOffset.QuadPart += bytesRead; // 更新写入文件的偏移
} else {
DbgPrint("Failed to read data. Status: 0x%X\\n", status);
break;
}
在内核模式下,可以使用 ZwSetInformationFile
函数来重命名文件。相关的结构是FILE_RENAME_INFORMATION
。
函数定义:
NTSTATUS ZwSetInformationFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ PVOID FileInformation,
_In_ ULONG Length,
_In_ FILE_INFORMATION_CLASS FileInformationClass
);
重点参数 FileInformationClass
,枚举值,指定要设置的文件信息的类型,包括:
FileBasicInformation
:设置基本文件信息。FileRenameInformation
:重命名文件。FileDispositionInformation
:删除文件。FilePositionInformation
:设置文件指针位置。FileEndOfFileInformation
:设置文件结束位置(文件大小)。结构定义:
typedef struct _FILE_RENAME_INFORMATION {
BOOLEAN ReplaceIfExists;
HANDLE RootDirectory;
ULONG FileNameLength; //文件名长度
WCHAR FileName[1]; //灵活的数组成员
} FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION;
ReplaceIfExists
设置为 TRUE 表示如果给定名称的文件已经存在,则应使用给定文件替换该文件。设置为 FALSE 表示如果给定名称的文件已经存在,则重命名操作会失败。
该结构的第四个成员FileName
是一个灵活的数组成员,实际大小取决于要重命名的目标文件名的长度,这意味着该FILE_RENAME_INFORMATION
结构的大小再编译的时候是不固定的,而是运行时动态分配的。
分配的内存大小需要根据文件名长度计算:sizeof(FILE_RENAME_INFORMATION) + FileNameLength
这里就涉及到了函数ExAllocatePoolWithTag
和ExFreePoolWithTag
,分别用于内存分配和释放内存。
实现步骤:
ZwCreateFile
返回一个文件句柄UNICODE_STRING
ExAllocatePoolWithTag
分配RtlZeroMermory
函数将分配的内存空间清零FILE_RENAME_INFORMATION
结构填满ZwSetInformationFile
重命名文件ExFreePoolWithTag
释放内存代码如下:
//变量声明:
PFILE_RENAME_INFORMATION renameInfo;
ULONG renameInfoSize;
......
//打开文件后
if (NT_SUCCESS(status)) {
DbgPrint("File opened successfully.\n");
// 初始化新文件名路径
RtlInitUnicodeString(&newFileName, L"\\??\\C:\\syswork4\\NewFileName.txt");
// 分配重命名信息结构
renameInfoSize = sizeof(FILE_RENAME_INFORMATION) + newFileName.Length;
renameInfo = (PFILE_RENAME_INFORMATION)ExAllocatePoolWithTag(NonPagedPool, renameInfoSize, 'tag1');
if (renameInfo != NULL) {
RtlZeroMemory(renameInfo, renameInfoSize);
renameInfo->ReplaceIfExists = FALSE;
renameInfo->RootDirectory = NULL;
renameInfo->FileNameLength = newFileName.Length;
RtlCopyMemory(renameInfo->FileName, newFileName.Buffer, newFileName.Length);
// 重命名文件
status = ZwSetInformationFile(hFile, &ioStatusBlock, renameInfo, renameInfoSize, FileRenameInformation);
if (NT_SUCCESS(status)) {
DbgPrint("File renamed successfully.\n");
} else {
DbgPrint("Failed to rename file. Status: 0x%X\n", status);
}
// 释放内存
ExFreePoolWithTag(renameInfo, 'tag1');
} else {
DbgPrint("Failed to allocate memory for rename info.\n");
}
// 关闭文件句柄
ZwClose(hFile);
} else {
DbgPrint("Failed to open file. Status: 0x%X\n", status);
}
在用户层中,是通过FindFirstFile
和FindNextFile
俩个函数实现文件遍历的,内核中主要由函数ZwQueryDirectoryFile
来完成文件遍历的操作。确切的说,该函数是用于枚举文件目录内容,可以返回目录中文件的列表和每个文件的详细信息。
函数定义:
NTSTATUS ZwQueryDirectoryFile(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID FileInformation,
ULONG Length,
FILE_INFORMATION_CLASS FileInformationClass,//文件信息类型
BOOLEAN ReturnSingleEntry, //是否只返回一个条目(通常为 FALSE)
PUNICODE_STRING FileName, //可选的文件名掩码(例如 *.txt),如果为 NULL 则返回目录中的所有文件。
BOOLEAN RestartScan //是否从目录开头重新扫描。
);
大多数上面都介绍完了类似的。
FileInformationClass
指定返回的文件信息类型,常见的:
FileDirectoryInformation
: 返回目录中的文件和子目录。FileFullDirectoryInformation
: 类似于 FileDirectoryInformation
,但提供更多信息FileBothDirectoryInformation
: 返回目录项,并包括 8.3 格式的短文件名。该结构是用于存储目录项信息的数据结构,包含了有关目录中每个文件或子目录的信息,例如文件名、文件大小、时间戳等。
typedef struct _FILE_DIRECTORY_INFORMATION {
ULONG NextEntryOffset; // 下一个条目的偏移量,以字节为单位。如果这是最后一个条目,则为零。
ULONG FileIndex; // 文件索引号,用于标识文件。
LARGE_INTEGER CreationTime; // 创建时间。
LARGE_INTEGER LastAccessTime; // 最后访问时间。
LARGE_INTEGER LastWriteTime; // 最后写入时间。
LARGE_INTEGER ChangeTime; // 最后修改时间。
LARGE_INTEGER EndOfFile; // 文件的结束位置,通常表示文件的大小。
LARGE_INTEGER AllocationSize; // 文件分配的大小,通常是磁盘上为文件预留的空间。
ULONG FileAttributes; // 文件的属性,例如只读、隐藏等。
ULONG FileNameLength; // 文件名的长度(以字节为单位,不包含 NULL 终止符)。
WCHAR FileName[1]; // 文件名(可变长度的字符串)。
} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
实现和重命名文件的代码类似,初始化时要分配好内存,可以直接分配一块例如1024大小的缓冲区:
// 初始化文件信息缓冲区
PVOID buffer;
ULONG bufferSize = 1024;
buffer = ExAllocatePoolWithTag(NonPagedPool, bufferSize, 'tag1');
接着初始目录句柄,然后ZwCreateFile
获取一个文件句柄。
循环调用ZwQueryDirectoryFile
函数来获取目录中的信息,循环中还有一个do循环是用来遍历该函数获得的目录信息(即多个FILE_DIRECTORY_INFORMATION
结构),创建一个UNICODE_STRING
来存储当前目录项的名称。
最后要释放内存。
while (TRUE) {
status = ZwQueryDirectoryFile(hDirectory, NULL, NULL, NULL, &ioStatusBlock, buffer,bufferSize,FileDirectoryInformation, FALSE, NULL, restartScan);
if (status == STATUS_NO_MORE_FILES) {
break;
} else if (!NT_SUCCESS(status)) {
DbgPrint("ZwQueryDirectoryFile failed. Status: 0x%X\n", status);
break;
}
PFILE_DIRECTORY_INFORMATION fileInfo = (PFILE_DIRECTORY_INFORMATION)buffer;
do {
UNICODE_STRING currentFileName;
currentFileName.Buffer = fileInfo->FileName;
currentFileName.Length = (USHORT)fileInfo->FileNameLength;
currentFileName.MaximumLength = (USHORT)fileInfo->FileNameLength;
DbgPrint("Found file: %wZ\n", &currentFileName);
fileInfo = (PFILE_DIRECTORY_INFORMATION)((PUCHAR)fileInfo + fileInfo->NextEntryOffset);
} while (fileInfo->NextEntryOffset != 0);
restartScan = FALSE;
}
ZwClose(hDirectory);
} else {
DbgPrint("Failed to open directory. Status: 0x%X\n", status);
}
ExFreePoolWithTag(buffer, 'tag1');
}
PVOID ExAllocatePoolWithTag(
[in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType,
[in] SIZE_T NumberOfBytes,
[in] ULONG Tag
);
PoolType
指定要分配的池内存类型,NumberOfBytes
是要分配的内存数,Tag
是用于分配内存的池标志。
关于池类型,常见的有:
NonPagedPool
: 非分页池,分配的内存在物理内存中,不能被分页出内存。这种类型的内存在内核模式下是可以随时访问的。PagedPool
: 分页池,分配的内存可以被分页到磁盘,只有在访问时才加载到内存。NonPagedPoolNx
: 和 NonPagedPool
类似,但不允许执行代码。(安全)不同的池类型对于IRQL也不同,在分配NonPagedPool
类型的内存时,可以在任意IRQL下调用,但是分配PagedPool
内存只能在IRQL<=APC_LEVEL
时调用,如果在高IRQL时尝试分配分页内存,可能导致系统奔溃。
在window10 2004中,ExAllocatePoolWithTag
已经被弃用了。要编写适用于更高win版本的驱动程序,可以使用ExAllocatePool2
。
同ExAllocatePoolWithTag
相比,该函数除非指定了特别的标识,否则内存将被初始化为零;ExAllocatePool2
的函数签名还包含了更多参数,新的函数签名为:
PVOID ExAllocatePool2(
POOL_FLAGS PoolFlags,
SIZE_T NumberOfBytes,
ULONG Tag
);
PoolFlags
是一个新的参数,可以控制内存分配的一些额外特性,而 ExAllocatePoolWithTag
只支持通过 POOL_TYPE
指定内存池的类型。
16 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!