Ring0内核层下读写文件实现文件复制

大葱

发布日期: 2019-01-29 20:14:55 浏览量: 1419
评分:
star star star star star star star star star star_border
*转载请注明来自write-bug.com

背景

本文讲解的文件的基础操作就是指对文件进行增、删、改、查等基础的操作,这也是管理文件常用的操作。在应用层,我们都是直接使用 WIN32 API 来直接操作。到了内核层,虽然不能使用 WIN32 API,但是 Windows 也专门提供了相应的内核 API 给我们开发者使用,我们直接调用内核 API 就能实现我们的功能。

现在,我就单独将文件基础管理的常用操作一一解析,每个技术点都形成独立文档,来向大家讲解下怎么使用内核 API 在内核下创建文件或目录、删除文件或目录、 计算获取文件大小、文件的读和写操作实现文件复制、文件重命名、文件查询遍历等。

本文主要讲解的是,使用内核 API 读取文件数据和写入文件数据,这样来实现文件的复制功能。

函数介绍

InitializeObjectAttributes 函数

InitializeObjectAttributes宏初始化不透明OBJECT_ATTRIBUTES结构,该结构指定了打开句柄的例程的对象句柄的属性。

函数声明

  1. VOID InitializeObjectAttributes(
  2. [out] POBJECT_ATTRIBUTES InitializedAttributes,
  3. [in] PUNICODE_STRING ObjectName,
  4. [in] ULONG Attributes,
  5. [in] HANDLE RootDirectory,
  6. [in, optional] PSECURITY_DESCRIPTOR SecurityDescriptor
  7. );

参数

  • InitializedAttributes [out]
    指定要初始化的OBJECT_ATTRIBUTES结构。
  • ObjectName [in]
    指向Unicode字符串的指针,该字符串包含要打开句柄的对象的名称。这必须是完全限定的对象名称,或者是由RootDirectory参数指定的对象目录的相对路径名。
  • Attributes[in]
    指定一个或多个以下标志:
    OBJ_INHERIT
    该句柄可以由当前进程的子进程继承。
    OBJ_PERMANENT
    此标志仅适用于在对象管理器中命名的对象。默认情况下,当所有打开的句柄关闭时,这些对象将被删除。如果指定了此标志,则当所有打开的手柄都关闭时,该对象不会被删除。驱动程序可以使用ZwMakeTemporaryObject删除永久对象。
    OBJ_EXCLUSIVE
    此对象只能打开单个句柄。
    OBJ_CASE_INSENSITIVE
    如果指定了此标志,则在将ObjectName参数与现有对象的名称匹配时使用不区分大小写的比较。否则,使用默认系统设置比较对象名称。
    OBJ_OPENIF
    如果将此标志指定给创建对象的例程,并且该对象已经存在,则例程应该打开该对象。否则,创建对象的例程将返回STATUS_OBJECT_NAME_COLLISION的NTSTATUS代码。
    OBJ_KERNEL_HANDLE
    指定句柄只能在内核模式下访问。
    OBJ_FORCE_ACCESS_CHECK
    打开句柄的例程应强制执行对象的所有访问检查,即使在内核模式下打开句柄。
  • RootDirectory [in]
    在ObjectName参数中指定的路径名的根对象目录的句柄。如果ObjectName是一个完全限定的对象名称,则RootDirectory为NULL。使用ZwCreateDirectoryObject获取对象目录的句柄。
  • SecurityDescriptor [in]
    指定在对象创建时应用于安全描述符。此参数是可选的。驱动程序可以指定NULL来接受对象的默认安全性。

返回值

  • 无返回值。

ZwCreateFile 函数

创建一个新文件或者打开一个存在的文件。

