إصطياد دوال الـ Native API والتحكم في إنشاء عملية جديدة
مقدمة:
مؤخراً رأيت برنامج أمني رائع, إسمه Sanctuary , هذا المنتج يقوم بمنع تنفيذ أي برنامج لا يظهر في قائمته الخاصة بالبرامج القابلة للتنفيذ
والنتيجة , مستخدم الكمبيوتر محمي من أي various add-on spyware, worms and trojans , حتى لو وجدت بعض الـ malware طريقها للحاسوب , فلا يوجد عندها فرصة للتنفيذ, و من هنا لا توجد لها فرصة لإلحاق أي تدمير بالحاسوب.
بكل تأكيد وجدت أن هذه الميزة ممتعة , وبعد قليل من التفكير , خرجت بطريقتي في إنجاز هذه الوظيفة.
لذلك, هذه المقالة توضح كيف يمكن برمجياً مراقبة عملية process creation والتحكم فيها وذلك على مستوى النظام ككل و باستخدام مفهوم اصطياد Hook دوال الـ Native API.
هذه المقالة تفترض افتراض مهم وهو أن الـ process المراد مراقبتها والتحكم بها target process تم إنشائها عن طريق الـ user-mode code أي في مستوى المستخدم باحد الطرق (shell functions, CreateProcess(), manual process creation as a sequence of native API calls, etc)
على الرغم من أنه, نظرياً , يمكن إنشاء process في مستوى النظام kernel-mode code , ومع وجود هذا الإحتمال و لسبب عملي يمكن تجاهل مثل هذه المشكلة , فلا تقلق من هذه الطريقة , و لكن لماذا ؟؟؟
لنفكر بطريقة منطقية, لتشغيل عملية process في مستوى النظام kernel-mode code , نحتاج أولاً إلى تحميل driver والتي تتضمن في عملها تنفيذ بعض الخطوات في مستوى المستخدم user-mode code .
ولهذا لكي نعيق تشغيل البرامج الغير مخولة بالتشغيل يمكننا بكل أمان التحكم في العمليات التي يتم انشائها على مستوى المستخدم فقط.
بسم الله نبدأ ...
توضيح استراتيجية العمل:
مبدئيا , دعونا نحدد ما هو المطلوب فعله تماما لنقوم بمراقبة والتحكم في إنشاء عملية process creation on a system-wide basis
إنشاء عملية للتنفيذ أمر معقد بحق , حيث أنها تشمل الكثير من الوظائف (إذا لم تصدقني, يمكنك عمل disassemble للدالة CreateProcess , وسوف ترى ما اتحدث عنه بعينيك), لنقوم ببدأ عملية جديدة لابد من المرور بهذه الخطوات وبهذا الترتيب:
1 - Executable file has to be opened for FILE_EXECUTE access.
يتم فتح الملف بغرض التنفيذ FILE_EXECUTE
2 - Executable image has to be loaded into RAM.
يتم تحميل نسخة من الملف التنفيذي في الذاكرة
3 - Process Executive Object (EPROCESS, KPROCESS and PEB structures) has to be set up.
يتم إعداد متطلبات التنفيذ , مثل جدول مكتبات الربط المطلوبة والدوال الخارجية و ...
4 - Address space for the newly created process has to be allocated.
تخصيص مساحة من الذاكرة للـ process الجديدة
5 - Thread Executive Object for the primary thread of the process (ETHREAD, KTHREAD and TEB structures) has to be set up.
ألإعداد للـ Thread الرئيسية في الـ process
6 - Stack for the primary thread has to be allocated.
تخصيص مساحة من الذاكرة كمكدس للـثريد الرئيسية في الـ process
7 - Execution context for the primary thread of the process has to be set up.
إعداد سياق التنفيذ للثريد الرئيسية
8 - Win32 subsystem has to be informed about the new process
إخبار النظام ببيانات الـ process الجديدة
لنجاح أي خطوة من هذه الخطوات يشترط نجاح الخطوات السابقة لها (لا يمكنك مثلا تنفيذ الخطوة رقم 3 دون نجاح الخطوة رقم 2 , ولا يمكنك تنفيذ الخطوة رقم 2 دون نجاح الخطوة رقم 1, وهكذا..)
لذلك, لو قررنا التخلي عن خطوة من هذه الخطوات, فكل الخطوات التابعة لها سوف تفشل, ومنه عملية تنفيذ الملف التنفيذي لن تنجح.
مفهوم طبعا أن كل خطوة من هذه الخطوات يتم تنفيذها عن طريق استدعاء دالة من دوال الـ Native API , لذلك , لكي نستطيع التحكم في انشاء process جديدة, فكل ما نحتاج فعله هو عمل hook على هذه الدوال التي لا يمكن تجنب تنفيذها في كود يقوم بإنشاء prcess جديدة
ولكن أي الدوال يجب عمل hook لها ؟ على الرغم من ان الدالة NtCreateProcess تبدوا وكأنها الاجابة الأكثر مناسبة لهذا السؤال, إلا أن هذه الإجابة تعتبر خاطئة - يمكن إنشاء process جديدة دون استدعاء هذه الدالة. على سبيل المثال CreateProcess تقوم بإعداد process-related kernel-mode structures دون استدعاء NtCreateProcess.لهذا عمل Hook للدالة NtCreateProcess وحدها لا يجدي.
لمراقبة إنشاء process جديدة, يجب عمل hook للدالة NtCreateFile و الدالة NtOpenFile أو عمل hook للدالة NtCreateSection. بكل تأكيد لا توجد طريقة لتشغيل أي برنامج بدون استدعاء هذه الدوال.
لو قررنا عمل hook على دوال فتح الملفات NtCreateFile و NtOpenFile يجب إضافة خطوات للتمييز بين فتح ملف تنفيذي و فتح ملف عادي بغرض عمليات الإدخال والإخراج, وهذه العملية ليست بهذه السهولة دائما, على سبيل المثال ماذا نفعل لو تم فتح ملف تنفيذي بغرض FILE_ALL_ACCESS ؟؟؟ هل هي مجرد عملية قرائة وكتابة من ملف أم إنها مرحلة من مراحل انشاء process جديدة ؟؟
من الصعب الحكم على مثل هذه الحالة, حيث أننا يجب أن نتابع ما هي الخطوة التالية لفتح الملف.
ولهذا عمل hook على دوال فتح الملف NtCreateFile و NtOpenFile لا يعتبر الخيار الأمثل.
عمل Hook على الدالة NtCreateSection يعتبر الأكثر مناسبة في هذه العملية, فلو قمنا باعتراض استدعاء الدالة NtCreateSection عند الطلب بتحميل الملف التنفيذي كنسخة بهذه الخاصية SEC_IMAGE مع طلب حماية المنطقة المخصصة في الذاكرة للسماح بالتنفيذ , يمكننا التأكد بأن هناك process سوف يتم تنفيذها, في هذه اللحظة لدينا الخيار لنقوم باتخاذ أي قرار مناسب , وفي حالة لو أردنا منع إنشاء هذه الـ process نجعل الدالة NtCreateSection تعيد القيمة STATUS_ACCESS_DENIE.
و لهذا للحصول على تحكم تام في إنشاء process جديد كل ما علينا هو عمل hook على الدالة NtCreateSection.
تصريح الدالة :
NTSYSAPI NTSTATUS NTAPI NtCreateSection( PHANDLE SectionHandle, /*[OUT]*/ ULONG DesiredAccess, /*[IN] SECTION_MAP_EXECUTE or SECTION_ALL_ACCESS*/ POBJECT_ATTRIBUTES ObjectAttributes, /*[IN] OPTIONAL*/ PLARGE_INTEGER MaximumSize, /*[IN] OPTIONAL*/ ULONG PageAttributess, /*[IN] PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY*/ ULONG SectionAttributes, /*[IN] SEC_IMAGE*/ HANDLE FileHandle); /*[IN] OPTIONAL*/
مثل أي دالة من دوال ntdll.dll تقوم الـدالة NtCreateSection بتحميل السجل EAX برقم الخدمة , وجعل EDX يشير الى باراميترات الدالة , ونقل التحكم الى KiDispatchService و الأخيرة من kernel-mode routine ( يتم هذا عن طريق المقاطعة INT 0x2E في كل من ويندوز NTأو 2000 أو عن طريق التعليمة SYSENTER في ويندوز XP).
بعد التحقق من باراميترات الدالة , تقوم الدالة KiDispatchService بنقل التحكم الى التمثيل الحقيقي للوظيفة داخل نواة النظام , عنوان هذه المنطقة يكون متاح في Service Descriptor Table مؤشر هذا الجدول يتم استيراده عن طريق ntoskrnl.exe كـ KeServiceDescriptorTable , لهذا فهي متاحة لسواقات الـ kernel-mode (لذلك نحتاج لبناء درايفر)
تتمثل الـ Service Descriptor Table بهذا الشكل
struct SYS_SERVICE_TABLE {
void **ServiceTable;
unsigned long CounterTable;
unsigned long ServiceLimit;
void **ArgumentsTable;
};المؤشر ServiceTable في هذا الهيكل يشير الى مصفوفة تحمل عناوين الدوال التي تمثل خدمات النظام system services. لذلك كل ما علينا فعله لعمل hook على أحد دوال الـ Native API هو كتابة عنوان دالتنا الخاصة في مكان عنوان الدالة التي نريد عمل hook عليها وليكن ترتيبها i , نقوم بوضع العنوان في ServiceTable في الخانة رقم i في الـ KeServiceDescriptorTable.
الى الان يبدوا لنا معرفة كل ما نحتاجه لتنفيذ هذه العملية, دعونا نبدا في العمل الفعلي....
التحكم في إنشاء process جديدة:
الحل يتلخص في بناء درايفر a kernel-mode driver و برنامج user-mode application.
طريقة العمل: للبدأ في عملية المراقبة يقوم البرنامج بتمرير (رقم الوظيفة the service index والذي يمثل الدالة NtCreateSection , إضافة إلى عنوان الـ exchange buffer مكان تبادل البيانات بين البرنامج والدرايفر) الى الـ driver , ويتم هذا عن طريق هذا الكود:
//open device
device=CreateFile("\\\\.\\PROTECTOR",GENERIC_READ|GENERIC_WRITE,
0,0,OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM,0);
// get index of NtCreateSection, and pass it to the driver, along with the
//address of output buffer
DWORD * addr=(DWORD *)
(1+(DWORD)GetProcAddress(GetModuleHandle("ntdll.dll"),
"NtCreateSection"));
ZeroMemory(outputbuff,256);
controlbuff[0]=addr[0];
controlbuff[1]=(DWORD)&outputbuff[0];
DeviceIoControl(device,1000,controlbuff,256,controlbuff,256,&dw,0);الكود تقريبا يشرح نفسه, الشيء الوحيد الذي يستحق الاهتمام هي الطريقة التي نحصل بها عن رقم الخدمة المناظرة للدالة NtCreateSection
جميع الوظائف داخل ntdll.dll تبدأ بالتعليمة
MOV EAX, ServiceIndex
وهي تعليمة تتمثل في 5 بايت , حيث mov eax - opcode - يتمثل في 1 بايت (الأول), والـ service index يتمثل في 4 بايت (الباقي) , ولذلك كل ما علينا هو قرائة 4 بايت من ثاني بايت في الدالة NtCreateSection لنحصل على قيمة ServiceIndex
والآن لنرى ماذا يفعل الـدرايفر الخاص بنا عند استلامه IOCTL من برنامجنا :
NTSTATUS DrvDispatch(IN PDEVICE_OBJECT device,IN PIRP Irp)
{
UCHAR*buff=0; ULONG a,base;
PIO_STACK_LOCATION loc=IoGetCurrentIrpStackLocation(Irp);
if(loc->Parameters.DeviceIoControl.IoControlCode==1000)
{
buff=(UCHAR*)Irp->AssociatedIrp.SystemBuffer;
// hook service dispatch table
memmove(&Index,buff,4);
a=4*Index+(ULONG)KeServiceDescriptorTable->ServiceTable;
base=(ULONG)MmMapIoSpace(MmGetPhysicalAddress((void*)a),4,0);
a=(ULONG)&Proxy;
_asm
{
mov eax,base
mov ebx,dword ptr[eax]
mov RealCallee,ebx
mov ebx,a
mov dword ptr[eax],ebx
}
MmUnmapIoSpace(base,4);
memmove(&a,&buff[4],4);
output=(char*)MmMapIoSpace(MmGetPhysicalAddress((void*)a),256,0);
}
Irp->IoStatus.Status=0;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return 0;
}كما يتضح هنا , لا يوجد شيء خاص بالاهتمام , قمنا فقط بتخطيط الـ exchange buffer في مساحة عناوين النواة عن طريق الدالة MmMapIoSpace , إضافة إلى كتابة عنوان الدالة البديلة our proxy function إلى الـ Service Table (ملحوظة : نقوم أولاً بحفظ عنوان الدالة الأصلية لكي يتم استخدامها لاحقاً في المتغير العام RealCallee ).
لكي نتمكن من إعادة الكتابة على الـ Service Table نقوم بتخطيط المكان المستهدف بالدالة MmMapIoSpace. لماذا ؟ لدينا بالفعل الصلاحية للتعامل مع الـ Service Table , أليس كذلك ؟
المشكلة تكمن في أنه يمكن للـ Service Table أن تتواجد في منطقة من الذاكرة مخصصة للقرائة فقط, ولهذا يجب التأكد أن لدينا الصلاحية للكتابة في الذاكرة الخاصة بالـ Service Table , وإذا لم تكن لدينا هذه الصلاحية فإنه يتحتم علينا تغيير الحماية على هذه المنطقة من الذاكرة ليتم السماح لنا بالكتابة عليها, مجهود كبير أليس كذلك ؟
ولهذا فإننا سنقوم فقط بتخطيط الذاكرة عن طريق MmMapIoSpace , ولهذا لا توجد مشاكل لدينا من ناحية الحماية يمكننا الكتابة في أي وقت نريد في هذه المنطقة من الآن فصاعدا
والآن دعونا نلقي نظرة عن الـدالة البديلة أو ما تعرف بــ our proxy function :
//this function decides whether we should
//allow NtCreateSection() call to be successfull
ULONG __stdcall check(PULONG arg)
{
HANDLE hand=0;PFILE_OBJECT file=0;
POBJECT_HANDLE_INFORMATION info;ULONG a;char*buff;
ANSI_STRING str; LARGE_INTEGER li;li.QuadPart=-10000;
//check the flags. If PAGE_EXECUTE access to the section is not requested,
//it does not make sense to be bothered about it
if((arg[4]&0xf0)==0)return 1;
if((arg[5]&0x01000000)==0)return 1;
//get the file name via the file handle
hand=(HANDLE)arg[6];
ObReferenceObjectByHandle(hand,0,0,KernelMode,&file,&info);
if(!file)return 1;
RtlUnicodeStringToAnsiString(&str,&file->FileName,1);
a=str.Length;buff=str.Buffer;
while(1)
{
if(buff[a]=='.'){a++;break;}
a--;
}
ObDereferenceObject(file);
//if it is not executable, it does not make sense to be bothered about it
//return 1
if(_stricmp(&buff[a],"exe")){RtlFreeAnsiString(&str);return 1;}
//now we are going to ask user's opinion.
//Write file name to the buffer, and wait until
//the user indicates the response
//(1 as a first DWORD means we can proceed)
//synchronize access to the buffer
KeWaitForSingleObject(&event,Executive,KernelMode,0,0);
// set first 2 DWORD of a buffer to zero,
// copy the string into the buffer, and loop
// until the user sets first DWORD to 1.
// The value of the second DWORD indicates user's
//response
strcpy(&output[8],buff);
RtlFreeAnsiString(&str);
a=1;
memmove(&output[0],&a,4);
while(1)
{
KeDelayExecutionThread(KernelMode,0,&li);
memmove(&a,&output[0],4);
if(!a)break;
}
memmove(&a,&output[4],4);
KeSetEvent(&event,0,0);
return a;
}
//just saves execution contect and calls check()
_declspec(naked) Proxy()
{
_asm{
//save execution contect and calls check()
//-the rest depends upon the value check() returns
// if it is 1, proceed to the actual callee.
//Otherwise,return STATUS_ACCESS_DENIED
pushfd
pushad
mov ebx,esp
add ebx,40
push ebx
call check
cmp eax,1
jne block
//proceed to the actual callee
popad
popfd
jmp RealCallee
//return STATUS_ACCESS_DENIED
block:popad
mov ebx, dword ptr[esp+8]
mov dword ptr[ebx],0
mov eax,0xC0000022L
popfd
ret 32
}
}تقوم الدالة Proxy بـحفظ السجلات والأعلام, تقوم بتمرير مؤشر لـ service parameters للدالة check والخطوات التالية تعتمد على القيمة التي تعيدها الدالة check
لو أعادت TRUE (أي اننا نريد تنفيذ البرنامج) , تقوم الدالة Proxy بإعادة قيم السجلات والأعلام واستدعاء الخدمة الأصلية NtCreateSection
لو أعادت FALSE (أي اننا نعترض على تنفيذ البرنامج) , تقوم الدالة Proxy بتمرير القيمة STATUS_ACCESS_DENIED إلى EAX , استرجاع القيمة ESP والعودة من الخدمة - من وجهة نظر مستدعي الوظيفة NtCreateSection يبدوا وكأن الوظيفة فشلت و أعادت القيمة STATUS_ACCESS_DENIED .
كيف تبني الدالة check قراراها ؟ بمجرد ما تتسلم الدالة check المؤشر للـ service parameters كبراميتر لها , يمكنها اختبار هذه الباراميترات
أولا: تختبر الأعلام والخصائص - لو الـ Section المطلوبة لم يطلب تخطيطها كنسخة قابلة للتنفيذ , أو انها طلبت حماية تمنع التنفيذ في هذا الـ Section من الذاكرة , ومنه يمكننا التأكد أن الخدمة NtCreateSection لن تفعل شيء مع هذا الطلب , وفي هذه الحالة نقوم بتمرير القيمة TRUE مباشرة.
ما عدا هذا , تقوم الدالة check باختبار امتداد الملف لأن الخاصية SEC_IMAGE و حماية الذاكرة بغرض التنفيذ يمكن طلبه لملفات الـ dll أيضا , لهذا لو كان امتداد الملف غير .exe تقوم الدالة check بإعادة القيمة TRUE
في حالة ما يكون الملف التنفيذي بامتداد .exe فانها تعطي الفرصة للبرنامج في مستوى الـ user-mode باتخاذ القرار, ولهذا تقوم فقط بكتابة إسم الملف و مكانه على القرص في الـ exchange buffer , وتنتظر القرار من المستخدم لتمرره مره أخرى الى الـ Proxy Function
قبل تشغيل الدرايفر الخاص بنا يقوم البرنامج الخاص بنا بانشاء ثريد يقوم بتشغيل هذه الدالة أولاً:
void thread()
{
DWORD a,x; char msgbuff[512];
while(1)
{
memmove(&a,&outputbuff[0],4);
//if nothing is there, Sleep() 10 ms and check again
if(!a){Sleep(10);continue;}
// looks like our permission is asked. If the file
// in question is already in the white list,
// give a positive response
char*name=(char*)&outputbuff[8];
for(x=0;x<stringcount;x++)
{
if(!stricmp(name,strings[x])){a=1;goto skip;}
}
// ask user's permission to run the program
strcpy(msgbuff, "Do you want to run ");
strcat(msgbuff,&outputbuff[8]);
// if user's reply is positive, add the program to the white list
if(IDYES==MessageBox(0, msgbuff,"WARNING",
MB_YESNO|MB_ICONQUESTION|0x00200000L))
{a=1; strings[stringcount]=_strdup(name);stringcount++;}
else a=0;
// write response to the buffer, and driver will get it
skip:memmove(&outputbuff[4],&a,4);
//tell the driver to go ahead
a=0;
memmove(&outputbuff[0],&a,4);
}
}الكود واضح , يقوم الثريد بقرائة الـ exchange buffer كل 10 ms , لو اكتشف أن الدرايفر صدر طلب للـ buffer , فإنه يقوم باختبار إسم الملف الممرر له ويختبر لو إسم الملف في القائمة المصرح لها بالتشغيل , لو كان ضمن القائمة فإنه يعطي OK للدرايفر ليسمح بتنفيذ العملية , وإلا فإنه يعطي رسالة تسأل المستخدم هل يسمح بتشغل هذا البرنامج أم لا , مع عرض إسم الملف ومكانه في الرسالة , لو الرد إيجابي نقوم بإضافة إسم البرنامج إلى القائمة المسموح بتشغيلها, أخيرا نمرر قرار المستخدم الى الدرايفر
في جميع الأحوال بعد تشغيل برنامجنا , بكل تأكيد لا توجد طريقة لتشغيل أي عملية دون سؤال المستخدم بالسماح لها بذلك
الخلاصة :
في النهاية يجب أن اقول أن عملية الـ hook لدوال الـ native API واحدة من أهم تقنيات البرمجة الموجودة, هذه المقالة أعطتك فقط مثال لما يمكن إنجازه بهذه التقنية , كما ترى تمكنا من مراقبة و التحكم في تنفيذ البرامج التي لم نعطيعا صلاحية التشغيل , وهذا فقط بعمل hook على دالة واحدة فقط (!!!) من دوال الـ native API, يمكنك التوسع في هذا المجال الى حد بعيد والتحكم في العتاد بصورة كاملة , التحكم في عمليات القرائة والكتابة , التحكم في الشبكات ...الخ


