Aplikasi Asli Windows dan layanan Pemulihan Aktif Acronis

Hari ini kami melanjutkan cerita tentang bagaimana kami, bersama dengan orang-orang dari Universitas Innopolis, mengembangkan teknologi Pemulihan Aktif yang memungkinkan pengguna untuk mulai mengerjakan mesin mereka sesegera mungkin setelah terjadi kegagalan. Kami akan berbicara tentang aplikasi Windows asli, termasuk fitur pembuatan dan peluncurannya. Di bawah ini adalah sedikit tentang proyek kami, serta panduan praktis tentang cara menulis aplikasi asli.

Aplikasi Asli Windows dan layanan Pemulihan Aktif Acronis

Pada postingan sebelumnya kita sudah membahas tentang apa itu Pemulihan Aktif, dan bagaimana siswa dari Innopolis berkembang layanan. Hari ini saya ingin fokus pada aplikasi asli, pada tingkat di mana kami ingin β€œmengubur” layanan pemulihan aktif kami. Jika semuanya berhasil, maka kita akan dapat:

  • Luncurkan layanan itu sendiri jauh lebih awal
  • Hubungi cloud tempat cadangan berada jauh lebih awal
  • Jauh lebih awal untuk memahami mode apa yang digunakan sistem - boot normal atau pemulihan
  • Jauh lebih sedikit file yang harus dipulihkan terlebih dahulu
  • Izinkan pengguna untuk memulai lebih cepat.

Apa sebenarnya aplikasi asli itu?

Untuk menjawab pertanyaan ini, mari kita lihat urutan panggilan yang dilakukan sistem, misalnya jika seorang programmer dalam aplikasinya mencoba membuat file.

Aplikasi Asli Windows dan layanan Pemulihan Aktif Acronis
Pavel Yosifovich - Pemrograman Kernel Windows (2019)

Pemrogram menggunakan fungsi tersebut Buat File, yang dideklarasikan di file header fileapi.h dan diimplementasikan di Kernel32.dll. Namun, fungsi ini sendiri tidak membuat file, ia hanya memeriksa argumen masukan dan memanggil fungsi tersebut NtCreateFile (awalan Nt hanya menunjukkan bahwa fungsi tersebut asli). Fungsi ini dideklarasikan di file header winternl.h dan diimplementasikan di ntdll.dll. Ia bersiap untuk terjun ke ruang nuklir, setelah itu ia membuat panggilan sistem untuk membuat file. Dalam hal ini, ternyata Kernel32 hanyalah pembungkus Ntdll. Salah satu alasan mengapa hal ini dilakukan adalah karena Microsoft memiliki kemampuan untuk mengubah fungsi dunia asli, tetapi tidak menyentuh antarmuka standar. Microsoft tidak menyarankan untuk memanggil fungsi asli secara langsung dan tidak mendokumentasikan sebagian besar fungsi tersebut. Omong-omong, fungsi yang tidak terdokumentasi dapat ditemukan di sini.

Keuntungan utama aplikasi asli adalah ntdll dimuat ke dalam sistem jauh lebih awal daripada kernel32. Ini logis, karena kernel32 memerlukan ntdll agar dapat berfungsi. Hasilnya, aplikasi yang menggunakan fungsi asli dapat mulai bekerja lebih awal.

Jadi, Aplikasi Asli Windows adalah program yang dapat dimulai pada awal booting Windows. Mereka HANYA menggunakan fungsi dari ntdll. Contoh aplikasinya: autochk siapa yang melakukan utilitas chkdisk untuk memeriksa kesalahan pada disk sebelum memulai layanan utama. Ini adalah tingkat yang kita inginkan dari Pemulihan Aktif kita.

Apa yang kita butuhkan?

  • DDK (Driver Development Kit), sekarang juga dikenal sebagai WDK 7 (Windows Driver Kit).
  • Mesin virtual (misalnya, Windows 7 x64)
  • Tidak perlu, tapi file header yang dapat diunduh mungkin membantu di sini

Apa isi kodenya?

Mari berlatih sedikit dan, misalnya, menulis aplikasi kecil yang:

  1. Menampilkan pesan di layar
  2. Mengalokasikan sebagian memori
  3. Menunggu masukan keyboard
  4. Membebaskan memori yang digunakan

Dalam aplikasi asli, titik masuknya bukanlah main atau winmain, tetapi fungsi NtProcessStartup, karena kami sebenarnya meluncurkan proses baru secara langsung dalam sistem.

Mari kita mulai dengan menampilkan pesan di layar. Untuk ini kami memiliki fungsi asli NtDisplayString, yang mengambil argumen sebagai penunjuk ke objek struktur UNICODE_STRING. RtlInitUnicodeString akan membantu kita menginisialisasinya. Hasilnya, untuk menampilkan teks di layar kita dapat menulis fungsi kecil ini:

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

Karena hanya fungsi dari ntdll yang tersedia bagi kami, dan belum ada perpustakaan lain di memori, kami pasti akan mengalami masalah dalam mengalokasikan memori. Operator baru belum ada (karena berasal dari dunia C++ tingkat tinggi), dan tidak ada fungsi malloc (membutuhkan pustaka C runtime). Tentu saja, Anda hanya dapat menggunakan tumpukan. Namun jika kita perlu mengalokasikan memori secara dinamis, kita harus melakukannya di heap (yaitu heap). Jadi mari kita buat heap untuk diri kita sendiri dan ambil memori darinya kapan pun kita membutuhkannya.

Fungsi ini cocok untuk tugas ini RtlCreateHeap. Selanjutnya, dengan menggunakan RtlAllocationHeap dan RtlFreeHeap, kita akan menempati dan mengosongkan memori saat kita membutuhkannya.

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

Mari beralih menunggu input keyboard.

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

Yang kita butuhkan hanyalah menggunakan File TidakBaca pada perangkat terbuka, dan tunggu hingga keyboard mengembalikan tekanan apa pun kepada kami. Jika tombol ESC ditekan, kami akan melanjutkan pekerjaan. Untuk membuka perangkat, kita perlu memanggil fungsi NtCreateFile (kita perlu membuka DeviceKeyboardClass0). Kami juga akan menelepon NtCreateEventuntuk menginisialisasi objek tunggu. Kami akan mendeklarasikan sendiri struktur KEYBOARD_INPUT_DATA, yang mewakili data keyboard. Ini akan membuat pekerjaan kita lebih mudah.

Aplikasi asli diakhiri dengan pemanggilan fungsi Proses NtTerminatekarena kita hanya mematikan proses kita sendiri.

Semua kode untuk aplikasi kecil kita:

#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: Kita dapat dengan mudah menggunakan fungsi DbgBreakPoint() dalam kode kita untuk menghentikannya di debugger. Benar, Anda perlu menghubungkan WinDbg ke mesin virtual untuk debugging kernel. Petunjuk tentang cara melakukan ini dapat ditemukan di sini atau gunakan saja VirtualKD.

Kompilasi dan perakitan

Cara termudah untuk membangun aplikasi asli adalah dengan menggunakan DDK (Kit Pengembangan Pengemudi). Kami memerlukan versi ketujuh yang kuno, karena versi yang lebih baru memiliki pendekatan yang sedikit berbeda dan bekerja sama dengan Visual Studio. Jika kita menggunakan DDK, maka proyek kita hanya membutuhkan Makefile dan source.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

sumber:

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

Makefile Anda akan sama persis, tapi mari kita lihat sumbernya lebih detail. File ini menentukan sumber program Anda (file .c), opsi build, dan parameter lainnya.

  • TARGETNAME – nama file yang dapat dieksekusi yang pada akhirnya harus diproduksi.
  • TARGETTYPE – jenis file yang dapat dieksekusi, bisa berupa driver (.sys), maka nilai fieldnya harus DRIVER, jika perpustakaan (.lib), maka nilainya adalah LIBRARY. Dalam kasus kami, kami memerlukan file yang dapat dieksekusi (.exe), jadi kami menetapkan nilainya ke PROGRAM.
  • UMTYPE – kemungkinan nilai untuk bidang ini: konsol untuk aplikasi konsol, jendela untuk bekerja dalam mode berjendela. Tapi kita perlu menentukan nt untuk mendapatkan aplikasi asli.
  • BUFFER_OVERFLOW_CHECKS – memeriksa tumpukan untuk buffer overflow, sayangnya bukan kasus kami, kami mematikannya.
  • MINWIN_SDK_LIB_PATH – nilai ini mengacu pada variabel SDK_LIB_PATH, jangan khawatir Anda tidak mendeklarasikan variabel sistem seperti itu, ketika kita menjalankan build yang diperiksa dari DDK, variabel ini akan dideklarasikan dan akan menunjuk ke perpustakaan yang diperlukan.
  • SUMBER – daftar sumber untuk program Anda.
  • TERMASUK – file header yang diperlukan untuk perakitan. Di sini mereka biasanya menunjukkan jalur ke file yang disertakan dengan DDK, tetapi Anda juga dapat menentukan yang lain.
  • TARGETLIBS – daftar perpustakaan yang perlu dihubungkan.
  • USE_NTDLL adalah bidang wajib yang harus disetel ke 1 karena alasan yang jelas.
  • USER_C_FLAGS – tanda apa pun yang dapat Anda gunakan dalam arahan praprosesor saat menyiapkan kode aplikasi.

Jadi untuk membangun, kita perlu menjalankan x86 (atau x64) Checked Build, ubah direktori kerja ke folder proyek dan jalankan perintah Build. Hasil tangkapan layar menunjukkan bahwa kita memiliki satu file yang dapat dieksekusi.

Aplikasi Asli Windows dan layanan Pemulihan Aktif Acronis

File ini tidak dapat diluncurkan dengan mudah, sistem mengutuk dan membuat kita memikirkan perilakunya dengan kesalahan berikut:

Aplikasi Asli Windows dan layanan Pemulihan Aktif Acronis

Bagaimana cara meluncurkan aplikasi asli?

Saat autochk dimulai, urutan startup program ditentukan oleh nilai kunci registri:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Manajer sesi menjalankan program dari daftar ini satu per satu. Manajer sesi mencari sendiri file yang dapat dieksekusi di direktori system32. Format nilai kunci registri adalah sebagai berikut:

autocheck autochk *MyNative

Nilainya harus dalam format heksadesimal, bukan ASCII biasa, sehingga kunci yang ditampilkan di atas akan berformat:

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

Untuk mengkonversi judul, Anda dapat menggunakan layanan online, misalnya, ini.

Aplikasi Asli Windows dan layanan Pemulihan Aktif Acronis
Ternyata untuk meluncurkan aplikasi asli, kita memerlukan:

  1. Salin file yang dapat dieksekusi ke folder system32
  2. Tambahkan kunci ke registri
  3. Nyalakan ulang mesin

Untuk kenyamanan, berikut skrip siap pakai untuk menginstal aplikasi asli:

instal.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
pause

tambahkan.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

Setelah instalasi dan reboot, bahkan sebelum layar pemilihan pengguna muncul, kita akan mendapatkan gambar berikut:

Aplikasi Asli Windows dan layanan Pemulihan Aktif Acronis

Total

Dengan menggunakan contoh aplikasi sekecil itu, kami yakin bahwa sangat mungkin untuk menjalankan aplikasi pada level Windows Native. Selanjutnya, saya dan orang-orang dari Universitas Innopolis akan terus membangun layanan yang akan memulai proses interaksi dengan pengemudi jauh lebih awal daripada versi proyek kami sebelumnya. Dan dengan munculnya shell win32, akan logis untuk mentransfer kontrol ke layanan lengkap yang telah dikembangkan (lebih lanjut tentang ini di sini).

Pada artikel selanjutnya kita akan membahas komponen lain dari layanan Active Restore, yaitu driver UEFI. Berlangganan ke blog kami agar Anda tidak ketinggalan postingan berikutnya.

Sumber: www.habr.com

Tambah komentar