函数声明

  1. NTSTATUS ZwCreateFile(
  2. _Out_ PHANDLE FileHandle,
  3. _In_ ACCESS_MASK DesiredAccess,
  4. _In_ POBJECT_ATTRIBUTES ObjectAttributes,
  5. _Out_ PIO_STATUS_BLOCK IoStatusBlock,
  6. _In_opt_ PLARGE_INTEGER AllocationSize,
  7. _In_ ULONG FileAttributes,
  8. _In_ ULONG ShareAccess,
  9. _In_ ULONG CreateDisposition,
  10. _In_ ULONG CreateOptions,
  11. _In_opt_ PVOID EaBuffer,
  12. _In_ ULONG EaLength
  13. );

参数

  • FileHandle [out]
    指向接收文件句柄的HANDLE变量的指针。
  • DesiredAccess [in]
    指定一个ACCESS_MASK值,用于确定请求的对象访问。
  • ObjectAttributes [in]
    指向OBJECT_ATTRIBUTES结构的指针,指定对象名称和其他属性。 使用InitializeObjectAttributes初始化此结构。 如果调用者未在系统线程上下文中运行,则调用InitializeObjectAttributes时必须设置OBJ_KERNEL_HANDLE属性。
  • IoStatusBlock [out]
    指向IO_STATUS_BLOCK结构的指针,用于接收最终完成状态以及有关所请求操作的其他信息。
  • AllocationSize [in]
    指向LARGE_INTEGER的指针,其中包含创建或覆盖的文件的初始分配大小(以字节为单位)。 如果AllocationSize为NULL,则不指定分配大小。 如果没有创建或覆盖文件,AllocationSize将被忽略。
  • FileAttributes [in]
    指定一个或多个FILE_ATTRIBUTE_XXX标志,表示要创建或覆盖文件时要设置的文件属性。 调用者通常指定FILE_ATTRIBUTE_NORMAL,它设置默认属性。 有关FILE_ATTRIBUTE_XXX标志的列表,请参阅Microsoft Windows SDK文档中的CreateFile例程。 如果没有创建或覆盖文件,FileAttributes将被忽略。
  • ShareAccess [in]
    共享访问类型,指定为 0 或以下标志的任意组合。当为 0 时,表示独占访问。
  • CreateDisposition [in]
    指定文件执行或不存在时执行的操作。 其中,FILE_CREATE 表示若文件存在,则返回一个错误,否则创建文件;FILE_OPEN 表示若文件,则打开文件,否则返回一个错误;存在,FILE_OPEN_IF 表示如果文件存在,则打开文件,否则创建文件;
  • CreateOptions [in]
    指定驱动程序创建或打开文件时应用的选项。 其中,FILE_DIRECTORY_FILE 表示要操作的文件是一个目录,CreateDisposition 参数必须设置为FILE_CREATE、FILE_OPEN 或者 FILE_OPEN_IF;FILE_SYNCHRONOUS_IO_NONALERT 表示文件中的所有操作都是同步执行的,DesiredAccess 参数必须设置 SYNCHRONIZE 标志。
  • EaBuffer [in,可选]
    对于设备和中间驱动程序,此参数必须为 NULL 指针。
  • EaLength [in]
    对于设备和中间驱动程序,此参数必须为 0。

返回值

  • 成功时,返回STATUS_SUCCESS;失败时,返回适当的NTSTATUS错误代码。

备注

  • 一旦FileHandle指向的句柄不再使用,驱动程序必须调用 ZwClose 关闭句柄。

ExAllocatePool 函数

ExAllocatePool例程已过时,仅针对现有二进制文件导出。 使用ExAllocatePoolWithTag代替。
ExAllocatePool分配指定类型的池内存,并返回指向分配块的指针。

函数声明

  1. PVOID ExAllocatePool(
  2. _In_ POOL_TYPE PoolType,
  3. _In_ SIZE_T NumberOfBytes
  4. );

参数

  • PoolType [in]
    指定要分配的池内存类型。其中,NonPagedPool 表示非分页内存池,这是不可分页的系统内存。 非分页内存可以从任何 IRQL 访问,但它是一个稀缺的资源。
  • NumberOfBytes [in]
    指定要分配的字节数。

返回值

  • 如果可用池中的内存不足以满足请求,ExAllocatePool 将返回 NULL。 否则,例程将返回指向分配的内存的指针。

备注

  • 当内存不再使用的时候,调用 ExFreePool 函数释放内存。

ZwReadFile 函数

从打开的文件中读取数据。

函数声明

  1. NTSTATUS ZwReadFile(
  2. _In_ HANDLE FileHandle,
  3. _In_opt_ HANDLE Event,
  4. _In_opt_ PIO_APC_ROUTINE ApcRoutine,
  5. _In_opt_ PVOID ApcContext,
  6. _Out_ PIO_STATUS_BLOCK IoStatusBlock,
  7. _Out_ PVOID Buffer,
  8. _In_ ULONG Length,
  9. _In_opt_ PLARGE_INTEGER ByteOffset,
  10. _In_opt_ PULONG Key
  11. );

参数

  • FileHandle [in]
    处理文件对象。该句柄是通过成功调用ZwCreateFile或ZwOpenFile创建的。
  • Event[in]
    在读取操作完成之后,事件对象的句柄被设置为信号状态。设备和中间驱动程序应该将此参数设置为NULL。
  • ApcRoutine [in,optional]
    此参数保留。设备和中间驱动程序应该将此指针设置为NULL。
  • ApcContext [in,optional]
    此参数保留。设备和中间驱动程序应该将此指针设置为NULL。
  • IoStatusBlock [out]
    指向接收最终完成状态的IO_STATUS_BLOCK结构的指针以及有关所请求的读取操作的信息。信息成员接收从文件中实际读取的字节数。
  • Buffer[out]
    指向从主机分配的缓冲区的指针,该缓冲区接收从文件读取的数据。
  • Length[in]
    缓冲区指向的缓冲区大小(以字节为单位)。
  • ByteOffset [in]
    指向变量的指针,该变量指定读操作开始的文件中的起始字节偏移量。如果尝试读取超出文件的末尾,ZwReadFile返回一个错误。

    Key[in]
    设备和中间驱动程序应该将此指针设置为NULL。

返回值

  • 成功,则返回 STATUS_SUCCESS;否则,返回 NTSTATUS 错误代码。

ZwWriteFile 函数

向一个打开的文件中写入数据。

函数声明

  1. NTSTATUS ZwWriteFile(
  2. _In_ HANDLE FileHandle,
  3. _In_opt_ HANDLE Event,
  4. _In_opt_ PIO_APC_ROUTINE ApcRoutine,
  5. _In_opt_ PVOID ApcContext,
  6. _Out_ PIO_STATUS_BLOCK IoStatusBlock,
  7. _In_ PVOID Buffer,
  8. _In_ ULONG Length,
  9. _In_opt_ PLARGE_INTEGER ByteOffset,
  10. _In_opt_ PULONG Key
  11. );

参数

  • FileHandle [in]
    处理文件对象。该句柄是通过成功调用ZwCreateFile或ZwOpenFile创建的。
  • Event[在,可选]
    在写操作完成之后,将事件对象的句柄设置为信号状态。设备和中间驱动程序应该将此参数设置为NULL。
  • ApcRoutine [in,optional]
    此参数保留。设备和中间驱动程序应该将此指针设置为NULL。
  • ApcContext [in,optional]
    此参数保留。设备和中间驱动程序应该将此指针设置为NULL。
  • IoStatusBlock [out]
    指向接收最终完成状态的IO_STATUS_BLOCK结构的指针以及有关所请求的写入操作的信息。信息成员接收实际写入文件的字节数。
  • 缓冲区[in]
    指向包含要写入文件的数据的调用者分配的缓冲区。
  • 长度[in]
    缓冲区指向的缓冲区大小(以字节为单位)。
  • ByteOffset [in]
    指向变量的指针,该变量指定用于开始写入操作的文件中的起始字节偏移量。如果Length和ByteOffset指定超过当前文件结束标记的写入操作,ZwWriteFile将自动扩展文件并更新文件结尾标记;在这些旧的和新的文件结束标记之间未明确写入的任何字节被定义为 0。
    如果调用ZwCreateFile仅设置DesiredAccess标志FILE_APPEND_DATA,则忽略ByteOffset。 给定缓冲区中的数据长度字节从文件的当前结束开始写入。
  • Key[in]
    设备和中间驱动程序应该将此指针设置为NULL。

