Windows Native Applications og Acronis Active Restore-tjenesten

I dag fortsætter vi historien om, hvordan vi sammen med fyrene fra Innopolis University udvikler Active Restore-teknologi, så brugeren kan begynde at arbejde på deres maskine så hurtigt som muligt efter en fejl. Vi vil tale om native Windows-applikationer, herunder funktionerne i deres oprettelse og lancering. Nedenfor klippet er lidt om vores projekt, samt en praktisk guide til hvordan man skriver native ansøgninger.

Windows Native Applications og Acronis Active Restore-tjenesten

I tidligere indlæg har vi allerede talt om, hvad det er Aktiv gendannelse, og hvordan elever fra Innopolis udvikler sig service. I dag vil jeg fokusere på native applikationer, til det niveau, hvor vi ønsker at "begrave" vores aktive gendannelsestjeneste. Hvis alt fungerer, så vil vi være i stand til at:

  • Start selve tjenesten meget tidligere
  • Kontakt skyen, hvor sikkerhedskopien er placeret meget tidligere
  • Meget tidligere for at forstå, hvilken tilstand systemet er i - normal opstart eller gendannelse
  • Meget færre filer at gendanne på forhånd
  • Giv brugeren mulighed for at komme endnu hurtigere i gang.

Hvad er en native app overhovedet?

For at besvare dette spørgsmål, lad os se på rækkefølgen af ​​opkald, som systemet foretager, for eksempel hvis en programmør i hans applikation forsøger at oprette en fil.

Windows Native Applications og Acronis Active Restore-tjenesten
Pavel Yosifovich - Windows Kernel Programmering (2019)

Programmøren bruger funktionen Opret fil, som er erklæret i header-filen fileapi.h og implementeret i Kernel32.dll. Denne funktion opretter dog ikke selve filen, den kontrollerer kun input-argumenterne og kalder funktionen NtCreateFile (præfikset Nt angiver blot, at funktionen er native). Denne funktion er deklareret i overskriftsfilen winternl.h og implementeret i ntdll.dll. Den forbereder sig på at springe ud i det nukleare rum, hvorefter den foretager et systemkald for at oprette en fil. I dette tilfælde viser det sig, at Kernel32 kun er en indpakning for Ntdll. En af grundene til, at det blev gjort, er, at Microsoft dermed har mulighed for at ændre funktionerne i den indfødte verden, men ikke røre ved standardgrænsefladerne. Microsoft anbefaler ikke at kalde indbyggede funktioner direkte og dokumenterer ikke de fleste af dem. Udokumenterede funktioner kan i øvrigt findes her.

Den største fordel ved native applikationer er, at ntdll indlæses i systemet meget tidligere end kernel32. Dette er logisk, fordi kernel32 kræver, at ntdll fungerer. Som et resultat kan applikationer, der bruger indbyggede funktioner, begynde at arbejde meget tidligere.

Windows Native Applications er således programmer, der kan starte tidligt i Windows-opstart. De bruger KUN funktioner fra ntdll. Et eksempel på en sådan applikation: autochk der optræder chkdisk værktøj for at kontrollere disken for fejl, før du starter hovedtjenesterne. Det er præcis det niveau, vi ønsker, at vores Active Restore skal være.

Hvad har vi brug for?

  • DDK (Driver Development Kit), nu også kendt som WDK 7 (Windows Driver Kit).
  • Virtuel maskine (f.eks. Windows 7 x64)
  • Ikke nødvendigt, men header-filer, der kan downloades, kan hjælpe her

Hvad står der i koden?

Lad os øve os lidt og for eksempel skrive en lille ansøgning, der:

  1. Viser en meddelelse på skærmen
  2. Tildeler noget hukommelse
  3. Venter på tastaturinput
  4. Frigør brugt hukommelse

I native applikationer er indgangspunktet ikke main eller winmain, men NtProcessStartup-funktionen, da vi faktisk direkte starter nye processer i systemet.

Lad os starte med at vise en besked på skærmen. Til dette har vi en native funktion NtDisplayString, der tager som argument en pointer til et UNICODE_STRING strukturobjekt. RtlInitUnicodeString hjælper os med at initialisere den. Som et resultat kan vi skrive denne lille funktion for at vise tekst på skærmen:

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

Da kun funktioner fra ntdll er tilgængelige for os, og der simpelthen ikke er andre biblioteker i hukommelsen endnu, vil vi helt sikkert have problemer med, hvordan vi allokerer hukommelse. Den nye operatør eksisterer endnu ikke (fordi den kommer fra C++-verdenen på for højt niveau), og der er ingen malloc-funktion (den kræver runtime C-biblioteker). Du kan selvfølgelig kun bruge en stak. Men hvis vi skal tildele hukommelse dynamisk, bliver vi nødt til at gøre det på heapen (dvs. heap). Så lad os skabe en bunke til os selv og tage hukommelsen fra den, når vi har brug for det.

Funktionen er velegnet til denne opgave RtlCreateHeap. Dernæst, ved at bruge RtlAllocateHeap og RtlFreeHeap, vil vi optage og frigøre hukommelse, når vi har brug for 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);

Lad os gå videre til at vente på tastaturinput.

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

Det eneste, vi behøver, er at bruge NtReadFile på en åben enhed, og vent, indtil tastaturet returnerer et tryk til os. Hvis der trykkes på ESC-tasten, fortsætter vi arbejdet. For at åbne enheden skal vi kalde funktionen NtCreateFile (vi skal åbne DeviceKeyboardClass0). Vi ringer også NtCreateEventfor at initialisere venteobjektet. Vi vil selv erklære KEYBOARD_INPUT_DATA-strukturen, som repræsenterer tastaturdataene. Dette vil gøre vores arbejde lettere.

Den oprindelige applikation slutter med et funktionskald NtTerminateProcessfordi vi simpelthen dræber vores egen proces.

Al koden til vores lille 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 nemt bruge DbgBreakPoint()-funktionen i vores kode til at stoppe den i debuggeren. Sandt nok skal du forbinde WinDbg til en virtuel maskine til kernefejlfinding. Instruktioner til, hvordan du gør dette, kan findes her eller bare bruge VirtualKD.

Kompilering og montage

Den nemmeste måde at bygge en indbygget applikation på er at bruge DDK (Driverudviklingssæt). Vi har brug for den ældgamle syvende version, da senere versioner har en lidt anden tilgang og arbejder tæt sammen med Visual Studio. Hvis vi bruger DDK, så behøver vores projekt kun Makefile og kilder.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

kilder:

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 vil være nøjagtig den samme, men lad os se på kilderne lidt mere detaljeret. Denne fil specificerer dit programs kilder (.c-filer), build-indstillinger og andre parametre.

  • TARGETNAME – navnet på den eksekverbare fil, der skal produceres til sidst.
  • TARGETTYPE – type eksekverbar fil, det kan være en driver (.sys), så skal feltværdien være DRIVER, hvis et bibliotek (.lib), så er værdien LIBRARY. I vores tilfælde har vi brug for en eksekverbar fil (.exe), så vi sætter værdien til PROGRAM.
  • UMTYPE - mulige værdier for dette felt: konsol til en konsolapplikation, vinduer til at arbejde i vinduestilstand. Men vi skal angive nt for at få en indbygget applikation.
  • BUFFER_OVERFLOW_CHECKS – tjekker stakken for bufferoverløb, desværre ikke vores tilfælde, vi slår den fra.
  • MINWIN_SDK_LIB_PATH – denne værdi refererer til SDK_LIB_PATH-variablen, bare rolig at du ikke har en sådan systemvariabel erklæret, når vi kører checket build fra DDK, vil denne variabel blive erklæret og vil pege på de nødvendige biblioteker.
  • KILDER – en liste over kilder til dit program.
  • INKLUDERER – header-filer, der kræves til montering. Her angiver de normalt stien til de filer, der følger med DDK, men du kan desuden angive andre.
  • TARGETLIBS – liste over biblioteker, der skal linkes.
  • USE_NTDLL er et obligatorisk felt, der skal sættes til 1 af indlysende årsager.
  • USER_C_FLAGS – alle flag, som du kan bruge i præprocessor-direktiver, når du forbereder applikationskode.

Så for at bygge skal vi køre x86 (eller x64) Checked Build, ændre arbejdsmappen til projektmappen og køre Build-kommandoen. Resultatet på skærmbilledet viser, at vi har én eksekverbar fil.

Windows Native Applications og Acronis Active Restore-tjenesten

Denne fil kan ikke startes så let, at systemet forbander og sender os til at tænke over dens adfærd med følgende fejl:

Windows Native Applications og Acronis Active Restore-tjenesten

Hvordan starter man en indbygget applikation?

Når autochk starter, bestemmes opstartssekvensen af ​​programmer af værdien af ​​registreringsdatabasenøglen:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Sessionslederen udfører programmer fra denne liste én efter én. Sessionsadministratoren leder selv efter de eksekverbare filer i system32-biblioteket. Registreringsnøgleværdiformatet er som følger:

autocheck autochk *MyNative

Værdien skal være i hexadecimalt format, ikke den sædvanlige ASCII, så nøglen vist ovenfor vil være i 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

For at konvertere titlen kan du bruge en onlinetjeneste, f.eks. dette.

Windows Native Applications og Acronis Active Restore-tjenesten
Det viser sig, at for at starte en indbygget applikation har vi brug for:

  1. Kopier den eksekverbare fil til system32-mappen
  2. Tilføj en nøgle til registreringsdatabasen
  3. Genstart maskinen

For nemheds skyld er her et færdigt script til installation af en indbygget 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 og genstart, selv før brugervalgskærmen vises, får vi følgende billede:

Windows Native Applications og Acronis Active Restore-tjenesten

Total

Ved at bruge eksemplet med sådan en lille applikation var vi overbevist om, at det er ganske muligt at køre applikationen på Windows Native-niveau. Dernæst vil fyrene fra Innopolis University og jeg fortsætte med at opbygge en service, der vil starte processen med interaktion med chaufføren meget tidligere end i den tidligere version af vores projekt. Og med fremkomsten af ​​win32-skallen ville det være logisk at overføre kontrol til en fuldgyldig tjeneste, der allerede er udviklet (mere om dette her).

I den næste artikel vil vi berøre en anden komponent af Active Restore-tjenesten, nemlig UEFI-driveren. Abonner på vores blog, så du ikke går glip af det næste indlæg.

Kilde: www.habr.com

Tilføj en kommentar