Aplikacje natywne systemu Windows i usługa Acronis Active Restore

Dzisiaj kontynuujemy historię o tym, jak wraz z chłopakami z Uniwersytetu Innopolis rozwijamy technologię Active Restore, aby umożliwić użytkownikowi jak najszybsze rozpoczęcie pracy na swojej maszynie po awarii. Porozmawiamy o natywnych aplikacjach Windows, w tym o funkcjach ich tworzenia i uruchamiania. Poniżej skrótu trochę o naszym projekcie, a także praktyczny poradnik jak pisać aplikacje natywne.

Aplikacje natywne systemu Windows i usługa Acronis Active Restore

W poprzednich postach pisaliśmy już o tym, co to jest Aktywne przywracaniei jak rozwijają się uczniowie z Innopolis usługa. 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.

Aplikacje natywne systemu Windows i usługa Acronis Active Restore
Pavel Yosifovich - Programowanie jądra systemu Windows (2019)

Programista korzysta z tej funkcji Utwórz plik, 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ę NtUtwórz plik (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 tutaj.

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.

Zatem aplikacje natywne systemu Windows to programy, które można uruchomić już podczas uruchamiania systemu Windows. Używają TYLKO funkcji z ntdll. Przykład takiej aplikacji: autochk kto występuje narzędzie chkdisk 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?

  • DDK (Driver Development Kit), obecnie znany również jako WDK 7 (Windows Driver Kit).
  • Maszyna wirtualna (na przykład Windows 7 x64)
  • Nie jest to konieczne, ale pomocne mogą być pliki nagłówkowe, które można pobrać tutaj

Co jest w kodzie?

Poćwiczmy trochę i napiszmy np. małą aplikację, która:

  1. Wyświetla komunikat na ekranie
  2. Przydziela część pamięci
  3. Oczekuje na wejście z klawiatury
  4. 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ą NtDisplayString, 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 RtlUtwórz stertę. 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ć NtReadFile 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ż NtCreateEventaby 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 NtZakończ procesponieważ 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źć tutaj lub po prostu użyj Wirtualny KD.

Kompilacja i montaż

Najłatwiejszą metodą zbudowania aplikacji natywnej jest użycie DDK (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			= 1

Twó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.

Aplikacje natywne systemu Windows i usługa Acronis Active Restore

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:

Aplikacje natywne systemu Windows i usługa Acronis Active Restore

Jak uruchomić natywną aplikację?

Po uruchomieniu autochk kolejność uruchamiania programów jest określana na podstawie wartości klucza rejestru:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Menedż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 *MyNative

Wartość 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,00

Do konwersji tytułu możesz skorzystać z usługi online, np. to.

Aplikacje natywne systemu Windows i usługa Acronis Active Restore
Okazuje się, że do uruchomienia aplikacji natywnej potrzebujemy:

  1. Skopiuj plik wykonywalny do folderu system32
  2. Dodaj klucz do rejestru
  3. 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
pause

dodaj.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,00

Po instalacji i ponownym uruchomieniu, jeszcze zanim pojawi się ekran wyboru użytkownika, otrzymamy następujący obraz:

Aplikacje natywne systemu Windows i usługa Acronis Active Restore

Łączny

Na przykładzie tak małej aplikacji przekonaliśmy się, że uruchomienie aplikacji na poziomie Windows Native jest całkiem możliwe. Następnie wraz z chłopakami z Innopolis University będziemy kontynuować budowę usługi, która zainicjuje proces interakcji z kierowcą znacznie wcześniej niż w poprzedniej wersji naszego projektu. A wraz z pojawieniem się powłoki win32 logiczne byłoby przeniesienie kontroli na pełnoprawną usługę, która została już opracowana (więcej na ten temat tutaj).

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

Dodaj komentarz