返回值

  • 成功,则返回 STATUS_SUCCESS;否则,返回 NTSTATUS 错误代码。

实现原理

很多人在看到函数介绍的时候,发现内核函数都有很多参数,就下意识觉得很难。其实,这大部分参数基本上我们都是置为空 NULL 用不到的。

对于文件的读写操作,应用层上便使用 CreateFile 打开文件,获取句柄;然后调用 ReadFile 和 WriteFile 函数来操作文件句柄,实现文件的读写。内核下也是一样的实现思路,连实现的函数也类似。内核下,我们使用 ZwCreateFile 打开文件,获取文件句柄,然后调用 ZwReadFile 和 ZwWriteFile 函数来操作文件句柄,实现文件的读写。具体实现步骤如下:

  1. 首先,我们调用 InitializeObjectAttributes 宏来初始化对象属性,包括文件路径和其它属性。

  2. 然后,再调用 ZwCreateFile 函数,根据上述的函数介绍,设置相应的参数来打开文件,获取内核文件句柄。

  3. 接着,调用 ZwReadFile 函数,读取指定大小和偏移的数据到缓冲区,这个需要在打开文件的时候,就指定文件句柄具有读权限。接着调用 ZwWriteFile 函数,写文件中写入指定大小和偏移的数据到文件中,这个需要在打开文件的时候,就指定文件句柄具有写权限。

  4. 最后,当我们不再使用文件句柄的时候,再调用 ZwClose 函数关闭文件句柄。

那么,所谓的文件复制,也就是打开现有的文件,读取文件中的数据;然后,新建一个文件,写入读取的数据。这样,就完成了文件的复制。

在内核下,我们也会申请内存来存放数据,一般会调用 ExAllocatePool 来申请非分页内存,因为非分页内存有个最大的好处就是它可以从任何 IRQL 访问。但是,要注意的是,在内核下,非分页内存是一个稀缺资源,不应大量申请,使用完毕要及时调用 ExFreePool 释放资源。

特别注意一点,内核下表示的文件或者目录路径要在路径前面加上 \??\,例如表示 C 盘下的 test.txt 文件路径:\??\C:\test.txt。

编码实现

读取文件数据

  1. // 读取文件数据
  2. BOOLEAN MyReadFile(UNICODE_STRING ustrFileName, LARGE_INTEGER liOffset, PUCHAR pReadData, PULONG pulReadDataSize)
  3. {
  4. HANDLE hFile = NULL;
  5. IO_STATUS_BLOCK iosb = { 0 };
  6. OBJECT_ATTRIBUTES objectAttributes = { 0 };
  7. NTSTATUS status = STATUS_SUCCESS;
  8. // 打开文件
  9. InitializeObjectAttributes(&objectAttributes, &ustrFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
  10. status = ZwCreateFile(&hFile, GENERIC_READ, &objectAttributes, &iosb, NULL,
  11. FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN,
  12. FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
  13. if (!NT_SUCCESS(status))
  14. {
  15. ShowError("ZwCreateFile", status);
  16. return FALSE;
  17. }
  18. // 读取文件数据
  19. RtlZeroMemory(&iosb, sizeof(iosb));
  20. status = ZwReadFile(hFile, NULL, NULL, NULL, &iosb,
  21. pReadData, *pulReadDataSize, &liOffset, NULL);
  22. if (!NT_SUCCESS(status))
  23. {
  24. *pulReadDataSize = iosb.Information;
  25. ZwClose(hFile);
  26. ShowError("ZwCreateFile", status);
  27. return FALSE;
  28. }
  29. // 获取实际读取的数据
  30. *pulReadDataSize = iosb.Information;
  31. // 关闭句柄
  32. ZwClose(hFile);
  33. return TRUE;
  34. }

写入文件数据

  1. // 向文件写入数据
  2. BOOLEAN MyWriteFile(UNICODE_STRING ustrFileName, LARGE_INTEGER liOffset, PUCHAR pWriteData, PULONG pulWriteDataSize)
  3. {
  4. HANDLE hFile = NULL;
  5. IO_STATUS_BLOCK iosb = { 0 };
  6. OBJECT_ATTRIBUTES objectAttributes = { 0 };
  7. NTSTATUS status = STATUS_SUCCESS;
  8. // 打开文件
  9. InitializeObjectAttributes(&objectAttributes, &ustrFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
  10. status = ZwCreateFile(&hFile, GENERIC_WRITE, &objectAttributes, &iosb, NULL,
  11. FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN_IF,
  12. FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
  13. if (!NT_SUCCESS(status))
  14. {
  15. ShowError("ZwCreateFile", status);
  16. return FALSE;
  17. }
  18. // 读取文件数据
  19. RtlZeroMemory(&iosb, sizeof(iosb));
  20. status = ZwWriteFile(hFile, NULL, NULL, NULL, &iosb,
  21. pWriteData, *pulWriteDataSize, &liOffset, NULL);
  22. if (!NT_SUCCESS(status))
  23. {
  24. *pulWriteDataSize = iosb.Information;
  25. ZwClose(hFile);
  26. ShowError("ZwCreateFile", status);
  27. return FALSE;
  28. }
  29. // 获取实际写入的数据
  30. *pulWriteDataSize = iosb.Information;
  31. // 关闭句柄
  32. ZwClose(hFile);
  33. return TRUE;
  34. }

复制文件

  1. // 文件复制
  2. // \??\C:\MyCreateFolder\1.exe -->
  3. // \??\C:\MyCreateFolder\2.exe
  4. BOOLEAN MyCopyFile(UNICODE_STRING ustrScrFile, UNICODE_STRING ustrDestFile)
  5. {
  6. ULONG ulBufferSize = 40960;
  7. ULONG ulReadDataSize = ulBufferSize;
  8. LARGE_INTEGER liOffset = { 0 };
  9. PUCHAR pBuffer = ExAllocatePool(NonPagedPool, ulBufferSize);
  10. // 一边读取, 一边写入, 实现文件复制
  11. do
  12. {
  13. // 读取文件
  14. ulReadDataSize = ulBufferSize;
  15. MyReadFile(ustrScrFile, liOffset, pBuffer, &ulReadDataSize);
  16. // 若读取的数据为空的时候, 结束复制操作
  17. if (0 >= ulReadDataSize)
  18. {
  19. break;
  20. }
  21. // 写入文件
  22. MyWriteFile(ustrDestFile, liOffset, pBuffer, &ulReadDataSize);
  23. // 更新偏移
  24. liOffset.QuadPart = liOffset.QuadPart + ulReadDataSize;
  25. } while (TRUE);
  26. // 释放内存
  27. ExFreePool(pBuffer);
  28. return TRUE;
  29. }

程序测试

在 Win7 32 位系统下,驱动程序正常执行:

在 Win10 64 位系统下,驱动程序正常执行:

总结

虽然非分页内存它可以从任何 IRQL 访问,但要注意的是,在内核下,非分页内存是一个稀缺资源,不应大量申请,使用完毕要及时调用 ExFreePool 释放资源。

特别注意一点,内核下表示的文件或者目录路径要在路径前面加上 \??\,例如表示 C 盘下的 test.txt 文件路径:\??\C:\test.txt。

参考

参考自《Windows黑客编程技术详解》一书

上传的附件 cloud_download FileManager_Test.7z ( 13.63kb, 8次下载 )

发送私信

这一切都不是我的,但总有一天,会是我的

83
文章数
69
评论数
最近文章
eject