Windowsin alkuperäiset sovellukset ja Acronis Active Restore -palvelu

Tänään jatkamme tarinaa siitä, kuinka kehitämme yhdessä Innopolis-yliopiston kavereiden kanssa Active Restore -teknologiaa, jotta käyttäjä voi aloittaa työskentelyn koneensa parissa mahdollisimman pian vian jälkeen. Puhumme alkuperäisistä Windows-sovelluksista, mukaan lukien niiden luomisen ja käynnistämisen ominaisuudet. Leikkauksen alla on vähän projektistamme sekä käytännön opas natiivisovellusten kirjoittamiseen.

Windowsin alkuperäiset sovellukset ja Acronis Active Restore -palvelu

Aiemmissa viesteissä olemme jo puhuneet siitä, mikä se on Aktiivinen palautusja kuinka Innopolin opiskelijat kehittyvät palvelu. Tänään haluan keskittyä alkuperäisiin sovelluksiin, joiden tasolle haluamme "haudata" aktiivisen palautuspalvelumme. Jos kaikki toimii, voimme:

  • Käynnistä itse palvelu paljon aikaisemmin
  • Ota yhteyttä pilveen, jossa varmuuskopio sijaitsee, paljon aikaisemmin
  • Paljon aikaisemmin ymmärtää, missä tilassa järjestelmä on - normaali käynnistys tai palautus
  • Paljon vähemmän tiedostoja palautettavia etukäteen
  • Anna käyttäjän päästä alkuun entistä nopeammin.

Mikä on natiivisovellus?

Vastataksesi tähän kysymykseen katsotaan järjestelmän kutsujen sarjaa, esimerkiksi jos ohjelmoija sovelluksessaan yrittää luoda tiedoston.

Windowsin alkuperäiset sovellukset ja Acronis Active Restore -palvelu
Pavel Yosifovich – Windows-ytimen ohjelmointi (2019)

Ohjelmoija käyttää toimintoa Luo tiedosto, joka on ilmoitettu otsikkotiedostossa fileapi.h ja toteutettu Kernel32.dll:ssä. Tämä funktio ei kuitenkaan itse luo tiedostoa, se vain tarkistaa syöteargumentit ja kutsuu funktiota NtCreateFile (etuliite Nt osoittaa vain, että funktio on natiivi). Tämä toiminto on ilmoitettu winternl.h-otsikkotiedostossa ja toteutettu tiedostossa ntdll.dll. Se valmistautuu hyppäämään ydinavaruuteen, minkä jälkeen se tekee järjestelmäkutsun luodakseen tiedoston. Tässä tapauksessa käy ilmi, että Kernel32 on vain Ntdll:n kääre. Yksi syy tähän on se, että Microsoftilla on siis mahdollisuus muuttaa alkuperäismaailman toimintoja, mutta ei kosketa standardiliittymiä. Microsoft ei suosittele alkuperäisten funktioiden kutsumista suoraan eikä dokumentoi useimpia niistä. Muuten, dokumentoimattomia toimintoja löytyy täällä.

Natiivisovellusten tärkein etu on, että ntdll ladataan järjestelmään paljon aikaisemmin kuin kernel32. Tämä on loogista, koska kernel32 vaatii ntdll:n toimiakseen. Tämän seurauksena alkuperäisiä toimintoja käyttävät sovellukset voivat alkaa toimia paljon aikaisemmin.

Siten Windowsin alkuperäiset sovellukset ovat ohjelmia, jotka voivat käynnistyä varhain Windowsin käynnistyksen yhteydessä. He käyttävät VAIN ntdll:n toimintoja. Esimerkki tällaisesta sovelluksesta: autochk kuka esiintyy chkdisk-apuohjelma tarkistaaksesi levyn virheiden varalta ennen pääpalvelujen käynnistämistä. Tämä on juuri se taso, jonka haluamme aktiivisen palautuksemme olevan.

Mitä tarvitaan?

  • DDK (Driver Development Kit), joka tunnetaan nyt myös nimellä WDK 7 (Windows Driver Kit).
  • Virtuaalikone (esimerkiksi Windows 7 x64)
  • Ei välttämätön, mutta ladattavat otsikkotiedostot voivat auttaa täällä

Mitä koodissa on?

Harjoitellaan vähän ja kirjoitetaan esimerkiksi pieni hakemus, joka:

  1. Näyttää viestin näytöllä
  2. Varaa jonkin verran muistia
  3. Odottaa näppäimistön syöttöä
  4. Vapauttaa käytettyä muistia

Alkuperäisissä sovelluksissa aloituspiste ei ole main tai winmain, vaan NtProcessStartup-toiminto, koska itse asiassa käynnistämme suoraan järjestelmään uusia prosesseja.

Aloitetaan näyttämällä viesti näytöllä. Tätä varten meillä on natiivitoiminto NtDisplayString, joka ottaa argumenttina osoittimen rakenneobjektiin UNICODE_STRING. RtlInitUnicodeString auttaa meitä alustamaan sen. Tämän seurauksena tekstin näyttämiseksi näytöllä voimme kirjoittaa tämän pienen funktion:

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

Koska vain ntdll:n toiminnot ovat käytettävissämme, eikä muistissa yksinkertaisesti ole vielä muita kirjastoja, meillä on varmasti ongelmia muistin varaamisessa. Uutta operaattoria ei ole vielä olemassa (koska se tulee liian korkean tason C++-maailmasta), eikä malloc-funktiota ole (se vaatii ajonaikaisia ​​C-kirjastoja). Tietysti voit käyttää vain pinoa. Mutta jos meidän on varattava muistia dynaamisesti, meidän on tehtävä se kasassa (eli kasassa). Luokaamme siis itsellemme kasa ja otamme siitä muistoa aina kun tarvitsemme sitä.

Toiminto sopii tähän tehtävään RtlCreateHeap. Seuraavaksi käytämme RtlAllocateHeap- ja RtlFreeHeap-muistia ja vapautamme muistia, kun tarvitsemme sitä.

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

Siirrytään odottamaan näppäimistön syötteitä.

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

Tarvitsemme vain käyttää NtReadFile avoimella laitteella ja odota, kunnes näppäimistö palauttaa meille minkä tahansa painalluksen. Jos ESC-näppäintä painetaan, jatkamme työtä. Laitteen avaamiseksi meidän on kutsuttava NtCreateFile-toiminto (meidän on avattava DeviceKeyboardClass0). Soitamme myös NtCreateEventalustaaksesi odotusobjektin. Ilmoitamme itse KEYBOARD_INPUT_DATA-rakenteen, joka edustaa näppäimistön tietoja. Tämä helpottaa työtämme.

Alkuperäinen sovellus päättyy funktiokutsuun NtTerminateProcesskoska me yksinkertaisesti tapamme oman prosessimme.

Kaikki koodi pienelle sovelluksellemme:

#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: Voimme helposti käyttää koodissamme olevaa DbgBreakPoint()-funktiota sen pysäyttämiseen debuggerissa. Totta, sinun on yhdistettävä WinDbg virtuaalikoneeseen ytimen virheenkorjausta varten. Ohjeet tämän tekemiseen löytyvät täällä tai vain käyttää VirtualKD.

Kokoaminen ja kokoonpano

Helpoin tapa rakentaa natiivisovellus on käyttää DDK (Driver Development Kit). Tarvitsemme vanhan seitsemännen version, koska myöhemmissä versioissa on hieman erilainen lähestymistapa ja ne toimivat tiiviisti Visual Studion kanssa. Jos käytämme DDK:ta, projektimme tarvitsee vain Makefile ja lähteet.

makefile

!INCLUDE $(NTMAKEENV)makefile.def

lähteet:

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

Makefilesi on täsmälleen sama, mutta katsotaanpa lähteitä hieman yksityiskohtaisemmin. Tämä tiedosto määrittää ohjelmasi lähteet (.c-tiedostot), koontiasetukset ja muut parametrit.

  • TARGETNAME – suoritettavan tiedoston nimi, joka tulee tuottaa lopussa.
  • TARGETTYPE – suoritettavan tiedoston tyyppi, se voi olla ohjain (.sys), silloin kentän arvon tulee olla DRIVER, jos kirjasto (.lib), niin arvo on LIBRARY. Meidän tapauksessamme tarvitsemme suoritettavan tiedoston (.exe), joten asetamme arvoksi PROGRAM.
  • UMTYPE – tämän kentän mahdolliset arvot: konsoli konsolisovellukselle, ikkunat ikkunatilassa työskentelemiseen. Mutta meidän on määritettävä nt saadaksemme alkuperäisen sovelluksen.
  • BUFFER_OVERFLOW_CHECKS – pinon tarkistaminen puskurin ylivuodon varalta, valitettavasti ei meidän tapauksessamme, sammutamme sen.
  • MINWIN_SDK_LIB_PATH – tämä arvo viittaa SDK_LIB_PATH-muuttujaan, älä huoli, että sinulla ei ole tällaista järjestelmämuuttujaa ilmoitettu. Kun suoritamme tarkistetun koontiversion DDK:sta, tämä muuttuja ilmoitetaan ja osoittaa tarvittaviin kirjastoihin.
  • LÄHTEET – luettelo ohjelmasi lähteistä.
  • SISÄLTÄÄ – kokoamiseen tarvittavat otsikkotiedostot. Täällä ne yleensä osoittavat polun DDK:n mukana tuleviin tiedostoihin, mutta voit lisäksi määrittää muita.
  • TARGETLIBS – luettelo kirjastoista, jotka on linkitettävä.
  • USE_NTDLL on pakollinen kenttä, joka on määritettävä arvoon 1 ilmeisistä syistä.
  • USER_C_FLAGS – kaikki liput, joita voit käyttää esikäsittelyohjeissa sovelluskoodia valmisteltaessa.

Joten rakentaaksemme meidän on suoritettava x86 (tai x64) Checked Build, vaihdettava työhakemisto projektikansioon ja suoritettava Build-komento. Kuvakaappauksen tulos osoittaa, että meillä on yksi suoritettava tiedosto.

Windowsin alkuperäiset sovellukset ja Acronis Active Restore -palvelu

Tätä tiedostoa ei voi käynnistää niin helposti, järjestelmä kiroilee ja lähettää meidät pohtimaan sen toimintaa seuraavalla virheellä:

Windowsin alkuperäiset sovellukset ja Acronis Active Restore -palvelu

Kuinka käynnistää natiivisovellus?

Kun autochk käynnistyy, ohjelmien käynnistysjärjestys määräytyy rekisteriavaimen arvon perusteella:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Istunnonhallinta suorittaa tämän luettelon ohjelmat yksitellen. Istunnonhallinta etsii itse suoritettavat tiedostot system32-hakemistosta. Rekisteriavaimen arvon muoto on seuraava:

autocheck autochk *MyNative

Arvon on oltava heksadesimaalimuodossa, ei tavallisessa ASCII-muodossa, joten yllä näkyvä avain on muodossa:

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

Otsikon muuntamiseen voit käyttää verkkopalvelua, esim. tämä.

Windowsin alkuperäiset sovellukset ja Acronis Active Restore -palvelu
Osoittautuu, että natiivisovelluksen käynnistämiseksi tarvitsemme:

  1. Kopioi suoritettava tiedosto system32-kansioon
  2. Lisää avain rekisteriin
  3. Käynnistä kone uudelleen

Mukavuuden vuoksi tässä on valmis komentosarja alkuperäisen sovelluksen asentamiseen:

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

Asennuksen ja uudelleenkäynnistyksen jälkeen, jopa ennen kuin käyttäjän valintanäyttö tulee näkyviin, saamme seuraavan kuvan:

Windowsin alkuperäiset sovellukset ja Acronis Active Restore -palvelu

Koko

Käyttämällä esimerkkiä tällaisesta pienestä sovelluksesta olimme vakuuttuneita, että sovellus on täysin mahdollista suorittaa Windows Native -tasolla. Seuraavaksi jatkamme Innopolis-yliopiston kaverien kanssa palvelun rakentamista, joka käynnistää vuorovaikutusprosessin kuljettajan kanssa paljon aikaisemmin kuin projektimme edellisessä versiossa. Ja win32-kuoren ilmaantumisen myötä olisi loogista siirtää ohjaus täysimittaiseen palveluun, joka on jo kehitetty (lisää tästä täällä).

Seuraavassa artikkelissa käsittelemme toista Active Restore -palvelun komponenttia, nimittäin UEFI-ohjainta. Tilaa blogimme, jotta et jää paitsi seuraavasta postauksesta.

Lähde: will.com

Lisää kommentti