ProHoster > Blogi > antaminen > Windowsin alkuperäiset sovellukset ja Acronis Active Restore -palvelu
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.
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.
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:
Näyttää viestin näytöllä
Varaa jonkin verran muistia
Odottaa näppäimistön syöttöä
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);
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.
Tätä tiedostoa ei voi käynnistää niin helposti, järjestelmä kiroilee ja lähettää meidät pohtimaan sen toimintaa seuraavalla virheellä:
Kuinka käynnistää natiivisovellus?
Kun autochk käynnistyy, ohjelmien käynnistysjärjestys määräytyy rekisteriavaimen arvon perusteella:
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:
Asennuksen ja uudelleenkäynnistyksen jälkeen, jopa ennen kuin käyttäjän valintanäyttö tulee näkyviin, saamme seuraavan kuvan:
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.