Windows Native-applikasjoner og Acronis Active Restore-tjenesten

I dag fortsetter vi historien om hvordan vi, sammen med gutta fra Innopolis University, utvikler Active Restore-teknologi for å la brukeren begynne å jobbe på maskinen sin så snart som mulig etter en feil. Vi vil snakke om innfødte Windows-applikasjoner, inkludert funksjonene for opprettelse og lansering. Under kuttet er litt om prosjektet vårt, samt en praktisk guide til hvordan du skriver innfødte søknader.

Windows Native-applikasjoner og Acronis Active Restore-tjenesten

I tidligere innlegg har vi allerede snakket om hva det er Aktiv gjenoppretting, og hvordan studenter fra Innopolis utvikler seg tjeneste. I dag ønsker jeg å fokusere på innfødte applikasjoner, til det nivået vi ønsker å "begrave" vår aktive gjenopprettingstjeneste. Hvis alt ordner seg, vil vi kunne:

  • Start selve tjenesten mye tidligere
  • Kontakt skyen der sikkerhetskopien er plassert mye tidligere
  • Mye tidligere for å forstå hvilken modus systemet er i - normal oppstart eller gjenoppretting
  • Mye færre filer å gjenopprette på forhånd
  • La brukeren komme i gang enda raskere.

Hva er egentlig en innebygd app?

For å svare på dette spørsmålet, la oss se på sekvensen av samtaler som systemet foretar, for eksempel hvis en programmerer i applikasjonen hans prøver å lage en fil.

Windows Native-applikasjoner og Acronis Active Restore-tjenesten
Pavel Yosifovich - Windows-kjerneprogrammering (2019)

Programmereren bruker funksjonen Create, som er deklarert i overskriftsfilen fileapi.h og implementert i Kernel32.dll. Denne funksjonen i seg selv oppretter imidlertid ikke filen, den sjekker bare input-argumentene og kaller opp funksjonen NtCreateFile (prefikset Nt indikerer bare at funksjonen er opprinnelig). Denne funksjonen er deklarert i overskriftsfilen winternl.h og implementert i ntdll.dll. Den forbereder seg på å hoppe inn i kjernefysisk rom, hvoretter den foretar et systemanrop for å lage en fil. I dette tilfellet viser det seg at Kernel32 bare er en innpakning for Ntdll. En av grunnene til at dette ble gjort er at Microsoft dermed har muligheten til å endre funksjonene til den innfødte verden, men ikke berøre standardgrensesnittene. Microsoft anbefaler ikke å kalle opp opprinnelige funksjoner direkte og dokumenterer ikke de fleste av dem. Forresten, udokumenterte funksjoner kan finnes her.

Den største fordelen med native applikasjoner er at ntdll lastes inn i systemet mye tidligere enn kernel32. Dette er logisk, fordi kernel32 krever at ntdll fungerer. Som et resultat kan applikasjoner som bruker innebygde funksjoner begynne å fungere mye tidligere.

Dermed er Windows Native-applikasjoner programmer som kan starte tidlig i Windows-oppstart. De bruker KUN funksjoner fra ntdll. Et eksempel på en slik applikasjon: autochk som opptrer chkdisk-verktøyet for å sjekke disken for feil før du starter hovedtjenestene. Dette er akkurat det nivået vi vil at vår Active Restore skal være.

Hva trenger vi?

  • DDK (Driver Development Kit), nå også kjent som WDK 7 (Windows Driver Kit).
  • Virtuell maskin (for eksempel Windows 7 x64)
  • Ikke nødvendig, men overskriftsfiler som kan lastes ned kan hjelpe her

Hva står i koden?

La oss øve litt og for eksempel skrive en liten søknad som:

  1. Viser en melding på skjermen
  2. Tildeler noe minne
  3. Venter på tastaturinndata
  4. Frigjør brukt minne

I native applikasjoner er ikke inngangspunktet hoved- eller winmain, men NtProcessStartup-funksjonen, siden vi faktisk starter nye prosesser i systemet direkte.

La oss starte med å vise en melding på skjermen. For dette har vi en innfødt funksjon NtDisplayString, som tar som argument en peker til et UNICODE_STRING-strukturobjekt. RtlInitUnicodeString vil hjelpe oss å initialisere den. Som et resultat, for å vise tekst på skjermen, kan vi skrive denne lille funksjonen:

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

Siden bare funksjoner fra ntdll er tilgjengelige for oss, og det rett og slett ikke er andre biblioteker i minnet ennå, vil vi definitivt ha problemer med hvordan vi skal allokere minne. Den nye operatøren eksisterer ikke ennå (fordi den kommer fra den for høye verdenen av C++), og det er ingen malloc-funksjon (den krever runtime C-biblioteker). Selvfølgelig kan du bare bruke en stabel. Men hvis vi trenger å tildele minne dynamisk, må vi gjøre det på haugen (dvs. haugen). Så la oss lage en haug for oss selv og ta minnet fra den når vi trenger det.

Funksjonen er egnet for denne oppgaven RtlCreateHeap. Deretter, ved å bruke RtlAllocateHeap og RtlFreeHeap, vil vi okkupere og frigjøre minne når vi trenger 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);

La oss gå videre til å vente på tastaturinndata.

// 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 vi trenger er å bruke NtReadFile på en åpen enhet, og vent til tastaturet returnerer et trykk til oss. Hvis ESC-tasten trykkes, fortsetter vi arbeidet. For å åpne enheten må vi kalle opp NtCreateFile-funksjonen (vi må åpne DeviceKeyboardClass0). Vi ringer også NtCreateEventfor å initialisere venteobjektet. Vi vil erklære KEYBOARD_INPUT_DATA-strukturen selv, som representerer tastaturdataene. Dette vil gjøre arbeidet vårt enklere.

Den opprinnelige applikasjonen avsluttes med et funksjonskall NtTerminateProcessfordi vi rett og slett dreper vår egen prosess.

All koden for vår lille applikasjon:

#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 bruke funksjonen DbgBreakPoint() i koden vår for å stoppe den i debuggeren. Riktignok må du koble WinDbg til en virtuell maskin for kjernefeilsøking. Instruksjoner for hvordan du gjør dette finner du her eller bare bruke VirtualKD.

Sammenstilling og montering

Den enkleste måten å bygge en innebygd applikasjon på er å bruke DDK (Driver Development Kit). Vi trenger den gamle syvende versjonen, siden senere versjoner har en litt annen tilnærming og jobber tett med Visual Studio. Hvis vi bruker DDK, trenger prosjektet vårt bare 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

Makefilen din vil være nøyaktig den samme, men la oss se på kildene litt mer detaljert. Denne filen spesifiserer programmets kilder (.c-filer), byggealternativer og andre parametere.

  • TARGETNAME – navnet på den kjørbare filen som skal produseres til slutt.
  • TARGETTYPE – type kjørbar fil, det kan være en driver (.sys), så skal feltverdien være DRIVER, hvis et bibliotek (.lib), så er verdien LIBRARY. I vårt tilfelle trenger vi en kjørbar fil (.exe), så vi setter verdien til PROGRAM.
  • UMTYPE - mulige verdier for dette feltet: konsoll for en konsollapplikasjon, vinduer for å jobbe i vindusmodus. Men vi må spesifisere nt for å få en innfødt applikasjon.
  • BUFFER_OVERFLOW_CHECKS – sjekker stabelen for bufferoverløp, dessverre ikke vårt tilfelle, vi slår den av.
  • MINWIN_SDK_LIB_PATH – denne verdien refererer til SDK_LIB_PATH-variabelen, ikke bekymre deg for at du ikke har en slik systemvariabel deklarert, når vi kjører sjekket build fra DDK, vil denne variabelen bli deklarert og vil peke til de nødvendige bibliotekene.
  • KILDER – en liste over kilder for programmet ditt.
  • INKLUDERER – overskriftsfiler som kreves for montering. Her angir de vanligvis banen til filene som følger med DDK, men du kan i tillegg spesifisere andre.
  • TARGETLIBS – liste over biblioteker som må kobles sammen.
  • USE_NTDLL er et obligatorisk felt som må settes til 1 av åpenbare grunner.
  • USER_C_FLAGS – alle flagg som du kan bruke i preprocessor-direktiver når du forbereder applikasjonskode.

Så for å bygge, må vi kjøre x86 (eller x64) Sjekket Build, endre arbeidskatalogen til prosjektmappen og kjøre Build-kommandoen. Resultatet i skjermbildet viser at vi har én kjørbar fil.

Windows Native-applikasjoner og Acronis Active Restore-tjenesten

Denne filen kan ikke startes så lett, systemet forbanner og sender oss til å tenke på oppførselen med følgende feil:

Windows Native-applikasjoner og Acronis Active Restore-tjenesten

Hvordan starte en innebygd applikasjon?

Når autochk starter, bestemmes oppstartssekvensen for programmer av verdien til registernøkkelen:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Sesjonslederen kjører programmer fra denne listen én etter én. Sesjonsbehandleren ser etter de kjørbare filene selv i system32-katalogen. Registernøkkelverdiformatet er som følger:

autocheck autochk *MyNative

Verdien må være i heksadesimalt format, ikke den vanlige ASCII, så nøkkelen 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 å konvertere tittelen kan du bruke en nettbasert tjeneste, for eksempel dette.

Windows Native-applikasjoner og Acronis Active Restore-tjenesten
Det viser seg at for å starte en innebygd applikasjon trenger vi:

  1. Kopier den kjørbare filen til system32-mappen
  2. Legg til en nøkkel til registeret
  3. Start maskinen på nytt

For enkelhets skyld er her et ferdig skript for å installere en innebygd applikasjon:

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

Etter installasjon og omstart, selv før brukervalgskjermen vises, får vi følgende bilde:

Windows Native-applikasjoner og Acronis Active Restore-tjenesten

Total

Ved å bruke eksemplet med en så liten applikasjon, var vi overbevist om at det er fullt mulig å kjøre applikasjonen på Windows Native-nivå. Deretter vil gutta fra Innopolis University og jeg fortsette å bygge en tjeneste som vil starte prosessen med samhandling med sjåføren mye tidligere enn i forrige versjon av prosjektet vårt. Og med bruken av win32-skallet, ville det være logisk å overføre kontrollen til en fullverdig tjeneste som allerede er utviklet (mer om dette her).

I den neste artikkelen vil vi berøre en annen komponent av Active Restore-tjenesten, nemlig UEFI-driveren. Abonner på bloggen vår slik at du ikke går glipp av neste innlegg.

Kilde: www.habr.com

Legg til en kommentar