Windows Native Applications و Acronis Active Restore سرویس

امروز ما این داستان را ادامه می دهیم که چگونه ما، همراه با بچه های دانشگاه Innopolis، در حال توسعه فناوری Active Restore هستیم تا به کاربر این امکان را بدهیم که پس از شکست در اسرع وقت کار روی دستگاه خود را شروع کند. ما در مورد برنامه های بومی ویندوز، از جمله ویژگی های ایجاد و راه اندازی آنها صحبت خواهیم کرد. در زیر برش کمی در مورد پروژه ما، و همچنین یک راهنمای عملی در مورد نحوه نوشتن برنامه های کاربردی بومی است.

Windows Native Applications و Acronis Active Restore سرویس

در پست های قبلی قبلاً در مورد چیستی آن صحبت کرده ایم بازیابی فعال، و چگونگی رشد دانش آموزان از Innopolis خدمات. امروز می‌خواهم روی برنامه‌های بومی تمرکز کنم، که می‌خواهیم سرویس بازیابی فعال خود را تا حدی «دفن» کنیم. اگر همه چیز درست شود، ما قادر خواهیم بود:

  • خود سرویس را خیلی زودتر راه اندازی کنید
  • خیلی زودتر با ابری که پشتیبان در آن قرار دارد تماس بگیرید
  • خیلی زودتر برای درک اینکه سیستم در چه حالتی است - بوت معمولی یا بازیابی
  • فایل های بسیار کمتری برای بازیابی از قبل
  • به کاربر اجازه دهید حتی سریعتر شروع به کار کند.

به هر حال یک برنامه بومی چیست؟

برای پاسخ به این سوال، بیایید به دنباله تماس هایی که سیستم انجام می دهد نگاهی بیندازیم، به عنوان مثال، اگر یک برنامه نویس در برنامه خود سعی در ایجاد یک فایل داشته باشد.

Windows Native Applications و Acronis Active Restore سرویس
Pavel Yosifovich - Windows Kernel Programming (2019)

برنامه نویس از تابع استفاده می کند ایجاد فایلکه در فایل هدر fileapi.h اعلام شده و در Kernel32.dll پیاده سازی شده است. با این حال، این تابع به خودی خود فایلی را ایجاد نمی کند، فقط آرگومان های ورودی را بررسی می کند و تابع را فراخوانی می کند NtCreateFile (پیشوند Nt فقط نشان می دهد که تابع بومی است). این تابع در فایل هدر winternl.h اعلام شده و در ntdll.dll پیاده سازی شده است. برای پرش به فضای هسته ای آماده می شود و پس از آن یک فراخوانی سیستمی برای ایجاد یک فایل برقرار می کند. در این مورد، معلوم می شود که Kernel32 فقط یک پوشش برای Ntdll است. یکی از دلایل انجام این کار این است که مایکروسافت بنابراین توانایی تغییر عملکردهای دنیای بومی را دارد، اما رابط های استاندارد را لمس نمی کند. مایکروسافت فراخوانی مستقیم توابع بومی را توصیه نمی کند و اکثر آنها را مستند نمی کند. به هر حال، توابع غیر مستند را می توان یافت اینجا.

مزیت اصلی برنامه های بومی این است که ntdll خیلی زودتر از kernel32 در سیستم بارگذاری می شود. این منطقی است، زیرا kernel32 برای کار کردن به ntdll نیاز دارد. در نتیجه، برنامه هایی که از توابع بومی استفاده می کنند می توانند خیلی زودتر شروع به کار کنند.

بنابراین، Windows Native Applications برنامه هایی هستند که می توانند در اوایل بوت ویندوز شروع شوند. آنها فقط از توابع ntdll استفاده می کنند. نمونه ای از چنین برنامه ای: autochk که اجرا می کند ابزار chkdisk برای بررسی دیسک برای وجود خطا قبل از شروع سرویس های اصلی. این دقیقا همان سطحی است که ما می خواهیم Active Restore ما باشد.

چه چیزی نیاز داریم؟

  • DDK (کیت توسعه درایور)، که اکنون با نام WDK 7 (کیت درایور ویندوز) نیز شناخته می شود.
  • ماشین مجازی (به عنوان مثال، ویندوز 7 x64)
  • لازم نیست، اما فایل های هدری که می توان دانلود کرد ممکن است کمک کند اینجا

در کد چیست؟

بیایید کمی تمرین کنیم و مثلاً یک برنامه کوچک بنویسیم که:

  1. پیامی را روی صفحه نمایش می دهد
  2. مقداری حافظه را اختصاص می دهد
  3. منتظر ورودی صفحه کلید است
  4. حافظه مصرف شده را آزاد می کند

در برنامه های بومی، نقطه ورودی اصلی یا winmain نیست، بلکه تابع NtProcessStartup است، زیرا ما در واقع به طور مستقیم فرآیندهای جدید را در سیستم راه اندازی می کنیم.

بیایید با نمایش یک پیام روی صفحه شروع کنیم. برای این ما یک تابع بومی داریم NtDisplayString، که به عنوان آرگومان یک اشاره گر به یک شی ساختار UNICODE_STRING می گیرد. RtlInitUnicodeString به ما کمک می کند تا آن را مقداردهی کنیم. در نتیجه، برای نمایش متن روی صفحه، می توانیم این تابع کوچک را بنویسیم:

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

از آنجایی که فقط توابع ntdll در دسترس ما هستند و به سادگی هنوز هیچ کتابخانه دیگری در حافظه وجود ندارد، قطعاً در نحوه تخصیص حافظه با مشکل مواجه خواهیم شد. اپراتور جدید هنوز وجود ندارد (زیرا از دنیای بسیار سطح بالای C++ می آید)، و هیچ تابع malloc وجود ندارد (به کتابخانه های C زمان اجرا نیاز دارد). البته، شما فقط می توانید از یک پشته استفاده کنید. اما اگر نیاز به تخصیص پویا حافظه داشته باشیم، باید این کار را روی پشته (یعنی heap) انجام دهیم. پس بیایید یک پشته برای خود بسازیم و هر زمان که به آن نیاز داشتیم از آن خاطره برداریم.

عملکرد برای این کار مناسب است RtlCreateHeap. در مرحله بعد، با استفاده از RtlAllocateHeap و RtlFreeHeap، زمانی که به آن نیاز داشته باشیم، حافظه را اشغال کرده و آزاد می کنیم.

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

بیایید به انتظار برای ورودی صفحه کلید برویم.

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

تنها چیزی که نیاز داریم استفاده از آن است NtReadFile در یک دستگاه باز، و صبر کنید تا صفحه کلید فشار را به ما بازگرداند. اگر کلید ESC فشار داده شود، کار را ادامه می دهیم. برای باز کردن دستگاه، باید تابع NtCreateFile را فراخوانی کنیم (باید DeviceKeyboardClass0 را باز کنیم). ما نیز تماس خواهیم گرفت NtCreateEventبرای مقداردهی اولیه شی انتظار. ساختار KEYBOARD_INPUT_DATA را خودمان اعلام می کنیم که نشان دهنده داده های صفحه کلید است. این کار ما را راحت تر می کند.

برنامه بومی با یک فراخوانی تابع به پایان می رسد NtTerminateProcessزیرا ما به سادگی روند خودمان را می کشیم.

همه کدهای برنامه کوچک ما:

#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: ما به راحتی می توانیم از تابع DbgBreakPoint() در کد خود برای توقف آن در دیباگر استفاده کنیم. درست است، برای اشکال زدایی هسته باید WinDbg را به یک ماشین مجازی متصل کنید. دستورالعمل نحوه انجام این کار را می توان یافت اینجا یا فقط استفاده کنید مجازی KD.

تدوین و مونتاژ

ساده ترین راه برای ساختن یک اپلیکیشن بومی استفاده از آن است DDK (کیت توسعه درایور). ما به نسخه هفتم باستانی نیاز داریم، زیرا نسخه های بعدی رویکرد کمی متفاوت دارند و با ویژوال استودیو همکاری نزدیک دارند. اگر از DDK استفاده کنیم، پروژه ما فقط به Makefile و منابع نیاز دارد.

makefile

!INCLUDE $(NTMAKEENV)makefile.def

منابع:

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 شما دقیقاً یکسان خواهد بود، اما بیایید منابع را با جزئیات بیشتری بررسی کنیم. این فایل منابع برنامه شما (فایل های c)، گزینه های ساخت و سایر پارامترها را مشخص می کند.

  • TARGETNAME – نام فایل اجرایی که باید در پایان تولید شود.
  • TARGETTYPE - نوع فایل اجرایی، می تواند یک درایور (.sys) باشد، سپس مقدار فیلد باید DRIVER باشد، اگر کتابخانه (.lib) باشد، مقدار آن LIBRARY است. در مورد ما، ما به یک فایل اجرایی (exe.) نیاز داریم، بنابراین مقدار را روی PROGRAM قرار می دهیم.
  • UMTYPE - مقادیر ممکن برای این فیلد: کنسول برای یک برنامه کنسول، ویندوز برای کار در حالت پنجره. اما برای دریافت یک برنامه بومی باید nt را مشخص کنیم.
  • BUFFER_OVERFLOW_CHECKS - بررسی پشته برای سرریز بافر، متأسفانه مورد ما نیست، ما آن را خاموش می کنیم.
  • MINWIN_SDK_LIB_PATH – این مقدار به متغیر SDK_LIB_PATH اشاره دارد، نگران نباشید که چنین متغیر سیستمی را اعلان نکرده‌اید، زمانی که build بررسی شده را از DDK اجرا می‌کنیم، این متغیر اعلام می‌شود و به کتابخانه‌های لازم اشاره می‌کند.
  • منابع - فهرستی از منابع برای برنامه شما.
  • شامل – فایل های هدر است که برای مونتاژ مورد نیاز است. در اینجا آنها معمولاً مسیر فایل‌های همراه DDK را نشان می‌دهند، اما می‌توانید سایر موارد را نیز مشخص کنید.
  • TARGETLIBS - فهرست کتابخانه هایی که باید پیوند داده شوند.
  • USE_NTDLL یک فیلد الزامی است که به دلایل واضح باید روی 1 تنظیم شود.
  • USER_C_FLAGS - هر پرچمی که می توانید در دستورالعمل های پیش پردازنده هنگام تهیه کد برنامه استفاده کنید.

بنابراین برای ساخت، باید x86 (یا x64) Checked Build را اجرا کنیم، دایرکتوری کاری را به پوشه پروژه تغییر دهیم و دستور Build را اجرا کنیم. نتیجه در اسکرین شات نشان می دهد که ما یک فایل اجرایی داریم.

Windows Native Applications و Acronis Active Restore سرویس

این فایل به این راحتی راه اندازی نمی شود، سیستم فحش می دهد و با خطای زیر ما را به فکر رفتار آن می فرستد:

Windows Native Applications و Acronis Active Restore سرویس

چگونه یک برنامه بومی راه اندازی کنیم؟

هنگامی که autochk شروع می شود، دنباله راه اندازی برنامه ها با مقدار کلید رجیستری تعیین می شود:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Session manager برنامه های این لیست را یکی یکی اجرا می کند. مدیر جلسه به دنبال فایل های اجرایی خود در فهرست system32 می گردد. فرمت مقدار کلید رجیستری به شرح زیر است:

autocheck autochk *MyNative

مقدار باید در قالب هگزادسیمال باشد، نه ASCII معمول، بنابراین کلید نشان داده شده در بالا در قالب خواهد بود:

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

برای تبدیل عنوان، می توانید از یک سرویس آنلاین استفاده کنید، به عنوان مثال، این.

Windows Native Applications و Acronis Active Restore سرویس
به نظر می رسد که برای راه اندازی یک برنامه بومی، ما نیاز داریم:

  1. فایل اجرایی را در پوشه system32 کپی کنید
  2. یک کلید به رجیستری اضافه کنید
  3. دستگاه را مجدد راه اندازی کنید

برای راحتی، در اینجا یک اسکریپت آماده برای نصب یک برنامه بومی وجود دارد:

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

پس از نصب و راه اندازی مجدد، حتی قبل از اینکه صفحه انتخاب کاربر ظاهر شود، تصویر زیر را دریافت خواهیم کرد:

Windows Native Applications و Acronis Active Restore سرویس

مجموع

با استفاده از مثال چنین برنامه کوچکی، ما متقاعد شدیم که اجرای برنامه در سطح Windows Native کاملاً امکان پذیر است. در مرحله بعد، من و بچه های دانشگاه Innopolis به ساخت سرویسی ادامه می دهیم که روند تعامل با راننده را خیلی زودتر از نسخه قبلی پروژه خود آغاز می کند. و با ظهور پوسته win32، منطقی است که کنترل را به یک سرویس کامل که قبلاً توسعه داده شده است منتقل کنید (بیشتر در این مورد اینجا).

در مقاله بعدی به یکی دیگر از اجزای سرویس Active Restore یعنی درایور UEFI خواهیم پرداخت. در وبلاگ ما عضو شوید تا پست بعدی را از دست ندهید.

منبع: www.habr.com

اضافه کردن نظر