Днес продължаваме разказа за това как заедно с момчетата от Университета Иннополис разработваме технологията Active Restore, за да позволим на потребителя да започне работа на своята машина възможно най-скоро след авария. Ще говорим за нативни приложения Windows, включително особеностите на тяхното създаване и стартиране. Под кат – малко за нашия проект, а също така практическо ръководство как да пишем нативни приложения.

В предишните постове вече говорихме какво е , и как студентите от Иннополис разработват . Днес искам да се спра на нативните приложения, до нивото на които искаме да "погребем" нашия сервис за активно възстановяване. Ако всичко върви по план, ще можем:
- Много по-рано да стартираме самия сервис
- Много по-рано да се свържем с облака, в който се намира бекъпът
- Много по-рано да разберем в какъв режим се намира системата – нормално зареждане или възстановяване
- Много по-малко файлове да възстановяваме предварително
- Да позволим на потребителя да започне работа още по-бързо.
Какво всъщност е нативно приложение?
За да отговорим на този въпрос, нека погледнем последователността от извиквания, които системата прави, например, ако програмистът в своето приложение се опитва да създаде файл.

Pavel Yosifovich — Windows Kernel Programming (2019)
Програмистът използва функцията , която е обявена в заглавния файл fileapi.h и реализирана в Kernel32.dll. Обаче самата тази функция не създава файл, а просто проверява аргументите на входа и извиква функцията (представката Nt показва, че функцията е нативна). Тази функция е обявена в заглавния файл winternl.h и реализирана в ntdll.dll. Тя подготвя преминаването в ядрено пространство, след което извършва системно извикване за създаване на файл. В този случай се оказва, че Kernel32 е просто обвивка за Ntdll. Една от причините за това е, че Microsoft по този начин има възможност да променя функциите на нативния свят, но без да засяга стандартните интерфейси. Microsoft не препоръчва директно извикване на нативни функции и не документира голяма част от тях. Между другото, недокументираните функции могат да бъдат намерени .
Основното предимство на нативните приложения е, че ntdll се зарежда в системата значително по-рано от kernel32. Това е логично, тъй като kernel32 изисква наличието на ntdll, за да работи. В резултат на това приложенията, които използват нативни функции, могат да започнат работа значително по-рано.
Следователно, Windows Native Applications са програми, способни да се стартират на ранния етап на зареждане на Windows. Те използват САМО функции от ntdll. Пример за такова приложение: което изпълнява за проверка на диска за грешки още преди стартиране на основните услуги. Именно на такова ниво искаме да видим нашето Active Restore.
Какво ще ни трябва?
- (Driver Development Kit), вече известен и като WDK 7 (Windows Driver Kit).
- Виртуална машина (например, Windows 7 x64)
- Не е задължително, но могат да помогнат header файлове, които могат да се изтеглят
Какво ще видим в кода?
Нека малко се упражним и за пример да напишем малко приложение, което:
- Извежда съобщение на екрана
- Аллоцира малко памет
- Изчаква вход от клавиатурата
- Освобождава заетата памет
В нативните приложения точката на вход е не main или winmain, а функцията NtProcessStartup, тъй като всъщност директно стартираме нови процеси в системата.
Нека започнем с извеждане на съобщение на екрана. За това имаме нативна функция , която приема указател на обект от структурата UNICODE_STRING като аргумент. Инициализацията на тази структура ще ни помогне с RtlInitUnicodeString. В резултат, за извеждане на текст на екрана можем да напишем следната малка функция:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Тъй като имаме достъп само до функции от ntdll и други библиотеки в паметта просто не съществуват, ще имаме затруднения с алокацията на памет. Операторът new все още не съществува (тъй като е от твърде високо ниво на C++), също така няма функция malloc (за нея са необходими библиотеки runtime C). Можем да ползваме само стека. Но ако трябва да алокираме динамично памет, ще трябва да го направим в купа (т.е. heap). Затова нека да си създадем купа и да взимаме доза памет, когато ни е необходима.
За тази задача подхожда функцията . След това, използвайки RtlAllocateHeap и RtlFreeHeap, ще заемаме и освобождаваме памет, когато това е необходимо.
PVOID памет = NULL;
PVOID буфер = NULL;
ULONG размерНаБуфера = 42;
// създаване на купчина, за да можем да разпределим памет по-късно
памет = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// разпределяне на буфер с размер размерНаБуфера
буфер = RtlAllocateHeap(
памет,
HEAP_ZERO_MEMORY,
размерНаБуфера
);
// освобождаване на буфера (всъщност не е необходимо, защото унищожаваме купчината на следващата стъпка)
RtlFreeHeap(памет, 0, буфер);
RtlDestroyHeap(памет);Да преминем към изчакване на вход от клавиатурата.
// 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"DeviceKeyboardClass0");
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;
}
}Всичко, от което се нуждаем, е да използваме на отвореното устройство и да чакаме, докато клавиатурата не върне някое натискане. В случай, че натиснем клавиша ESC, ние ще продължим работата. За да отворим устройството, ще трябва да извикаме функцията NtCreateFile (трябва да отворим DeviceKeyboardClass0). Освен това ще извикаме , за да инициализираме обект за изчакване. Ние сами ще обявим структурата KEYBOARD_INPUT_DATA, която представлява данните от клавиатурата. Това ще улесни нашата работа.
Работата на нативното приложение завършва с извикването на функцията , защото просто убиваме собствения си процес.
Целият код на нашето малко приложение:
#include "ntifs.h" // WinDDK7600.16385.1incddk
#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"DeviceKeyboardClass0");
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 readyn");
// create heap in order to allocate memory later
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
WriteLn(L"Heap readyn");
// allocate buffer of size bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
WriteLn(L"Buffer allocatedn");
// free buffer (actually not needed because we destroy heap in next step)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);
WriteLn(L"Heap destroyedn");
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 с виртуалната машина за ядрен дебъгинг. Инструкцията как да направите това може да бъде намерена или просто да използвате .
Компилация и изграждане
Най-простият начин да съберете нативно приложение е да използвате (Driver Development Kit). Нуждаем се точно от старата седма версия, тъй като по-късните версии имат съвсем различен подход и работят тясно с Visual Studio. Ако използваме DDK, нашият проект се нуждае само от Makefile и sources.
Makefile
!INCLUDE $(NTMAKEENV)makefile.defsources:
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:WinDDK7600.16385.1ndk;
TARGETLIBS = $(DDK_LIB_PATH)ntdll.lib
$(DDK_LIB_PATH)nt.lib
USE_NTDLL = 1Вашият Makefile ще бъде точно такъв, а за sources нека спрем малко по-подробно. В този файл се посочват източниците на вашата програма (файлове .c), опции за компилация и други параметри.
- TARGETNAME – името на изпълняемия файл, който трябва да се получи в крайна сметка.
- TARGETTYPE – тип на изпълняемия файл, това може да бъде драйвер (.sys), тогава стойността на полето трябва да бъде DRIVER, ако е библиотека (.lib), стойността LIBRARY. В нашия случай ни трябва изпълняем файл (.exe), затова задаваме стойността PROGRAM.
- UMTYPE – възможни стойности за това поле: console за конзолно приложение, windows за работа в прозоречен режим. Но трябва да зададем nt, за да получим нативно приложение.
- BUFFER_OVERFLOW_CHECKS – проверка на стека за преливане на буфера, за съжаление, не е нашият случай, изключваме.
- MINWIN_SDK_LIB_PATH – тази стойност сочи към променливата SDK_LIB_PATH, не трябва да се притеснявате, ако подобна системна променлива не е обявена, когато стартираме checked build от DDK, тази променлива ще бъде обявена и ще сочи към необходимите библиотеки.
- SOURCES – списък с изходните файлове на вашата програма.
- INCLUDES – заглавни файлове, които са необходими за компилация. Тук обикновено се указва пътят към файловете, които идват с DDK, но можете да посочите допълнително всякакви други.
- TARGETLIBS – списък с библиотеки, които трябва да бъдат свързани.
- USE_NTDLL – задължително поле, което трябва да бъде зададено на 1. По напълно очевидни причини.
- USER_C_FLAGS – всякакви флагове, които можете да използвате в директивите на предпроцесора при подготовка на кода на приложението.
И така, за да компилираме, трябва да стартираме x86 (или x64) Checked Build, да сменим работната директория на папката с проекта и да изпълним командата Build. Резултатът на екрана показва, че сме събрали един изпълним файл.

Този файл не може просто да се стартира, системата се оплаква и ни изпраща да помислим за поведението си с следната грешка:

Как да стартираме нативно приложение?
При стартиране, autochk последователността на стартиране на програмите се определя от стойността на ключа в регистъра:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteМениджърът на сесиите поетапно изпълнява програмите от този списък. Самите изпълними файлове мениджърът на сесиите търси в директорията 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За да конвертирате името, можете да използвате онлайн услуга, например, .

Получава се, че за да стартираме нативно приложение, трябва:
- Да копираме изпълнимия файл в папката system32
- Да добавим ключ в регистъра
- Да перезагрузим машината
За удобство ето ви готов скрипт за инсталиране на нативното приложение:
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo НатNative установен
pauseadd.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 Native. По-нататък ние и партньорите от Университета Иннополис ще продължим да изграждаме услуга, която ще подготви взаимодействието с драйвера много по-рано, отколкото в предишната версия на нашия проект. А с появата на обвивката win32, ще бъде логично да предадем управлението на напълно разработена услуга, която вече е на разположение (повече за това ).
В следващата статия ще обсъдим още един компонент на услугата Active Restore, а именно UEFI драйвера. Абонирайте се за блога ни, за да не пропуснете следващата публикация.
Източник: habr.com
