Aplikacje natywne Windows i usługa Acronis Active Restore

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.

Aplikacje natywne Windows i usługa Acronis Active Restore

W poprzednich postach mówiliśmy już o tym, czym jest Active Restore, oraz jak studenci z Innopolis rozwijają usługa. 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.

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

Programista używa funkcji CreateFile, 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ę NtCreateFile (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źć tutaj.

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: autochk który wykonuje chkdisk utility 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?

  • DDK (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ć. tutaj

Co z kodem?

Przećwiczmy trochę i napiszmy małą aplikację, która:

  1. Wyświetla komunikat na ekranie
  2. Alokuje trochę pamięci
  3. Czeka na wejście z klawiatury
  4. 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ę NtDisplayString, 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 RtlCreateHeap. 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ć NtReadFile 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 NtCreateEvent, 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 NtTerminateProcess, 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źć tutaj lub po prostu używać VirtualKD.

Kompilacja i budowa

Najprostszy sposób na zbudowanie aplikacji natywnej to użycie DDK (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.def

sources:

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

Aplikacje natywne Windows i usługa Acronis Active Restore

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:

Aplikacje natywne Windows i usługa Acronis Active Restore

Jak uruchomić aplikację natywną?

W momencie uruchamiania autochk sekwencja uruchamiania programów określana jest przez wartość klucza rejestru:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

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

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

Aby przekonwertować nazwę, możesz użyć usługi online, na przykład, ten.

Aplikacje natywne Windows i usługa Acronis Active Restore
Zatem, aby uruchomić aplikację natywną, musimy:

  1. Skopiować plik wykonywalny do folderu system32
  2. Dodać klucz do rejestru
  3. 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
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

Po zainstalowaniu i ponownym uruchomieniu, jeszcze przed pojawieniem się ekranu wyboru użytkowników, zobaczymy następujący obrazek:

Aplikacje natywne Windows i usługa Acronis Active Restore

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 tutaj).

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

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster