Windows本机应用程序和Acronis Active Restore

今天,我们继续讲述有关我们如何与Innopolis大学的人员一起开发Active Restore技术的故事,该技术可使用户在出现故障后尽快开始在其计算机上工作。 我们将讨论本机Windows应用程序,包括其创建和启动的功能。 切入-有关我们的项目的一些知识,以及有关如何编写本机应用程序的实用指南。



在之前的文章中,我们已经讨论了什么是Active Restore ,以及Innopolis的学生如何开发该服务 。 今天,我想专注于本机应用程序,在这种级别上我们要“掩埋”我们的主动恢复服务。 如果一切顺利,那么我们可以:

  • 更早地启动服务本身
  • 更早地联系备份所在的云
  • 了解系统处于哪种模式要早得多-正常启动或恢复
  • 提前还原少得多的文件
  • 允许用户更快上手。

一般而言,什么是本机应用程序?


为了回答这个问题,让我们看一下系统进行的调用顺序,例如,如果应用程序中的程序员尝试创建文件。


Pavel Yosifovich-Windows内核编程(2019)

程序员使用CreateFile函数,该函数在fileapi.h头文件中声明,并在Kernel32.dll中实现。 但是,此函数本身不会创建文件;它只会检查输入中的参数并调用NtCreateFile函数(Nt前缀仅表示该函数是本机的)。 该函数在winternl.h头文件中声明,并在ntdll.dll中实现。 她准备跳入核空间,然后进行系统调用以创建文件。 在这种情况下,事实证明Kernel32只是Ntdll的包装器。 这样做的原因之一是,Microsoft因此可以更改本地世界的功能,但不能使用标准界面。 Microsoft不建议直接调用本机功能,并且不记录其中的大多数功能。 顺便说一句,未记录的功能可以在这里找到。

本机应用程序的主要优点是ntdll早于kernel32加载到系统中。 这是合乎逻辑的,因为kernel32需要ntdll才能工作。 结果,使用本机功能的应用程序可以更早地开始工作。

因此,Windows本机应用程序是可以在启动Windows的早期阶段运行的程序。 他们仅使用ntdll中的函数。 这样的应用程序的一个示例: autochk ,它在启动主要服务之前执行chkdisk实用程序以检查磁盘是否有错误。 我们希望在此级别上看到活动还原。

我们需要什么?


  • DDK (驱动程序开发套件),现在也称为WDK 7(Windows驱动程序套件)。
  • 虚拟机(例如Windows 7 x64)
  • 不一定,但是可以在此处下载头文件

代码是什么?


让我们进行一些练习,以一个示例为例,我们将编写一个小应用程序:

  1. 在屏幕上显示一条消息。
  2. 分配一点内存
  3. 等待键盘输入
  4. 释放忙碌的记忆

在本机应用程序中,入口点不是main或winmain,而是NtProcessStartup函数,因为实际上我们直接在系统中启动了新进程。

让我们从在屏幕上显示消息开始。 为此,我们有一个本地函数NtDisplayString ,该函数将指向UNICODE_STRING结构的对象的指针作为参数。 RtlInitUnicodeString将帮助我们对其进行初始化。 结果,为了在屏幕上显示文本,我们可以编写一个很小的函数:

//usage: WriteLn(L"Here is my text\n"); void WriteLn(LPWSTR Message) { UNICODE_STRING string; RtlInitUnicodeString(&string, Message); NtDisplayString(&string); } 

由于只有ntdll中的函数可供我们使用,而且内存中还没有其他库,因此我们肯定会在如何分配内存方面遇到问题。 新操作符尚不存在(因为它来自过于高级的C ++世界),也没有malloc函数(它需要运行时C库)。 当然,您只能使用堆栈。 但是,如果我们需要动态分配内存,则必须在堆(即堆)上执行此操作。 因此,让我们为自己创建一堆,并在需要时从中获取内存。

RtlCreateHeap函数适用于此任务。 此外,使用RtlAllocateHeap和RtlFreeHeap,我们将在需要时占用并释放内存。

 PVOID memory = NULL; PVOID buffer = NULL; ULONG bufferSize = 42; // create heap in order to allocate memory later memory = RtlCreateHeap( HEAP_GROWABLE, NULL, 1000, 0, NULL, NULL ); // allocate buffer of size bufferSize buffer = RtlAllocateHeap( memory, HEAP_ZERO_MEMORY, bufferSize ); // free buffer (actually not needed because we destroy heap in next step) RtlFreeHeap(memory, 0, buffer); RtlDestroyHeap(memory); 

