Windows Native Applications og Acronis Active Restore tjeneste

I dag fortsætter vi fortællingen om, hvordan vi sammen med gutterne fra Innopolis Universitet udvikler teknologien Active Restore, for at give brugeren mulighed for hurtigst muligt at begynde at arbejde på sin maskine efter et nedbrud. Vi vil tale om nativ Windows-applikationer, herunder særlige aspekter af deres oprettelse og kørsel. Under katten – lidt om vores projekt samt en praktisk vejledning til, hvordan man skriver native applikationer.

Windows Native Applications og Acronis Active Restore tjeneste

I tidligere indlæg har vi allerede fortalt, hvad Active Restoreer, og hvordan studerende fra Innopolis udvikler service. I dag vil jeg fokusere på native applikationer, som vi ønsker, at vores aktive gendannelsestjeneste skal "grave sig ned" til. Hvis alt går godt, så kan vi:

  • Start servicen meget tidligere
  • Forbindes til skyen, hvor backup'en ligger, meget tidligere
  • Forstå i hvilken tilstand systemet befinder sig – normal opstart eller gendannelse – meget tidligere
  • Restaurere langt færre filer på forhånd
  • Tillade brugeren at komme i gang med arbejdet hurtigere.

Hvad er egentlig en native applikation?

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

Windows Native Applications og Acronis Active Restore tjeneste
Pavel Yosifovich – Windows Kernel Programming (2019)

Programmøren bruger funktionen CreateFile, som er deklareret i header-filen fileapi.h og implementeret i Kernel32.dll. Men denne funktion beskæftiger sig ikke med at oprette filen; den kontrollerer kun de indkommende argumenter og kalder funktionen NtCreateFile (præfikset Nt indikerer netop, at funktionen er native). Denne funktion er deklareret i header-filen winternl.h og implementeret i ntdll.dll. Den forbereder et spring ind i kernerummet og foretager derefter et systemopkald for at oprette filen. I dette tilfælde viser det sig, at Kernel32 blot er en wrapper for Ntdll. En af grundene til dette er, at Microsoft har mulighed for at ændre native funktioner, men stadig ikke røre ved standard grænseflader. Microsoft anbefaler ikke at kalde native funktioner direkte og dokumenterer størstedelen af dem ikke. For øvrigt kan man finde ikke-dokumenterede funktioner her.

Den primære fordel ved native applikationer er, at ntdll indlæses i systemet betydeligt tidligere end kernel32. Det giver mening, da kernel32 kræver ntdll for at fungere. Som følge heraf kan applikationer, der bruger native funktioner, starte betydeligt tidligere.

Dermed er Windows Native Applications programmer, der kan startes i den tidlige opstart af Windows. De bruger KUN funktioner fra ntdll. Et eksempel på en sådan applikation: autochk som udfører chkdisk utility til at kontrollere disken for fejl, før de primære tjenester starter. Det er på dette niveau, vi ønsker at se vores Active Restore.

Hvad har vi brug for?

  • DDK (Driver Development Kit), nu også kendt som WDK 7 (Windows Driver Kit).
  • En virtuel maskine (f.eks. Windows 7 x64)
  • Ikke nødvendigt, men include-filer kan være nyttige, som kan downloades. her

Hvad med koden?

Lad os øve os lidt og som eksempel skrive en lille applikation, der:

  1. Viser en besked på skærmen
  2. Allokerer lidt hukommelse
  3. Venter på input fra tastaturet
  4. Frigiver den optagne hukommelse

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

Lad os starte med at vise en besked på skærmen. Til dette har vi den native funktion NtDisplayString, som som argument tager en pointer til et objekt af strukturen UNICODE_STRING. RtlInitUnicodeString kan hjælpe os med at initialisere det. Som resultat kan vi skrive en lille funktion til at vise teksten på skærmen:

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

Da vi kun har adgang til funktioner fra ntdll, og der ikke er andre biblioteker i hukommelsen endnu, vil vi helt sikkert støde på problemer med at allokere hukommelse. Operatoren new eksisterer ikke endnu (fordi den stammer fra en for høj-niveau verden C++), og der er heller ikke nogen malloc funktion (da den kræver C runtime biblioteker). Vi kan selvfølgelig kun bruge stakken. Men hvis vi har brug for dynamisk at allokere hukommelse, må vi gøre det i heapen. Så lad os oprette en heap til os selv og tage hukommelse fra den, når vi har brug for det.

Til denne opgave kan funktionen RtlCreateHeap. Derefter, ved hjælp af RtlAllocateHeap og RtlFreeHeap, vil vi tage og frigive hukommelse, når vi har brug for det.

PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;

// opretter en bunke for at kunne allokere hukommelse senere
memory = RtlCreateHeap(
  HEAP_GROWABLE, 
  NULL, 
  1000, 
  0, NULL, NULL
);

// allokerer en buffer af størrelse bufferSize
buffer = RtlAllocateHeap(
  memory, 
  HEAP_ZERO_MEMORY, 
  bufferSize
);

// frigiver buffer (faktisk ikke nødvendigt, da vi ødelægger bunken i næste trin)
RtlFreeHeap(memory, 0, buffer);

