Dziś kontynuujemy opowieść o tym, jak razem z chłopakami z Uniwersytetu Innopolis rozwijamy technologię Active Restore, aby umożliwić użytkownikowi jak najszybsze rozpoczęcie pracy na swoim urządzeniu po awarii. Będzie mowa o natywnych aplikacjach Windows, w tym cechach ich tworzenia i uruchamiania. Poniżej – trochę o naszym projekcie, a także praktyczny przewodnik dotyczący pisania aplikacji natywnych.

W poprzednich postach mówiliśmy już o tym, czym jest , oraz jak studenci z Innopolis rozwijają . Dziś chciałbym skupić się na aplikacjach natywnych, do poziomu których chcemy „zakopać” naszą usługę aktywnego odzyskiwania. Jeśli wszystko pójdzie dobrze, będziemy mogli:
- O wiele wcześniej uruchomić samą usługę
- O wiele wcześniej połączyć się z chmurą, w której znajduje się kopia zapasowa
- O wiele wcześniej zrozumieć, w jakim trybie znajduje się system – normalnego uruchamiania czy odzyskiwania
- O wiele mniej plików odzyskiwać z wyprzedzeniem
- Pozwolić użytkownikowi na szybsze rozpoczęcie pracy.
Czym w ogóle jest aplikacja natywna?
Aby odpowiedzieć na to pytanie, spójrzmy na sekwencję wywołań, które wykonuje system, na przykład, jeśli programista w swojej aplikacji próbuje utworzyć plik.

Pavel Yosifovich – Programowanie jądra Windows (2019)
Programista używa funkcji , która jest zadeklarowana w pliku nagłówkowym fileapi.h i zrealizowana w Kernel32.dll. Jednak sama ta funkcja nie zajmuje się tworzeniem pliku, jedynie sprawdza argumenty na wejściu i wywołuje funkcję (przyrostek Nt świadczy o tym, że funkcja jest natywna). Ta funkcja jest zadeklarowana w pliku nagłówkowym winternl.h i zrealizowana w ntdll.dll. Przygotowuje ona skok do przestrzeni jądra, po czym wykonuje wywołanie systemowe w celu utworzenia pliku. W tym przypadku Kernel32 jest jedynie opakowaniem dla Ntdll. Jednym z powodów, dla których tak zrobiono, jest to, że Microsoft ma możliwość modyfikacji funkcji natywnego świata, nie ruszając jednak standardowych interfejsów. Microsoft nie zaleca bezpośredniego wywoływania funkcji natywnych i nie dokumentuje większości z nich. Tak przy okazji, nieudokumentowane funkcje można znaleźć .
Główną zaletą aplikacji natywnych jest to, że ntdll ładowany jest do systemu znacznie wcześniej niż kernel32. To ma sens, ponieważ kernel32 wymaga istnienia ntdll do działania. W rezultacie aplikacje, które korzystają z natywnych funkcji, mogą rozpocząć pracę znacznie wcześniej.
W ten sposób aplikacje natywne Windows to programy, które mogą uruchamiać się na wczesnym etapie ładowania systemu Windows. Używają TYLKO funkcji z ntdll. Przykład takiej aplikacji: który wykonuje w celu sprawdzenia dysku pod kątem błędów jeszcze przed uruchomieniem głównych usług. Właśnie na tym poziomie chcemy zobaczyć naszą funkcję Active Restore.
Czego potrzebujemy?
- (Zestaw narzędzi do tworzenia sterowników), obecnie znany również jako WDK 7 (Zestaw narzędzi do sterowników Windows).
- Maszyna wirtualna (na przykład Windows 7 x64)
- Nie jest to konieczne, ale mogą pomóc pliki nagłówkowe, które można pobrać.
Co z kodem?
Przećwiczmy trochę i napiszmy małą aplikację, która:
- Wyświetla komunikat na ekranie
- Alokuje trochę pamięci
- Czeka na wejście z klawiatury
- Zwalnia zajętą pamięć
W aplikacjach natywnych punktem wejścia nie jest main ani winmain, lecz funkcja NtProcessStartup, ponieważ faktycznie uruchamiamy nowe procesy w systemie bezpośrednio.
Zacznijmy od wyświetlenia komunikatu na ekranie. W tym celu mamy natywną funkcję , która jako argument przyjmuje wskaźnik do obiektu struktury UNICODE_STRING. Inicjalizować go pomoże nam RtlInitUnicodeString. W rezultacie, aby wyświetlić tekst na ekranie, możemy napisać taką małą funkcję:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Ponieważ mamy dostęp tylko do funkcji z ntdll i nie ma jeszcze innych bibliotek w pamięci, napotkamy problemy z alokacją pamięci. Operator new jeszcze nie istnieje (bo pochodzi z zbyt wysokiego świata C++), nie ma też funkcji malloc (potrzebuje bibliotek runtime C). Można oczywiście korzystać tylko ze stosu. Ale jeśli potrzebujemy dynamicznie alokować pamięć, musimy to zrobić w stercie (czyli heap). Dlatego stwórzmy dla siebie stertę i będziemy brać z niej pamięć, gdy zajdzie taka potrzeba.
Do tego zadania nada się funkcja . Następnie, używając RtlAllocateHeap i RtlFreeHeap, będziemy zajmować i zwalniać pamięć, gdy będzie to potrzebne.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// tworzenie sterty w celu późniejszego przydzielenia pamięci
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// przydzielanie bufora o rozmiarze bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// zwalnianie bufora (właściwie niepotrzebne, ponieważ niszczymy stertę w następnym kroku)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);Przejdźmy teraz do oczekiwania na dane 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 czekać, aż klawiatura zwróci nam jakieś naciśnięcie. W przypadku naciśnięcia klawisza ESC, będziemy kontynuować pracę. Aby otworzyć urządzenie, będziemy musieli wywołać funkcję NtCreateFile (musimy otworzyć DeviceKeyboardClass0). Ponadto wywołamy , aby zainicjować obiekt do oczekiwania. Samodzielnie zadeklarujemy strukturę KEYBOARD_INPUT_DATA, która reprezentuje dane wejściowe z klawiatury. To ułatwi nam pracę.
Praca aplikacji natywnej kończy się wywołaniem funkcji , ponieważ po prostu zabijamy 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 swobodnie użyć w kodzie funkcji DbgBreakPoint() do zatrzymania w debuggerze. Należy jednak podłączyć WinDbg do maszyny wirtualnej w celu debugowania jądra. Instrukcję, jak to zrobić, można znaleźć lub po prostu używać .
Kompilacja i budowa
Najprostszy sposób na zbudowanie aplikacji natywnej to użycie (Zestaw narzędzi do tworzenia sterowników). Potrzebujemy dokładnie siódmej, starej wersji, ponieważ późniejsze wersje mają nieco inne podejście i ściśle współpracują z Visual Studio. Jeśli jednak użyjemy DDK, nasz projekt potrzebuje jedynie Makefile i 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 = 1Twój Makefile będzie dokładnie taki sam, zatrzymajmy się jednak na sources nieco dłużej. W tym pliku wskazujemy źródła twojego programu (pliki .c), opcje kompilacji i inne parametry.
- TARGETNAME – nazwa pliku wykonywalnego, który ma być ostatecznie uzyskany.
- TARGETTYPE – typ pliku wykonywalnego, może to być sterownik (.sys), więc wartość powinna być DRIVER, jeśli biblioteka (.lib), to wartość LIBRARY. W naszym przypadku potrzebny jest plik wykonywalny (.exe), dlatego ustawiamy wartość PROGRAM.
- UMTYPE – możliwe wartości tego pola: console dla aplikacji konsolowej, windows dla pracy w trybie okienkowym. Musimy jednak podać nt, aby uzyskać aplikację natywną.
- BUFFER_OVERFLOW_CHECKS – sprawdzenie stosu pod kątem przepełnienia bufora, niestety nie jest to nasz przypadek, wyłączamy.
- MINWIN_SDK_LIB_PATH – ta wartość odnosi się do zmiennej SDK_LIB_PATH, nie martw się, że nie masz zadeklarowanej takiej zmiennej systemowej, w momencie uruchomienia checked build z DDK, ta zmienna zostanie zadeklarowana i będzie wskazywać na potrzebne biblioteki.
- SOURCES – lista źródeł twojego programu.
- INCLUDES – pliki nagłówkowe, które są potrzebne do kompilacji. Zwykle podaje się tutaj ścieżkę do plików dołączanych z DDK, ale możesz dodać dowolne inne.
- TARGETLIBS – lista bibliotek, które należy zlinkować.
- USE_NTDLL – obowiązkowe pole, które musi być ustawione na 1. Z oczywistych powodów.
- USER_C_FLAGS – dowolne flagi, które możesz używać w dyrektywach preprocesora podczas przygotowywania kodu aplikacji.
Aby zbudować, musimy uruchomić x86 (lub x64) Checked Build, zmienić katalog roboczy na folder projektu i wykonać komendę Build. Wynik na zrzucie ekranu pokazuje, że zbudowaliśmy jeden plik wykonywalny.

