Dzisiaj kontynuujemy naszą opowieść o współpracy z Uniwersytetem Innopolis nad rozwojem technologii Active Restore, która umożliwi użytkownikom jak najszybsze wznowienie pracy na komputerach po awarii. Porozmawiamy o aplikacjach natywnych. Windows, w tym szczegóły dotyczące ich tworzenia i uruchomienia. Poniżej znajduje się krótkie wprowadzenie do naszego projektu, a także praktyczny przewodnik po pisaniu aplikacji natywnych.

W poprzednich postach pisaliśmy już o tym, co to jest i jak rozwijają się uczniowie z Innopolis . Dziś chcę się skupić na aplikacjach natywnych, do poziomu których chcemy „zakopać” naszą usługę aktywnego odzyskiwania. Jeśli wszystko się powiedzie, będziemy mogli:
- Uruchom samą usługę znacznie wcześniej
- Skontaktuj się z chmurą, w której znajduje się kopia zapasowa, znacznie wcześniej
- Znacznie wcześniej, aby zrozumieć, w jakim trybie znajduje się system - normalny rozruch lub odzyskiwanie
- Znacznie mniej plików do wcześniejszego odzyskania
- Pozwól użytkownikowi rozpocząć pracę jeszcze szybciej.
Czym w ogóle jest aplikacja natywna?
Aby odpowiedzieć na to pytanie, przyjrzyjmy się sekwencji wywołań, jakie wykonuje system, jeśli np. programista w swojej aplikacji próbuje utworzyć plik.

Paweł Josifowicz — Windows Programowanie jądra (2019)
Programista korzysta z tej funkcji , który jest zadeklarowany w pliku nagłówkowym fileapi.h i zaimplementowany w Kernel32.dll. Jednak ta funkcja sama nie tworzy pliku, sprawdza jedynie argumenty wejściowe i wywołuje funkcję (przedrostek Nt wskazuje po prostu, że funkcja jest natywna). Ta funkcja jest zadeklarowana w pliku nagłówkowym winternl.h i zaimplementowana w ntdll.dll. Przygotowuje się do skoku w przestrzeń nuklearną, po czym wykonuje wywołanie systemowe w celu utworzenia pliku. W tym przypadku okazuje się, że Kernel32 jest po prostu opakowaniem dla Ntdll. Jednym z powodów, dla których tak zrobiono, jest to, że Microsoft ma w ten sposób możliwość zmiany funkcji natywnego świata, ale nie dotykania standardowych interfejsów. Microsoft nie zaleca bezpośredniego wywoływania funkcji natywnych i nie dokumentuje większości z nich. Nawiasem mówiąc, można znaleźć nieudokumentowane funkcje .
Główną zaletą aplikacji natywnych jest to, że ntdll jest ładowany do systemu znacznie wcześniej niż kernel32. Jest to logiczne, ponieważ kernel32 wymaga do działania ntdll. Dzięki temu aplikacje korzystające z natywnych funkcji mogą zacząć działać znacznie wcześniej.
Tak więc, Windows Aplikacje natywne to programy, które można uruchomić wcześniej podczas rozruchu. WindowsUżywają TYLKO funkcji z ntdll. Przykład takiego zastosowania: kto występuje aby sprawdzić dysk pod kątem błędów przed uruchomieniem głównych usług. To jest dokładnie taki poziom, na jakim chcemy, żeby było nasze Active Restore.
Czego potrzebujemy?
- (Zestaw narzędzi do tworzenia sterowników), obecnie znany również jako WDK 7 (Windows Zestaw sterowników).
- Maszyna wirtualna (np. Windows 7x64)
- Nie jest to konieczne, ale pomocne mogą być pliki nagłówkowe, które można pobrać
Co jest w kodzie?
Poćwiczmy trochę i napiszmy np. małą aplikację, która:
- Wyświetla komunikat na ekranie
- Przydziela część pamięci
- Oczekuje na wejście z klawiatury
- Zwalnia używaną pamięć
W aplikacjach natywnych punktem wejścia nie jest main czy winmain, ale funkcja NtProcessStartup, gdyż tak naprawdę bezpośrednio uruchamiamy nowe procesy w systemie.
Zacznijmy od wyświetlenia komunikatu na ekranie. Do tego mamy funkcję natywną , który jako argument przyjmuje wskaźnik do obiektu struktury UNICODE_STRING. RtlInitUnicodeString pomoże nam go zainicjować. W rezultacie, aby wyświetlić tekst na ekranie, możemy napisać tę małą funkcję:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Jako że dostępne są dla nas tylko funkcje z ntdll, a w pamięci po prostu nie ma jeszcze innych bibliotek, to na pewno będziemy mieli problem z alokacją pamięci. Nowy operator jeszcze nie istnieje (ponieważ pochodzi ze zbyt wysokiego poziomu świata C++) i nie ma funkcji malloc (wymaga bibliotek wykonawczych C). Oczywiście możesz używać tylko stosu. Ale jeśli będziemy musieli dynamicznie przydzielać pamięć, będziemy musieli to zrobić na stercie (tj. stercie). Stwórzmy więc dla siebie stertę i wyciągajmy z niej pamięć, kiedy tylko tego potrzebujemy.
Funkcja jest odpowiednia do tego zadania . Następnie za pomocą RtlAllocateHeap i RtlFreeHeap będziemy zajmować i zwalniać pamięć, kiedy jej potrzebujemy.
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);Przejdźmy do oczekiwania na wejście z klawiatury.
// 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;
}
}Wszystko, czego potrzebujemy, to użyć na otwartym urządzeniu i poczekaj, aż klawiatura zwróci nam jakiekolwiek naciśnięcie. Jeśli zostanie naciśnięty klawisz ESC, będziemy kontynuować pracę. Aby otworzyć urządzenie będziemy musieli wywołać funkcję NtCreateFile (będziemy musieli otworzyć DeviceKeyboardClass0). Zadzwonimy również aby zainicjować obiekt oczekiwania. Sami zadeklarujemy strukturę KEYBOARD_INPUT_DATA, która reprezentuje dane klawiatury. Ułatwi nam to pracę.
Aplikacja natywna kończy się wywołaniem funkcji ponieważ po prostu zabijamy nasz własny proces.
Cały kod naszej małej aplikacji:
#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: Możemy łatwo użyć funkcji DbgBreakPoint() w naszym kodzie, aby zatrzymać ją w debugerze. To prawda, że będziesz musiał podłączyć WinDbg do maszyny wirtualnej w celu debugowania jądra. Instrukcje, jak to zrobić, można znaleźć lub po prostu użyj .
Kompilacja i montaż
Najłatwiejszą metodą zbudowania aplikacji natywnej jest użycie (Zestaw deweloperski sterownika). Potrzebujemy starożytnej siódmej wersji, ponieważ późniejsze wersje mają nieco inne podejście i ściśle współpracują z Visual Studio. Jeśli używamy DDK, nasz projekt potrzebuje tylko pliku Makefile i źródeł.
Makefile
!INCLUDE $(NTMAKEENV)makefile.defźródła:
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 = 1Twój plik Makefile będzie dokładnie taki sam, ale przyjrzyjmy się źródłom nieco bardziej szczegółowo. Ten plik określa źródła programu (pliki .c), opcje kompilacji i inne parametry.
- TARGETNAME – nazwa pliku wykonywalnego, który ma zostać wygenerowany na końcu.
- TARGETTYPE – typ pliku wykonywalnego, może to być sterownik (.sys), wówczas pole powinno mieć wartość DRIVER, jeżeli jest to biblioteka (.lib), wówczas wartość to BIBLIOTEKA. W naszym przypadku potrzebny jest plik wykonywalny (.exe), dlatego ustawiamy wartość na PROGRAM.
- UMTYPE – możliwe wartości tego pola: konsola dla aplikacji konsolowej, okna do pracy w trybie okienkowym. Ale musimy określić nt, aby uzyskać aplikację natywną.
- BUFFER_OVERFLOW_CHECKS – sprawdzanie stosu pod kątem przepełnienia bufora, niestety nie w naszym przypadku, wyłączamy to.
- MINWIN_SDK_LIB_PATH – ta wartość odnosi się do zmiennej SDK_LIB_PATH, nie martw się, że nie masz zadeklarowanej takiej zmiennej systemowej, gdy uruchomimy sprawdzoną kompilację z DDK, zmienna ta zostanie zadeklarowana i wskaże potrzebne biblioteki.
- ŹRÓDŁA – lista źródeł Twojego programu.
- ZAWIERA – pliki nagłówkowe wymagane do montażu. Tutaj zwykle wskazują ścieżkę do plików dostarczonych z DDK, ale możesz dodatkowo podać dowolne inne.
- TARGETLIBS – lista bibliotek, które należy połączyć.
- USE_NTDLL jest polem wymaganym, które z oczywistych powodów musi być ustawione na 1.
- USER_C_FLAGS – dowolne flagi, których możesz użyć w dyrektywach preprocesora podczas przygotowywania kodu aplikacji.
Aby więc zbudować, musimy uruchomić x86 (lub x64) Checked Build, zmienić katalog roboczy na folder projektu i uruchomić polecenie Build. Wynik na zrzucie ekranu pokazuje, że mamy jeden plik wykonywalny.

Tego pliku nie można tak łatwo uruchomić, system przeklina i każe nam pomyśleć o jego zachowaniu, wyświetlając następujący błąd:

Jak uruchomić natywną aplikację?
Po uruchomieniu autochk kolejność uruchamiania programów jest określana na podstawie wartości klucza rejestru:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteMenedżer sesji uruchamia programy z tej listy jeden po drugim. Menedżer sesji sam szuka plików wykonywalnych w katalogu system32. Format wartości klucza rejestru jest następujący:
autocheck autochk *MyNativeWartość musi być w formacie szesnastkowym, a nie zwykłym ASCII, więc klucz pokazany powyżej będzie miał format:
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,00Do konwersji tytułu możesz skorzystać z usługi online, np. .

Okazuje się, że do uruchomienia aplikacji natywnej potrzebujemy:
- Skopiuj plik wykonywalny do folderu system32
- Dodaj klucz do rejestru
- Uruchom ponownie maszynę
Dla wygody poniżej gotowy skrypt do instalacji aplikacji natywnej:
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
pausedodaj.reg
REGEDIT4
[HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession 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,00Po instalacji i ponownym uruchomieniu, jeszcze zanim pojawi się ekran wyboru użytkownika, otrzymamy następujący obraz:

Łączny
Biorąc za przykład tę małą aplikację, byliśmy przekonani, że uruchomienie aplikacji na poziomie Windows Native jest całkowicie możliwe. Następnie, wraz z ekipą z Uniwersytetu Innopolis, będziemy kontynuować tworzenie usługi, która będzie inicjować interakcję ze sterownikiem znacznie wcześniej niż w poprzedniej wersji naszego projektu. Wraz z pojawieniem się powłoki Win32, logiczne będzie przeniesienie kontroli do w pełni rozwiniętej usługi, która już została opracowana (więcej o tym później). ).
W następnym artykule poruszymy inny element usługi Active Restore, a mianowicie sterownik UEFI. Zapisz się do naszego bloga, aby nie przegapić kolejnego wpisu.
Źródło: www.habr.com
