Windows Native Applications och Acronis Active Restore-tjänsten

Idag fortsätter vi historien om hur vi, tillsammans med killarna från Innopolis University, utvecklar Active Restore-teknologin för att användaren ska kunna börja arbeta på sin maskin så snart som möjligt efter ett fel. Vi kommer att prata om inbyggda Windows-applikationer, inklusive funktionerna i deras skapande och lansering. Nedan klippet är lite om vårt projekt, samt en praktisk guide om hur man skriver infödda ansökningar.

Windows Native Applications och Acronis Active Restore-tjänsten

I tidigare inlägg har vi redan pratat om vad det är Aktiv återställning, och hur elever från Innopolis utvecklas tjänsten. Idag vill jag fokusera på inbyggda applikationer, till den nivå som vi vill "begrava" vår aktiva återställningstjänst. Om allt fungerar, kommer vi att kunna:

  • Starta själva tjänsten mycket tidigare
  • Kontakta molnet där säkerhetskopian finns mycket tidigare
  • Mycket tidigare för att förstå vilket läge systemet är i - normal start eller återställning
  • Mycket färre filer att återställa i förväg
  • Låt användaren komma igång ännu snabbare.

Vad är en inbyggd app egentligen?

För att svara på denna fråga, låt oss titta på sekvensen av samtal som systemet gör, till exempel om en programmerare i hans applikation försöker skapa en fil.

Windows Native Applications och Acronis Active Restore-tjänsten
Pavel Yosifovich - Windows Kernel Programmering (2019)

Programmeraren använder funktionen Skapa fil, som deklareras i rubrikfilen fileapi.h och implementeras i Kernel32.dll. Denna funktion skapar dock inte själva filen, den kontrollerar bara inmatningsargumenten och anropar funktionen NtCreateFile (prefixet Nt indikerar bara att funktionen är ursprunglig). Denna funktion deklareras i rubrikfilen winternl.h och implementeras i ntdll.dll. Den förbereder sig för att hoppa in i kärnrymden, varefter den gör ett systemanrop för att skapa en fil. I det här fallet visar det sig att Kernel32 bara är ett omslag för Ntdll. En av anledningarna till att detta gjordes är att Microsoft därmed har möjlighet att ändra funktionerna i den infödda världen, men inte röra standardgränssnitten. Microsoft rekommenderar inte att anropa inbyggda funktioner direkt och dokumenterar inte de flesta av dem. Odokumenterade funktioner kan förresten hittas här.

Den största fördelen med inbyggda applikationer är att ntdll läses in i systemet mycket tidigare än kernel32. Detta är logiskt, eftersom kernel32 kräver att ntdll fungerar. Som ett resultat kan applikationer som använder inbyggda funktioner börja fungera mycket tidigare.

Windows Native Applications är alltså program som kan starta tidigt i Windows-start. De använder ENDAST funktioner från ntdll. Ett exempel på en sådan applikation: autochk som uppträder chkdisk verktyg för att kontrollera disken för fel innan du startar huvudtjänsterna. Det är precis den nivån vi vill att vår Active Restore ska vara.

Vad behöver vi?

  • DDK (Driver Development Kit), nu även känt som WDK 7 (Windows Driver Kit).
  • Virtuell maskin (till exempel Windows 7 x64)
  • Inte nödvändigt, men rubrikfiler som kan laddas ner kan hjälpa här

Vad står i koden?

Låt oss öva lite och till exempel skriva en liten ansökan som:

  1. Visar ett meddelande på skärmen
  2. Tilldelar lite minne
  3. Väntar på tangentbordsinmatning
  4. Frigör använt minne

I inbyggda applikationer är ingångspunkten inte main eller winmain, utan NtProcessStartup-funktionen, eftersom vi faktiskt direkt startar nya processer i systemet.

Låt oss börja med att visa ett meddelande på skärmen. För detta har vi en infödd funktion NtDisplayString, som tar som argument en pekare till ett UNICODE_STRING-strukturobjekt. RtlInitUnicodeString hjälper oss att initiera den. Som ett resultat kan vi skriva denna lilla funktion för att visa text på skärmen:

//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
    UNICODE_STRING string;
    RtlInitUnicodeString(&string, Message);
    NtDisplayString(&string);
}

Eftersom endast funktioner från ntdll är tillgängliga för oss, och det helt enkelt inte finns några andra bibliotek i minnet än, kommer vi definitivt att ha problem med hur man allokerar minne. Den nya operatören existerar inte ännu (eftersom den kommer från C++-världen på alltför hög nivå), och det finns ingen malloc-funktion (den kräver runtime C-bibliotek). Naturligtvis kan du bara använda en stack. Men om vi behöver allokera minne dynamiskt måste vi göra det på högen (dvs. högen). Så låt oss skapa en hög för oss själva och ta minnet från den när vi behöver det.

Funktionen är lämplig för denna uppgift RtlCreateHeap. Därefter, med hjälp av RtlAllocateHeap och RtlFreeHeap, kommer vi att ockupera och frigöra minne när vi behöver det.

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);

Låt oss gå vidare till att vänta på tangentbordsinmatning.

// 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;
	}
}

Allt vi behöver är att använda NtReadFile på en öppen enhet och vänta tills tangentbordet returnerar någon tryckning till oss. Om du trycker på ESC-tangenten fortsätter vi att arbeta. För att öppna enheten måste vi anropa funktionen NtCreateFile (vi måste öppna DeviceKeyboardClass0). Vi kommer också att ringa NtCreateEventför att initiera vänta-objektet. Vi kommer att deklarera KEYBOARD_INPUT_DATA-strukturen själva, som representerar tangentbordsdata. Detta kommer att underlätta vårt arbete.

Den inbyggda applikationen avslutas med ett funktionsanrop NtTerminateProcesseftersom vi helt enkelt dödar vår egen process.

All kod för vår lilla applikation:

#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: Vi kan enkelt använda funktionen DbgBreakPoint() i vår kod för att stoppa den i felsökaren. Det är sant att du måste ansluta WinDbg till en virtuell maskin för kärnfelsökning. Instruktioner om hur du gör detta finns här eller bara använda VirtualKD.

Sammanställning och montering

Det enklaste sättet att bygga en inbyggd applikation är att använda DDK (Driver Development Kit). Vi behöver den gamla sjunde versionen, eftersom senare versioner har ett lite annorlunda tillvägagångssätt och arbetar nära med Visual Studio. Om vi ​​använder DDK behöver vårt projekt bara Makefile och källor.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

källor:

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

Din Makefile kommer att vara exakt densamma, men låt oss titta på källorna lite mer detaljerat. Den här filen anger programmets källor (.c-filer), byggalternativ och andra parametrar.

  • TARGETNAME – namnet på den körbara filen som ska produceras i slutet.
  • TARGETTYPE – typ av körbar fil, det kan vara en drivrutin (.sys), då ska fältvärdet vara DRIVER, om ett bibliotek (.lib) så är värdet LIBRARY. I vårt fall behöver vi en körbar fil (.exe), så vi ställer in värdet på PROGRAM.
  • UMTYPE – möjliga värden för detta fält: konsol för en konsolapplikation, fönster för att arbeta i fönsterläge. Men vi måste ange nt för att få en inbyggd applikation.
  • BUFFER_OVERFLOW_CHECKS – kontrollerar stacken för buffertspill, tyvärr inte vårt fall, vi stänger av det.
  • MINWIN_SDK_LIB_PATH – detta värde hänvisar till variabeln SDK_LIB_PATH, oroa dig inte för att du inte har en sådan systemvariabel deklarerad, när vi kör checked build från DDK kommer denna variabel att deklareras och pekar på de nödvändiga biblioteken.
  • KÄLLOR – en lista med källor för ditt program.
  • INKLUDERAR – header-filer som krävs för montering. Här anger de vanligtvis sökvägen till filerna som följer med DDK, men du kan dessutom ange andra.
  • TARGETLIBS – lista över bibliotek som behöver länkas.
  • USE_NTDLL är ett obligatoriskt fält som måste sättas till 1 av uppenbara skäl.
  • USER_C_FLAGS – alla flaggor som du kan använda i preprocessor-direktiv när du förbereder applikationskod.

Så för att bygga måste vi köra x86 (eller x64) Markerad Build, ändra arbetskatalogen till projektmappen och köra Build-kommandot. Resultatet i skärmdumpen visar att vi har en körbar fil.

Windows Native Applications och Acronis Active Restore-tjänsten

Den här filen kan inte startas så lätt, systemet förbannar och skickar oss att tänka på dess beteende med följande fel:

Windows Native Applications och Acronis Active Restore-tjänsten

Hur startar man en inbyggd applikation?

När autochk startar, bestäms startsekvensen för program av värdet på registernyckeln:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Sessionshanteraren kör program från denna lista ett efter ett. Sessionshanteraren letar efter de körbara filerna i system32-katalogen. Formatet för registernyckelns värde är följande:

autocheck autochk *MyNative

Värdet måste vara i hexadecimalt format, inte det vanliga ASCII-formatet, så nyckeln som visas ovan kommer att ha formatet:

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

För att konvertera titeln kan du använda en onlinetjänst, t.ex. detta.

Windows Native Applications och Acronis Active Restore-tjänsten
Det visar sig att för att starta en inbyggd applikation behöver vi:

  1. Kopiera den körbara filen till mappen system32
  2. Lägg till en nyckel till registret
  3. Starta om maskinen

För enkelhetens skull är här ett färdigt skript för att installera en inbyggd applikation:

install.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
pause

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

Efter installation och omstart, även innan skärmen för användarval visas, får vi följande bild:

Windows Native Applications och Acronis Active Restore-tjänsten

Totalt

Med exemplet med en så liten applikation var vi övertygade om att det är fullt möjligt att köra applikationen på Windows Native-nivå. Därefter kommer killarna från Innopolis University och jag att fortsätta bygga en tjänst som kommer att initiera processen för interaktion med föraren mycket tidigare än i den tidigare versionen av vårt projekt. Och med tillkomsten av win32-skalet skulle det vara logiskt att överföra kontrollen till en fullfjädrad tjänst som redan har utvecklats (mer om detta här).

I nästa artikel kommer vi att beröra en annan komponent i Active Restore-tjänsten, nämligen UEFI-drivrutinen. Prenumerera på vår blogg så att du inte missar nästa inlägg.

Källa: will.com

Lägg en kommentar