Ten plik nie może być tak łatwo uruchomiony, system zgłasza błąd i ostrzega nas o swoim zachowaniu z następującym komunikatem:

Jak uruchomić aplikację natywną?
W momencie uruchamiania autochk sekwencja uruchamiania programów określana jest przez wartość klucza rejestru:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteMenadżer sesji kolejno wykonuje programy z tej listy. Same pliki wykonywalne menedżer sesji wyszukuje w katalogu system32. Format wartości klucza rejestru jest następujący:
autocheck autochk *MyNativeWartość powinna być w formacie szesnastkowym, a nie standardowym ASCII, w związku z tym klucz przedstawiony 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,00Aby przekonwertować nazwę, możesz użyć usługi online, na przykład, .

Zatem, aby uruchomić aplikację natywną, musimy:
- Skopiować plik wykonywalny do folderu system32
- Dodać klucz do rejestru
- Uruchomić ponownie komputer
Dla wygody oto gotowy skrypt do instalacji aplikacji natywnej:
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Zainstalowano przykład 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,00Po zainstalowaniu i ponownym uruchomieniu, jeszcze przed pojawieniem się ekranu wyboru użytkowników, zobaczymy następujący obrazek:

Podsumowanie
Na przykładzie tego niewielkiego aplikacji przekonaliśmy się, że możliwe jest uruchomienie aplikacji na poziomie Windows Native. Następnie będziemy z chłopakami z Uniwersytetu Innopolis kontynuować budowanie usługi, która zainicjuje proces interakcji z sterownikiem znacznie wcześniej niż w poprzedniej wersji naszego projektu. A po wprowadzeniu interfejsu win32 logiczne będzie przekazać kontrolę pełnoprawnej usłudze, która już została opracowana (więcej o tym później ).
W kolejnej części artykułu poruszymy kolejny komponent usługi Active Restore, a mianowicie sterownik UEFI. Subskrybuj nasz blog, aby nie przegapić kolejnego wpisu.
Źródło: habr.com