让我们继续等待键盘输入。

 // https://docs.microsoft.com/en-us/windows/win32/api/ntddkbd/ns-ntddkbd-keyboard_input_data typedef struct _KEYBOARD_INPUT_DATA { USHORT UnitId; USHORT MakeCode; USHORT Flags; USHORT Reserved; ULONG ExtraInformation; } KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA; //... HANDLE hKeyBoard, hEvent; UNICODE_STRING skull, keyboard; OBJECT_ATTRIBUTES ObjectAttributes; IO_STATUS_BLOCK Iosb; LARGE_INTEGER ByteOffset; KEYBOARD_INPUT_DATA kbData; // inialize variables RtlInitUnicodeString(&keyboard, L"\\Device\\KeyboardClass0"); InitializeObjectAttributes(&ObjectAttributes, &keyboard, OBJ_CASE_INSENSITIVE, NULL, NULL); // open keyboard device NtCreateFile(&hKeyBoard, SYNCHRONIZE | GENERIC_READ | FILE_READ_ATTRIBUTES, &ObjectAttributes, &Iosb, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN,FILE_DIRECTORY_FILE, NULL, 0); // create event to wait on InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL); NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, &ObjectAttributes, 1, 0); while (TRUE) { NtReadFile(hKeyBoard, hEvent, NULL, NULL, &Iosb, &kbData, sizeof(KEYBOARD_INPUT_DATA), &ByteOffset, NULL); NtWaitForSingleObject(hEvent, TRUE, NULL); if (kbData.MakeCode == 0x01) // if ESC pressed { break; } } 

我们需要做的就是在打开的设备上使用NtReadFile ,然后等到键盘将单击返回给我们。 如果按下ESC键,我们将继续工作。 要打开设备,我们需要调用NtCreateFile函数(您需要打开\ Device \ KeyboardClass0)。 我们还将调用NtCreateEvent初始化要等待的对象。 我们将独立声明代表键盘数据的KEYBOARD_INPUT_DATA结构。 这将有助于我们的工作。

本机应用程序以对NtTerminateProcess函数的调用结尾,因为我们只是杀死了自己的进程。

我们的小型应用程序的所有代码:

 #include "ntifs.h" // \WinDDK\7600.16385.1\inc\ddk #include "ntdef.h" //------------------------------------ // Following function definitions can be found in native development kit // but I am too lazy to include `em so I declare it here //------------------------------------ NTSYSAPI NTSTATUS NTAPI NtTerminateProcess( IN HANDLE ProcessHandle OPTIONAL, IN NTSTATUS ExitStatus ); NTSYSAPI NTSTATUS NTAPI NtDisplayString( IN PUNICODE_STRING String ); NTSTATUS NtWaitForSingleObject( IN HANDLE Handle, IN BOOLEAN Alertable, IN PLARGE_INTEGER Timeout ); NTSYSAPI NTSTATUS NTAPI NtCreateEvent( OUT PHANDLE EventHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN EVENT_TYPE EventType, IN BOOLEAN InitialState ); // https://docs.microsoft.com/en-us/windows/win32/api/ntddkbd/ns-ntddkbd-keyboard_input_data typedef struct _KEYBOARD_INPUT_DATA { USHORT UnitId; USHORT MakeCode; USHORT Flags; USHORT Reserved; ULONG ExtraInformation; } KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA; //---------------------------------------------------------- // Our code goes here //---------------------------------------------------------- // usage: WriteLn(L"Hello Native World!\n"); void WriteLn(LPWSTR Message) { UNICODE_STRING string; RtlInitUnicodeString(&string, Message); NtDisplayString(&string); } void NtProcessStartup(void* StartupArgument) { // it is important to declare all variables at the beginning HANDLE hKeyBoard, hEvent; UNICODE_STRING skull, keyboard; OBJECT_ATTRIBUTES ObjectAttributes; IO_STATUS_BLOCK Iosb; LARGE_INTEGER ByteOffset; KEYBOARD_INPUT_DATA kbData; PVOID memory = NULL; PVOID buffer = NULL; ULONG bufferSize = 42; //use it if debugger connected to break //DbgBreakPoint(); WriteLn(L"Hello Native World!\n"); // inialize variables RtlInitUnicodeString(&keyboard, L"\\Device\\KeyboardClass0"); InitializeObjectAttributes(&ObjectAttributes, &keyboard, OBJ_CASE_INSENSITIVE, NULL, NULL); // open keyboard device NtCreateFile(&hKeyBoard, SYNCHRONIZE | GENERIC_READ | FILE_READ_ATTRIBUTES, &ObjectAttributes, &Iosb, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN,FILE_DIRECTORY_FILE, NULL, 0); // create event to wait on InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL); NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, &ObjectAttributes, 1, 0); WriteLn(L"Keyboard ready\n"); // create heap in order to allocate memory later memory = RtlCreateHeap( HEAP_GROWABLE, NULL, 1000, 0, NULL, NULL ); WriteLn(L"Heap ready\n"); // allocate buffer of size bufferSize buffer = RtlAllocateHeap( memory, HEAP_ZERO_MEMORY, bufferSize ); WriteLn(L"Buffer allocated\n"); // free buffer (actually not needed because we destroy heap in next step) RtlFreeHeap(memory, 0, buffer); RtlDestroyHeap(memory); WriteLn(L"Heap destroyed\n"); WriteLn(L"Press ESC to continue...\n"); while (TRUE) { NtReadFile(hKeyBoard, hEvent, NULL, NULL, &Iosb, &kbData, sizeof(KEYBOARD_INPUT_DATA), &ByteOffset, NULL); NtWaitForSingleObject(hEvent, TRUE, NULL); if (kbData.MakeCode == 0x01) // if ESC pressed { break; } } NtTerminateProcess(NtCurrentProcess(), 0); } 

PS:我们可以轻松地使用代码中的DbgBreakPoint()函数在调试器中停止。 没错,您需要将WinDbg连接到虚拟机以进行内核调试。 有关如何执行此操作的说明,请参见此处,或仅使用VirtualKD

编译组装


构建本机应用程序的最简单方法是使用DDK (驱动程序开发套件)。 我们完全需要旧的第七个版本,因为更高版本的方法略有不同,并且可以与Visual Studio紧密协作。 如果我们使用DDK,则我们的项目仅需要Makefile和源代码。

生成文件
 !INCLUDE $(NTMAKEENV)\makefile.def 

资料来源:
 TARGETNAME = MyNative TARGETTYPE = PROGRAM UMTYPE = nt BUFFER_OVERFLOW_CHECKS = 0 MINWIN_SDK_LIB_PATH = $(SDK_LIB_PATH) SOURCES = source.c INCLUDES = $(DDK_INC_PATH); \ C:\WinDDK\7600.16385.1\ndk; TARGETLIBS = $(DDK_LIB_PATH)\ntdll.lib \ $(DDK_LIB_PATH)\nt.lib USE_NTDLL = 1 

您的Makefile将完全相同,但让我们更详细地介绍源代码。 该文件包含程序的源文件(.c文件),构建选项和其他参数。

  • TARGETNAME-可执行文件的名称,应为结果。
  • TARGETTYPE-可执行文件的类型,它可以是驱动程序(.sys),则字段值应为DRIVER,如果是库文件(.lib),则值为LIBRARY。 在我们的例子中,我们需要一个可执行文件(.exe),因此我们将值设置为PROGRAM。
  • UMTYPE-此字段的可能值:控制台应用程序的控制台,窗口以窗口模式运行。 但是我们需要指定nt来获取本机应用程序。
  • BUFFER_OVERFLOW_CHECKS-检查堆栈是否有缓冲区溢出,不幸的是在我们的情况下,将其关闭。
  • MINWIN_SDK_LIB_PATH-此值引用变量SDK_LIB_PATH,请不要担心您尚未声明这样的系统变量,当我们从DDK运行经检查的构建时,将声明此变量并将其指向必要的库。
  • 源-程序源的列表。
  • 包含-组装所需的头文件。 它们通常指示DDK随附文件的路径,但是您可以选择指定其他文件。
  • TARGETLIBS-需要链接的库的列表。
  • USE_NTDLL是必填字段,必须将其设置为位置1。出于明显的原因。
  • USER_C_FLAGS-在准备应用程序代码时可以在预处理程序指令中使用的任何标志。

因此,要构建,我们需要运行x86(或x64)Checked Build,将工作目录更改为项目文件夹,然后执行Build命令。 屏幕截图中的结果表明我们已经收集了一个可执行文件。

建立

这个文件不能这么简单地运行,系统发誓并向我们发送有关以下错误的行为的思考:

失误


如何运行本机应用程序?


在autochk开始时,程序的启动顺序由注册表项的值确定:

 HKLM\System\CurrentControlSet\Control\Session Manager\BootExecute 

会话管理器一一执行该列表中的程序。 会话管理器本身在system32目录中查找可执行文件。 注册表项值的格式如下:

 autocheck autochk *MyNative 

该值应为十六进制格式,而不是通常的ASCII,因此,上面显示的密钥将具有以下格式:

 61,75,74,6f,63,68,65,63,6b,20,61,75,74,6f,63,68,6b,20,2a,00,4d,79,4e,61,74,69,76,65,00,00 

要转换名称,您可以使用在线服务,例如this


事实证明,要运行本机应用程序,我们需要:

  1. 将可执行文件复制到system32文件夹
  2. 将密钥添加到注册表
  3. 重新启动机器

为了方便起见,以下是用于安装本机应用程序的现成脚本:

install.bat

 @echo off copy MyNative.exe %systemroot%\system32\. regedit /s add.reg echo Native Example Installed pause 

add.reg

 REGEDIT4 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager] "BootExecute"=hex(7):61,75,74,6f,63,68,65,63,6b,20,61,75,74,6f,63,68,6b,20,2a,00,4d,79,4e,61,74,69,76,65,00,00 

安装并重新启动后,甚至在出现用户选择屏幕之前,我们得到以下图片:

结果

总结


通过使用这样一个小型应用程序的示例,我们确信可以在Windows本机级别运行该应用程序。 此外,来自Innopolis University的人员将继续构建一项服务,该服务将比我们项目的先前版本早得多地启动与驾驶员进行交互的过程。 随着win32 shell的出现,将控制权转移到已经开发的成熟服务将是合乎逻辑的(在此有更多信息 )。

在下一篇文章中,我们将介绍Active Restore服务的另一个组件,即UEFI驱动程序。 订阅我们的博客,不要错过下一篇文章。

Source: https://habr.com/ru/post/zh-CN481382/


All Articles