Windows Native Applications og Acronis Active Restore-tjeneste

I dag fortsetter vi historien om hvordan vi sammen med guttene fra Innopolis Universitet utvikler Active Restore-teknologien for å la brukeren begynne å jobbe på maskinen sin så raskt som mulig etter et crash. Vi skal snakke om nativ applikasjoner for Windows, inkludert spesifikasjoner for deres opprettelse og kjøring. Under vil det være litt om prosjektet vårt, samt en praktisk veiledning for hvordan man skriver native applikasjoner.

Windows Native Applications og Acronis Active Restore-tjeneste

I tidligere innlegg har vi allerede snakket om hva Active Restoreer, og hvordan studentene fra Innopolis utvikler den. I dag vil jeg fokusere på nativ applikasjoner, det nivået vi ønsker å "grave ned" tjenesten for aktiv gjenoppretting. Hvis alt går etter planen, vil vi kunne: tjenesteStarte selve tjenesten mye tidligere

  • Koble til skyen som inneholder backupen mye tidligere
  • Forstå mye tidligere hvilken modus systemet er i – normal oppstart eller gjenoppretting
  • Restaurere mye færre filer på forhånd
  • La brukeren begynne å jobbe enda raskere.
  • Hva er egentlig en nativ applikasjon?

For å svare på dette spørsmålet, la oss se på sekvensen av anrop som systemet gjør, for eksempel hvis programmereren i sin applikasjon prøver å opprette en fil.

Pavel Yosifovich – Windows Kernel Programming (2019)

Windows Native Applications og Acronis Active Restore-tjeneste
Programmereren bruker funksjonen

CreateFile , som er erklært i header-filen fileapi.h og implementert i Kernel32.dll. Men denne funksjonen håndterer ikke filopprettelsen, den sjekker bare argumentene og kaller funksjonenNtCreateFile (prefikset Nt indikerer nettopp at funksjonen er nativ). Denne funksjonen er erklært i header-filen winternl.h og implementert i ntdll.dll. Den forbereder et hopp inn i kjernesystemet, hvoretter den gjør et systemanrop for å opprette filen. I dette tilfellet viser det seg at Kernel32 bare er en omslagsfunksjon for Ntdll. En av grunnene til dette er at Microsoft på denne måten kan endre funksjoner i den native verden, uten å berøre de standard grensesnittene. Microsoft anbefaler ikke å kalle native funksjoner direkte og dokumenterer ikke størstedelen av dem. For øvrig kan udokumenterte funksjoner finnes (prefiksen Nt viser nettopp at funksjonen er native). Denne funksjonen er erklært i header-filen winternl.h og er implementert i ntdll.dll. Den forbereder et sprang inn i kjernesonen, hvoretter den gjør et systemanrop for å opprette en fil. I dette tilfellet viser det seg at Kernel32 bare er et lag rundt Ntdll. En av grunnene til at dette gjøres, er at Microsoft på denne måten kan endre funksjonene i den native verden, men samtidig ikke berøre de standardgrensesnittene. Microsoft anbefaler ikke å kalle på native funksjoner direkte, og dokumenterer ikke det meste av dem. Forresten kan udokumenterte funksjoner finnes here.

Det viktigste med native applikasjoner er at ntdll lastes inn i systemet betydelig før kernel32. Dette gir mening, ettersom kernel32 krever ntdll for å fungere. Som følge av dette kan applikasjoner som bruker native funksjoner starte opp betydelig tidligere.

Dermed er Windows Native Applications programmer som kan starte i de tidlige fasene av Windows-oppstarten. De bruker KUN funksjoner fra ntdll. Et eksempel på en slik applikasjon: autochk som kjører chkdisk utility for å sjekke disken for feil før de viktigste tjenestene starter. På akkurat dette nivået ønsker vi å se vår Active Restore.

Hva trenger vi?

  • DDK (Driver Development Kit), nå også kjent som WDK 7 (Windows Driver Kit).
  • En virtuell maskin (for eksempel Windows 7 x64)
  • Ikke nødvendig, men kan være nyttig med header-filer som kan lastes ned here

Hva er i koden?

La oss trene litt og skrive et lite program som:

  1. Viser en melding på skjermen
  2. Allokerer litt minne
  3. Venter på input fra tastaturet
  4. Frigir allokert minne

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

La oss begynne med å vise en melding på skjermen. For dette har vi en native funksjon NtDisplayString, som tar en peker til et UNICODE_STRING struktur som argument. Vi kan bruke RtlInitUnicodeString for å initialisere den. Som et resultat kan vi skrive en liten funksjon for å vise tekst på skjermen:

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

Siden vi bare har tilgang til funksjoner fra ntdll, og ingen andre biblioteker er lastet inn i minnet, vil vi definitivt støte på problemer med hvordan vi kan allokere minne. New-operatøren eksisterer ikke ennå (fordi den kommer fra en for høy nivå verden av C++), og funksjonen malloc finnes heller ikke (den krever C runtime-biblioteker). Vi kan selvfølgelig bruke bare stakken. Men hvis vi må allokere minne dynamisk, må vi gjøre det i heapen. Derfor la oss opprette en heap for oss selv og ta minne fra den når vi trenger det.

For denne oppgaven vil funksjonen RtlCreateHeapvære passende. Videre, ved å bruke RtlAllocateHeap og RtlFreeHeap, vil vi allokere og frigjøre minne når vi trenger det.

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

// opprett heap for å allokere minne senere
memory = RtlCreateHeap(
  HEAP_GROWABLE, 
  NULL, 
  1000, 
  0, NULL, NULL
);