RtlDestroyHeap(memory);

Lad os gå videre til at vente på input fra tastaturet.

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

Alt hvad vi skal bruge – er at bruge NtReadFile på den åbne enhed, og vente på at tastaturet returnerer et hvilket som helst tryk. Hvis ESC-tasten trykkes, fortsætter vi arbejdet. For at åbne enheden skal vi kalde funktionen NtCreateFile (vi skal åbne DeviceKeyboardClass0). Vi vil også kalde NtCreateEvent, for at initialisere en objekt til at vente. Vi vil selv erklære strukturen KEYBOARD_INPUT_DATA, som repræsenterer tastaturdata. Dette vil lette vores arbejde.

Dreneden af den native applikation afsluttes ved at kalde funktionen NtTerminateProcess, fordi vi bare dræber vores egen proces.

Hele 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 koden for at stoppe i debuggeren. Dog skal vi forbinde WinDbg til den virtuelle maskine for kernel debugging. Instruktioner til hvordan man gør dette kan findes her eller bare bruge VirtualKD.

Kompilering og opbygning

Den nemmeste måde at samle en native applikation på er at bruge DDK (Driver Development Kit). Vi har brug for den gamle syvende version, da nyere versioner har en lidt anden tilgang og arbejder tæt sammen med Visual Studio. Hvis vi bruger DDK, har vores projekt kun brug for Makefile og sources.

Makefil

!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

Din Makefile vil være præcis den samme, men lad os stoppe lidt længere ved sources. I denne fil angives kildeteksterne til dit program (filene .c), byggeindstillinger og andre parametre.

  • TARGETNAME – navnet på den eksekverbare fil, der skal fremkomme i sidste ende.
  • TARGETTYPE – typen af den eksekverbare fil, dette kan være en driver (.sys), så skal værdien være DRIVER, hvis en bibliotek (.lib), så skal værdien være 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: console for konsolapplikation, windows til brug i vinduing. Men vi skal angive nt for at få en native applikation.
  • BUFFER_OVERFLOW_CHECKS – kontrol af stakken for buffer overflow, desværre ikke vores tilfælde, vi slukker for det.
  • MINWIN_SDK_LIB_PATH – denne værdi henviser til variablen SDK_LIB_PATH, bekymr dig ikke hvis du ikke har denne systemvariabel erklæret; når vi kører checked build fra DDK, vil denne variabel blive erklæret og pege på de nødvendige biblioteker.
  • SOURCES – listen over kilder til dit program.
  • INCLUDES – headerfiler, der er nødvendige for buildprocessen. Her angiver man typisk stien til de filer, der følger med DDK, men du kan også angive andre filer.
  • TARGETLIBS – liste over biblioteker, der skal linkes.
  • USE_NTDLL – et obligatorisk felt, der skal indstilles til 1. Af åbenlyse grunde.
  • USER_C_FLAGS – alle flags, du kan bruge i præprocessor direktiver under forberedelsen af applikationskoden.

Så for at bygge skal vi starte x86 (eller x64) Checked Build, ændre arbejdsbiblioteket til projektmappen og udføre Build-kommandoen. Resultatet på skærmbilledet viser, at vi har genereret en executabel fil.

Windows Native Applications og Acronis Active Restore tjeneste

Denne fil kan ikke bare startes. Systemet klager og sender os til at overveje dens adfærd med følgende fejl:

Windows Native Applications og Acronis Active Restore tjeneste

Hvordan starter man en native applikation?

Ved opstart af autochk bestemmes rækkefølgen af programmer i opstart via værdien af registreringsnøglen:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Session manager kører programmerne i denne liste sekventielt. Selve eksekverbare filer findes af session manager i system32 biblioteket. Formatet for registreringsnøgleværdien er som følger:

autocheck autochk *MyNative

Værdien skal være i hexadecimal format og ikke i det sædvanlige ASCII, derfor vil nøglen, som præsenteret ovenfor, have 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 navnet kan man bruge en online service, for eksempel, denne.

Windows Native Applications og Acronis Active Restore tjeneste
Så for at starte en native applikation skal vi:

  1. Kopiere den eksekverbare fil til system32 biblioteket
  2. Tilføje en nøgle til registreringsdatabasen
  3. Genstarte maskinen

For nemheds skyld, her er et færdigt script til installation af den native applikation:

install.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
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

Efter installation og genstart, før skærmen for brugervalg vises, vil vi få følgende billede:

Windows Native Applications og Acronis Active Restore tjeneste

Resultat

Vi har gennem det lille eksempelapplikation set, at det er muligt at køre en applikation på Windows Native-niveau. Fremadrettet vil vi sammen med gutterne fra Innopolis Universitet fortsætte med at udvikle en service, der vil igangsætte processen for interaktion med driveren meget tidligere end i den tidligere version af vores projekt. Med introduktionen af win32-grænsefladen vil det være logisk at overdrage kontrol til en fuldt udviklet service, som allerede er lavet (mere om dette senere. her).

I en kommende artikel vil vi berøre endnu en komponent af Active Restore-tjenesten, nemlig UEFI-driveren. Tilmeld dig vores blog for ikke at gå glip af det næste indlæg.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster