تطبيقات Windows الأصلية وخدمة Acronis Active Restore

نواصل اليوم قصة كيف نقوم، جنبًا إلى جنب مع شباب من جامعة إنوبوليس، بتطوير تقنية Active Restore للسماح للمستخدم ببدء العمل على أجهزته في أقرب وقت ممكن بعد حدوث فشل. سنتحدث عن تطبيقات Windows الأصلية، بما في ذلك ميزات إنشائها وإطلاقها. يوجد أدناه القليل عن مشروعنا، بالإضافة إلى دليل عملي حول كيفية كتابة التطبيقات الأصلية.

تطبيقات Windows الأصلية وخدمة Acronis Active Restore

في المشاركات السابقة تحدثنا بالفعل عن ما هو عليه استعادة نشطةوكيف يتطور الطلاب من إنوبوليس خدمة. اليوم أريد التركيز على التطبيقات المحلية، إلى المستوى الذي نريد "دفن" خدمة الاسترداد النشطة لدينا. إذا سارت الأمور على ما يرام، فسنكون قادرين على:

  • قم بتشغيل الخدمة نفسها قبل ذلك بكثير
  • اتصل بالسحابة حيث توجد النسخة الاحتياطية قبل ذلك بكثير
  • قبل ذلك بكثير، لفهم الوضع الذي يوجد فيه النظام - التمهيد العادي أو الاسترداد
  • عدد أقل بكثير من الملفات التي يمكن استعادتها مسبقًا
  • السماح للمستخدم بالبدء بشكل أسرع.

ما هو التطبيق الأصلي على أي حال؟

للإجابة على هذا السؤال، دعونا ننظر إلى تسلسل الاستدعاءات التي يقوم بها النظام، على سبيل المثال، إذا حاول أحد المبرمجين في تطبيقه إنشاء ملف.

تطبيقات Windows الأصلية وخدمة Acronis Active Restore
بافل يوسيفوفيتش - برمجة Windows Kernel (2019)

يستخدم المبرمج الوظيفة CreateFile، والذي تم الإعلان عنه في الملف الرأسي fileapi.h وتم تنفيذه في Kernel32.dll. ومع ذلك، هذه الوظيفة نفسها لا تقوم بإنشاء الملف، بل تتحقق فقط من وسائط الإدخال وتستدعي الوظيفة NtCreateFile (تشير البادئة Nt فقط إلى أن الوظيفة أصلية). تم الإعلان عن هذه الوظيفة في ملف رأس Winternl.h وتنفيذها في ntdll.dll. يستعد للقفز إلى الفضاء النووي، وبعد ذلك يقوم باستدعاء النظام لإنشاء ملف. في هذه الحالة، اتضح أن Kernel32 هو مجرد غلاف لـ Ntdll. أحد أسباب القيام بذلك هو أن Microsoft لديها القدرة على تغيير وظائف العالم الأصلي، ولكن دون لمس الواجهات القياسية. لا توصي Microsoft باستدعاء الوظائف الأصلية مباشرة ولا تقوم بتوثيق معظمها. بالمناسبة، يمكن العثور على وظائف غير موثقة هنا.

الميزة الرئيسية للتطبيقات الأصلية هي أن ntdll يتم تحميله في النظام في وقت أبكر بكثير من kernel32. وهذا أمر منطقي، لأن kernel32 يتطلب ntdll للعمل. ونتيجة لذلك، يمكن للتطبيقات التي تستخدم الوظائف الأصلية أن تبدأ العمل في وقت أبكر بكثير.

وبالتالي، فإن تطبيقات Windows الأصلية هي برامج يمكن أن تبدأ مبكرًا عند تشغيل Windows. إنهم يستخدمون فقط وظائف من ntdll. مثال على مثل هذا التطبيق: com.autochk من يؤدي فائدة القرص لفحص القرص بحثًا عن الأخطاء قبل بدء الخدمات الرئيسية. هذا هو بالضبط المستوى الذي نريد أن تكون عليه الاستعادة النشطة.

ماذا نحتاج؟

  • DDK (مجموعة تطوير برامج التشغيل)، المعروفة الآن أيضًا باسم WDK 7 (مجموعة برامج تشغيل Windows).
  • الجهاز الظاهري (على سبيل المثال، Windows 7 x64)
  • ليس ضروريًا، لكن ملفات الرأس التي يمكن تنزيلها قد تساعد هنا

ماذا يوجد في الكود؟

دعونا نتدرب قليلًا، وعلى سبيل المثال، نكتب تطبيقًا صغيرًا:

  1. يعرض رسالة على الشاشة
  2. يخصص بعض الذاكرة
  3. ينتظر إدخال لوحة المفاتيح
  4. يحرر الذاكرة المستخدمة

في التطبيقات الأصلية، نقطة الإدخال ليست main أو winmain، ولكن وظيفة NtProcessStartup، نظرًا لأننا نقوم فعليًا بتشغيل عمليات جديدة في النظام بشكل مباشر.

لنبدأ بعرض رسالة على الشاشة. لهذا لدينا وظيفة أصلية نتديسبلايسترينغ، والذي يأخذ كوسيطة مؤشر لكائن بنية 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 في وقت التشغيل). وبطبيعة الحال، يمكنك فقط استخدام المكدس. ولكن إذا كنا بحاجة إلى تخصيص الذاكرة ديناميكيًا، فسيتعين علينا القيام بذلك على الكومة (أي الكومة). لذلك دعونا ننشئ كومة لأنفسنا ونأخذ الذاكرة منها عندما نحتاج إليها.

الوظيفة مناسبة لهذه المهمة 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;
	}
}

كل ما نحتاجه هو الاستخدام نتريدفيل على جهاز مفتوح، وانتظر حتى تقوم لوحة المفاتيح بإرجاع أي ضغطة إلينا. إذا تم الضغط على مفتاح 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 بجهاز افتراضي لتصحيح أخطاء kernel. يمكن العثور على تعليمات حول كيفية القيام بذلك هنا أو مجرد استخدام VirtualKD.

التجميع والتجميع

أسهل طريقة لإنشاء تطبيق أصلي هي استخدام DDK (مجموعة تطوير السائق). نحن بحاجة إلى الإصدار السابع القديم، حيث أن الإصدارات الأحدث لها نهج مختلف قليلاً وتعمل بشكل وثيق مع Visual Studio. إذا استخدمنا DDK، فإن مشروعنا يحتاج فقط إلى 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، لا تقلق من عدم الإعلان عن متغير النظام هذا، عندما نقوم بتشغيل الإصدار المحدد من DDK، سيتم الإعلان عن هذا المتغير وسيشير إلى المكتبات الضرورية.
  • المصادر – قائمة مصادر لبرنامجك.
  • يتضمن - ملفات الرأس المطلوبة للتجميع. تشير عادةً هنا إلى المسار إلى الملفات التي تأتي مع DDK، ولكن يمكنك بالإضافة إلى ذلك تحديد أي ملفات أخرى.
  • TARGETLIBS – قائمة المكتبات التي تحتاج إلى الارتباط.
  • USE_NTDLL هو حقل مطلوب يجب ضبطه على 1 لأسباب واضحة.
  • USER_C_FLAGS – أي إشارات يمكنك استخدامها في توجيهات المعالج المسبق عند إعداد كود التطبيق.

لذا، للإنشاء، نحتاج إلى تشغيل الإصدار x86 (أو x64) الذي تم التحقق منه، وتغيير دليل العمل إلى مجلد المشروع وتشغيل أمر البناء. تظهر النتيجة في لقطة الشاشة أن لدينا ملفًا واحدًا قابلاً للتنفيذ.

تطبيقات Windows الأصلية وخدمة Acronis Active Restore

لا يمكن إطلاق هذا الملف بهذه السهولة، يلعن النظام ويدفعنا للتفكير في سلوكه مع ظهور الخطأ التالي:

تطبيقات Windows الأصلية وخدمة Acronis Active Restore

كيفية تشغيل التطبيق الأصلي؟

عند بدء التشغيل التلقائي، يتم تحديد تسلسل بدء تشغيل البرامج من خلال قيمة مفتاح التسجيل:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

يقوم مدير الجلسة بتنفيذ البرامج من هذه القائمة واحدًا تلو الآخر. يبحث مدير الجلسة عن الملفات القابلة للتنفيذ بنفسه في دليل 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 الأصلية وخدمة 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 الأصلية وخدمة Acronis Active Restore

مجموع

باستخدام مثال هذا التطبيق الصغير، كنا مقتنعين بأنه من الممكن تمامًا تشغيل التطبيق على مستوى Windows الأصلي. بعد ذلك، سنواصل أنا ورفاق جامعة إنوبوليس بناء خدمة من شأنها أن تبدأ عملية التفاعل مع السائق في وقت أبكر بكثير مما كانت عليه في الإصدار السابق من مشروعنا. ومع ظهور Win32 Shell، سيكون من المنطقي نقل التحكم إلى خدمة كاملة تم تطويرها بالفعل (المزيد حول هذا) هنا).

في المقالة التالية سوف نتطرق إلى مكون آخر من خدمة Active Restore، وهو برنامج تشغيل UEFI. اشترك في مدونتنا حتى لا تفوت المنشور التالي.

المصدر: www.habr.com

إضافة تعليق