// allokere buffer med størrelse bufferSize
buffer = RtlAllocateHeap(
  memory, 
  HEAP_ZERO_MEMORY, 
  bufferSize
);

// frigjør buffer (faktisk ikke nødvendig, fordi vi ødelegger heap i neste trinn)
RtlFreeHeap(memory, 0, buffer);

RtlDestroyHeap(memory);

La oss forvente inndata 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 vi trenger er å bruke NtReadFile på den åpne enheten, og vente til tastaturet returnerer et tastetrykk. I tilfelle ESC-tasten trykkes, vil vi fortsette å jobbe. For å åpne enheten, må vi kalle funksjonen NtCreateFile (vi må åpne DeviceKeyboardClass0). Vi vil også kalle NtCreateEvent, for å initialisere et objekt for venting. Vi vil selv erklære strukturen KEYBOARD_INPUT_DATA, som representerer tastaturdata. Dette vil gjøre jobben vår enklere.

Arbeidet til det native programmet avsluttes med å kalle funksjonen NtTerminateProcess, fordi vi bare dreper vår egen prosess.

Hele koden til 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 for å stoppe i debuggeren. Du må imidlertid koble WinDbg til den virtuelle maskinen for kjernedebugging. Instruksjoner om hvordan du gjør dette kan finnes here eller bare bruke VirtualKD.

Kompilering og bygging

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

Ditt Makefile vil være akkurat det samme, når det gjelder sources, la oss stoppe litt opp. I denne filen angir du kildefilene til programmet ditt (filer .c), byggalternativer og andre parametere.

  • TARGETNAME – navnet på den kjørbare filen som skal oppnås til slutt.
  • TARGETTYPE – typen av den kjørbare filen, dette kan være en driver (.sys), da skal verdien være DRIVER, hvis det er et bibliotek (.lib), så skal verdien være LIBRARY. I vårt tilfelle trenger vi en kjørbar fil (.exe), så vi setter verdien til PROGRAM.
  • UMTYPE – mulige verdier for dette feltet: console for konsollapplikasjoner, windows for vindusmodus. Men vi må angi nt for å få en nativer applikasjon.
  • BUFFER_OVERFLOW_CHECKS – sjekk av stakken for bufferoverløp, dessverre ikke vårt tilfelle, vi slår av.
  • MINWIN_SDK_LIB_PATH – denne verdien refererer til variabelen SDK_LIB_PATH, det er ikke nødvendig å bekymre seg hvis denne systemvariabelen ikke er erklært, når vi kjører en checked build fra DDK, vil denne variabelen bli erklært og peke på nødvendige biblioteker.
  • SOURCES – listen over kildekodene til programmet ditt.
  • INCLUDES – headerfiler som er nødvendige for bygging. Vanligvis angis stien til filene som følger med DDK, men du kan også spesifisere tillegg andre.
  • TARGETLIBS – listen over biblioteker som må lenkes.
  • USE_NTDLL – et obligatorisk felt som må settes til 1. Av åpenbare grunner.
  • USER_C_FLAGS – hvilke som helst flagg du kan bruke i preprossesordirektiver når du forbereder applikasjonskoden.

Så for å bygge må vi kjøre x86 (eller x64) Checked Build, endre arbeidskatalogen til mappen med prosjektet og utføre Build-kommandoen. Resultatet på skjermbildet viser at vi har bygget en kjørbar fil.

Windows Native Applications og Acronis Active Restore-tjeneste

Denne filen kan ikke enkelt kjøres, systemet klager og sender oss til å tenke over oppførselen sin med følgende feil:

Windows Native Applications og Acronis Active Restore-tjeneste

Hvordan kjøre en nativer applikasjon?

Ved oppstart bestemmes rekkefølgen for kjøring av programmene av verdien til registreringsnøkkelen:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Sessionsadministratoren kjører programmene fra denne listen i rekkefølge. De kjørbare filene leter sessionsadministratoren etter i system32-katalogen. Formatet til registreringsnøkkelverdien er som følger:

autocheck autochk *MyNative

Verdien må være i heksadesimal format, ikke det vanlige ASCII, derfor vil nøkkelen som angitt ovenfor ha 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

For å konvertere navnet kan man bruke en nettjeneste, for eksempel, materialet..

Windows Native Applications og Acronis Active Restore-tjeneste
Så for å kjøre en nativer applikasjon, må vi:

  1. Kopiere den kjørbare filen til system32-mappen
  2. Legge til en nøkkel i registeret
  3. Starte maskinen på nytt

For enkelhets skyld her er et klart skript for å installere den nativer applikasjonen:

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

Etter installasjonen og omstarten, før valgskjermen for brukere vises, vil vi få følgende bilde:

Windows Native Applications og Acronis Active Restore-tjeneste

Resultat

Ved hjelp av et så lite program har vi vist at det er fullt mulig å kjøre programmet på Windows Native-nivå. Videre vil vi med gutta fra Universitetet Innopolis fortsette å bygge en tjeneste som vil starte interaksjonen med driveren mye tidligere enn i den forrige versjonen av prosjektet vårt. Når win32-grensesnittet blir tilgjengelig, vil det logisk være å overføre kontrollen til en fullverdig tjeneste som allerede er utviklet (mer om dette i neste artikkel). her).

I en kommende artikkel vil vi ta for oss en annen komponent av tjenesten Active Restore, nemlig UEFI-driveren. Abonner på bloggen vår, så du ikke går glipp av neste innlegg.

Kilde: habr.com

Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere 🔥 Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster