نوشتن یک File System Filter Driver
در ادامه سری آموزش هایی که در مورد درایور نویسی در ویندوز نوشته ام، این بار آموزشی در مورد نوشتن یک Filesystem Filter Driver آماده کرده ام. از عنوان آموزش مشخص است که این درایور از نوع Filter Driver است که قبلا توضیح مختصری در مورد این نوع درایورها در آموزش مربوط به Device Stack داده ام، این درایور قرار است Filesystem هایی که روی سیستم رجیستر شده یا بعدا رجیستر می شوند را مانیتور کند. در انتهای این آموزش بخشی اختصاص داده ام در مورد کار با WinDbg. تمام فرمان هایی که در این بخش آمده را قبلا در آموزش مربوط به Device Stack در موردشان توضیح داده ام. دلیل آوردن چنین بخشی این بود که قسمت هایی از این آموزش هست که به موضوعات دیگری و مفصلی مرتبط هستند. برای اینکه وارد این توضیحات مفصل نشوم ترجیح دادم با نشان دادن آن مساله در WinDbg مساله را برایتان روشن کنم.
قبل ادامه مروری هم روی آموزش های زیر داشته باشید.
- آموزش درایور نویسی - قسمت اول - سلام دنیا
- آموزش درایور نویسی - قسمت دوم - ارتباط با سطح کاربر
- Device Stack چیست؟
سرفصل های این آموزش
مقدمه
همانطور که بالاتر اشاره کرده ام در این آموزش قرار است یک Filter Driver بنویسیم. این Filter Driver در واقع یک Upper Filter Driver است. همانطور که در آموزشهای قبلی گفته بودم این نوع درایور قبل یک Function Driver قرار می گیرد. در آموزش ما Function Driver هایی که ما با آنها سروکار داریم مربوط به Filesystem ها می شوند. هدف از نوشتن این فیلتر درایور این است که بتوانیم تمام درخواست هایی که به یک Filesystem فرستاده میشود را دریافت کنیم. این درخواست ها برای مثال می تواند ایجاد (Create)، خواندن (Read)، نوشتن (Write) یک فایل روی یک Filesystem باشد. ما در این مثال بعد دریافت همه درخواست ها فقط درخواست های ایجاد (Create) را با DbgPrint به بافر Debug می فرستیم که بعد با برنامه DbgView بتوانیم این درخواست ها را مشاهده کنیم.
پایین تصویری است از Stack یکسری Filesystem رجیستر شده روی یک سیستم. این Filesystem ها عبارتند از NTFS, UDFS, MUP, .... درایور ما اصطلاحا باید به Stack یک Filesystem متصل (Attach) شود. همانطور که می بینید درایور ما که به رنگ قرمز مشخص شده در بالای Stack هر کدام از Filesystem ها قرار گرفته.
اگر خواستین روی آموزشهای قبلی که در مورد Device Stack و فرمان های WinDbg بود مروری داشته باشید می توانید، به انتهای این آموزش بخش "کار با Windbg - قسمت اول" رجوع کنید.
مفهوم Stack Location
یکسری مفاهیم است که لازمه قبل از ادامه آموزش آنها را توضیح بدهم. یکی از این مفاهیم Stack Location است. در آموزش های قبلی گفته بودم که یک ساختار IO_STACK_LOCATION داریم که همراه ساختار IRP است که وقتی برنامه سطح کاربر یک درخواست دارد (مثلا خواندن محتویات یک فایل) پارامترهای مربوط به این درخواست داخل این ساختار قرار می گیرد. لازم به ذکر است که استفاده از این ساختار فقط محدود به برنامه سطح کاربر نیست، و در ارتباط درایور با یک درایور دیگر نیز این ساختار مورد استفاده قرار می گیرد.
بطور دقیقتر هر ساختار IRP می تواند آرایه ای از ساختار IO_STACK_LOCATION به همراه خود داشته باشد. این آرایه دقیقا بعد از ساختار IRP قرار گرفته است. (مانند تصویر زیر).
دلیل داشتن آرایه ای از IO_STACK_LOCATION این است که یک IRP از Stack ای از درایورها می گذرد. در نتیجه به ازای هر درایور در مسیر IRP یک IO_STACK_LOCATION وجود دارد. تعداد Stack Location ها از عضو داده ای به نام StackSize مربوط به بالاترین Device Object داخل Stack مشخص می شود.
از عضو داده های این ساختار که در آموزش های قبلی دیده ایم، ما با Parameters بیشتر سروکار داشته ایم (رجوع به اینجا و اینجا). در این آموزش هم در مورد عضو داده دیگری به نام CompletionRoutine صحبت خواهیم کرد.
درایور می تواند با ماکروی زیر به ساختار Stack Location خود دسترسی داشته باشد.
PIO_STACK_LOCATION irpSp; irpSp = IoGetCurrentIrpStackLocation( Irp );
یک درایور می تواند از پردازش یک درخواست (IRP) صرف نظر کند (Skip). این کار را با این ماکرو انجام می دهد. در این حالت ما نباید هیچ عضو داده ای از Stack Location را تغییر دهیم.
IoSkipCurrentIrpStackLocation( Irp );
اگر بخواهیم تغییر در Stack Location بدهیم. مثلا بخواهیم عضو داده CompletionRoutine را مقدار بدهیم این ماکروها را صدا می زنیم. در واقع اول یک کپی از Stack Location می گیریم و بعد CompletionRoutine را مقدار می دهیم.
IoCopyCurrentIrpStackLocationToNext( Irp ); IoSetCompletionRoutine( Irp, CompletionRoutine, ...);
جلوتر که به تحلیل کد درایور برسیم این موضوعات روشن تر خواهد شد.
مفهوم Fast I/O
وقتی یک درخواست از برنامه سطح کاربر به کرنل می آید (مثلا خواندن از یک فایل). I/O Manager قبل از اینکه برای این درخواست یک ساختار IRP ایجاد کند یکسری روال ها را انجام می دهد. یکی از این کارها صدا زدن توابعی است که به آنها Fast I/O گفته می شود. هدف از Fast I/O این است که در صورتی که درخواست قبلا در حافظه Cache شده باشد و این اطلاعات Cache شده پاسخگوی درخواست باشند، I/O Manager دیگر IRP نمی سازد، در نتیجه اینطوری سریعتر می توان به یک درخواست پاسخ داد، و همچنین کارهایی مثل تخصیص حافظه برای IRP و پیمایش IRP در Stack ای از دایورها را هم نداریم.
داخل ساختار DRIVER_OBJECT یک عضو داده به نام FastIoDispatch وجود دارد. این عضو داده خود یک ساختار است با این محتویات
typedef struct _FAST_IO_DISPATCH { ULONG32 SizeOfFastIoDispatch; PVOID FastIoCheckIfPossible; PVOID FastIoRead; PVOID FastIoWrite; PVOID FastIoQueryBasicInfo; PVOID FastIoQueryStandardInfo; PVOID FastIoLock; PVOID FastIoUnlockSingle; PVOID FastIoUnlockAll; PVOID FastIoUnlockAllByKey; PVOID FastIoDeviceControl; PVOID AcquireFileForNtCreateSection; PVOID ReleaseFileForNtCreateSection; PVOID FastIoDetachDevice; PVOID FastIoQueryNetworkOpenInfo; PVOID AcquireForModWrite; PVOID MdlRead; PVOID MdlReadComplete; PVOID PrepareMdlWrite; PVOID MdlWriteComplete; PVOID FastIoReadCompressed; PVOID FastIoWriteCompressed; PVOID MdlReadCompleteCompressed; PVOID MdlWriteCompleteCompressed; PVOID FastIoQueryOpen; PVOID ReleaseForModWrite; PVOID AcquireForCcFlush; PVOID ReleaseForCcFlush; } FAST_IO_DISPATCH, *PFAST_IO_DISPATCH;
هر فیلتر درایور حتما باید این ساختار را مقدار دهی کند. جلوتر که به تحلیل کد درایور برسیم نشان داده ام که چطور این توابع را باید پیاده سازی کرد.
تحلیل کد درایور
درایوری که برای این آموزش انتخاب کرده ام، یکی از نمونه درایورهای WDK (نسخه های قدیمی ترش) است به نام sfilter. این کد برای ویندوزهای 2000 به بالا نوشته شده و همچنین موضوعات مختلفی را پوشش داده است. من برای اینکه پیچیدگی این کد را کمتر کنم خیلی قسمت ها را از کد حذف و همچنین کد را به چند سورس فایل کوچکتر تقسیم کرده ام. لینک هر دو کد را اینجا قرار می دهم اگر خواستین کد اصلی را هم داشته باشید.
- کد نمونه درایور (کد مورد استفاده در این آموزش)
- کد نمونه درایور اصلی (بدون تغییر)
تحلیل کد تابع DriverEntry
با اینکه من خیلی بخش ها را از کد حذف کردم ولی بازم فکر می کنم مطلب برای گفتن زیاد دارد. مراحلی که برای ساخت یک فیلتر درایور باید انجام بدهید به قرار زیر است.
اول ایجاد یک Device Object
RtlInitUnicodeString( &nameString, L"\\FileSystem\\Filters\\SFilter" ); status = IoCreateDevice( DriverObject, 0, &nameString, FILE_DEVICE_DISK_FILE_SYSTEM, FILE_DEVICE_SECURE_OPEN, FALSE, &gSFilterControlDeviceObject );
در مورد پارامتر های بالا قبلا در آموزش دوم توضیحاتی داده ام و فقط پارامتر چهارم تغییر کرده. این پارامتر در واقع DeviceType بود که به FILE_DEVICE_DISK_FILE_SYSTEM مقدار داده ایم که نشان می دهد نوع درایور از نوع Filesystem است.
دوم مقدار دادن آرایه MajorFunction
از همه مقادیر این آرایه فقط IRP_MJ_CREATE برای ما مهم است و باقی ایندکس ها که Irp مربوط به آنها را نمی خواهیم پردازش کنیم با SfPassThrough مقدار داده ایم.
for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) { DriverObject->MajorFunction[i] = SfPassThrough; } DriverObject->MajorFunction[IRP_MJ_CREATE] = SfCreate;
توضیح تابع SfPassThrough
این تابع را به صورت زیر تعریف کرده ایم و کارش این است که اولا، از پردازش درخواست صرف نظر کند با IoSkipCurrentIrpStackLocation و دوما، Irp را با به درایور پایین تر از خود در Stack می فرستد.
NTSTATUS SfPassThrough ( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) ... ... IoSkipCurrentIrpStackLocation( Irp ); return IoCallDriver( ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension)->AttachedToDeviceObject, Irp ) }
تابع IoCallDriver که در کد بالا استفاده کرده ایم به صورت زیر تعریف شده است. کار این تابع ارسال یک Irp به یک درایور دیگر است.
NTSTATUS IoCallDriver( _In_ PDEVICE_OBJECT DeviceObject, _Inout_ PIRP Irp );
توضیح پارامترهای این تابع به نظرم خیلی واضح است ولی با این حال یک توضیحی می دهیم.
DeviceObject: مربوط به درایوری است که می خواهیم Irp را به آن بفرستیم. اگر یادتان باشد قبلا گفته ام برای ارتباط با یک درایور باید با Device Object ای که آن درایور ایجاد کرده ارتباط برقرار کنیم.
Irp: این هم که معلوم است Irp مربوطه که قرار است ارسال شود.
حالا در مثالی که زدم این Device Object از کجا آمده، این مطلب را جلوتر وقتی به Attach شدن به یک Device Object و توضیح ساختا SFILTER_DEVICE_EXTENSION رسیدیم شرح خواهم داد.
توضیح تابع SfCreate
و اما ادامه مطلب، بالاتر گفتیم که ما قرار است فقط درخواست IRP_MJ_CREATE را پردازش کنیم و این کار توسط تابع SfCreate انجام می شود. این تابع را به صورت زیر تعریف کرده ایم.
NTSTATUS SfCreate ( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { NTSTATUS status; PAGED_CODE();
if (IS_MY_CONTROL_DEVICE_OBJECT(DeviceObject)) { /**** (1) شرط اول *****/ Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST; Irp->IoStatus.Information = 0; IoCompleteRequest( Irp, IO_NO_INCREMENT ); return STATUS_INVALID_DEVICE_REQUEST; } ... if (!FlagOn( SfDebug, SFDEBUG_DO_CREATE_COMPLETION | SFDEBUG_GET_CREATE_NAMES| SFDEBUG_DISPLAY_CREATE_NAMES )) { /**** (2) شرط دوم ****/
IoSkipCurrentIrpStackLocation( Irp );
return IoCallDriver( ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension)->AttachedToDeviceObject, Irp ); } else { /**** (3) شرط سوم ****/ ...
...
IoCopyCurrentIrpStackLocationToNext( Irp ); IoSetCompletionRoutine( Irp, SfCreateCompletion, &waitEvent, TRUE, TRUE, TRUE ); status = IoCallDriver( ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension)->AttachedToDeviceObject, Irp ); ... ... return status; } }
این تابع کی صدا زده می شود،؟وقتی یک برنامه سطح کاربر یک فایل بخواهد ایجاد/باز (Create/Oprn) کند (البته باز اشاره کنم فرستادن یک درخواست فقط محدود به برنامه سطح کاربر نمی شود). یادتان باشد که ما تمام در خواست های ایجاد یا باز کردن فایل روی سیستم را خواهیم گرفت. یعنی هر برنامه ای بخواهد یک فایل ایجاد کند در این تابع ما درخواست را دریافت میکنیم. (خیلی از Antivirus ها و IDS ها یک درایور دارند که دقیقا از همین تکنیک برای مانیتور کردن سیستم استفاده می کنند. شاید یکم با جزئیات بیشتر ولی تکنیک همین است).
حالا تابع ما چکار می کند. در واقع بر اساس سه شرط کارهای مختلفی انجام می دهیم
- (شرط اول): اگر درخواست به Device Object درایور ما بود یعنی همانی که در DriverEntry ساخته ایم. همینجا در خواست را خاتمه می دهیم. اگر یادتان باشد در آموزش دوم هم کدی مشابه این داشتیم.
- (شرط دوم): این درایور یک امکانی داخل خودش پیاده کرده است. در واقع از طریق یک کلید رجیستری ما می توانیم پیام هایی که در بافر Debug چاپ می کنیم کنترل کنیم. اینجا دراین شرط گفتیم اگر هیچ کدام از مقدارهای SFDEBUG_DO_CREATE_COMPLETION یا SFDEBUG_GET_CREATE_NAMES یا SFDEBUG_DISPLAY_CREATE_NAMES تعریف نشده دستورات این شرط را اجرا کند (در مورد این کلید رجیستری و این مقدارها جلوتر توضیح داده ام). حالا این دستورات چکار می کنند؟ اول اینکه از Stack Location صرف نظر و درخواست را به درایور پایین Stack ارسال می کند.
- (شرط سوم): اگر یکی از مقادیری که اشاره کردیم تعریف شده بود دستورات این شرط اجرا می شوند. این دستورات چکار می کنند؟ یک CompletionRoutine برای درایور ما تعریف می کند. چطوری؟ بالاتر گفته بودیم که ساختار IO_STACK_LOCATION یک عضو داده دارد به نام CompletionRoutine که یک اشاره گر به تابع است. برای اینکه بتوانیم عضو داده های Stack Location را تغییر دهیم گفتیم اول از ماکروی IoCopyCurrentIrpStackLocationToNext و بعد برای تعریف یک CompletionRoutine از ماکروی IoSetCompletionRoutine استفاده میکنیم. تابع CompletionRoutine ای که در درایور خود تعریف کرده ایم (تابع SfCreateCompletion) فقط یک Event را به حالت signal در می آورد (با تابع KeSetEvent). به این طریق متوجه می شویم که پردازش درخواست Irp به پایان رسیده و همچنین تابعی که تعریف کرده ایم توسط I/O Manager صدا زده شده است.
توضیح در مورد CompletionRoutine
در مورد مقدار دادن عضو داده CompletionRoutine زیاد توضیح داده ام ولی در مورد اینکه کی این تابع صدا زده می شود هنوز توضیحی نداده ام. این تابع کی صدا زده می شود؟ وقتی یکی از درایورهای داخل Stack با تابع IoCompleteRequest مشخص کند که کارش با درخواست به پایان رسیده است. در این حالت I/O Manager شروع می کند تمام درایورهای داخل Stack را پیمایش می کند و در صورتی که تابع CompletionRoutine در درایور تعریف شده باشد آن را صدا می زند.
از آنجایی که این تابع در IRQL <= DISPATCH_LEVEL اجرا می شود (IRQL در سطح DPC و پایین تر، برای اطلاعات بیشتر در مورد IRQL به این آموزش رجوع کنید ).
اولا) تابع ممکن است در IRQL سطح DPC اجرا بشود، در نتیجه کد آن ناحیه باید از نوع Nonpaged باشد (یعنی باید همیشه در حافظه باقی بماند). چطور مطمعن بشیم کد آن تابع از نوع Nonpaged باشد؟ در واقع بطور پیشفرض حافظه مربوط به کدهای درایور بصورت Nonpaged است مگر اینکه خودمان این مساله را تغییر بدهیم. چطور؟ با استفاده از کدهای زیر که در ابتدای فایل sfilter.c وجود دارد.
#ifdef ALLOC_PRAGMA #pragma alloc_text(INIT, DriverEntry) #if DBG #pragma alloc_text(PAGE, DriverUnload) #endif #pragma alloc_text(PAGE, SfFsNotification) #pragma alloc_text(PAGE, SfCreate) #pragma alloc_text(PAGE, SfCleanupClose) ... ... #endif
دوما) تابع ممکن است در IRQL سطوح پایین تر از DPC اجرا بشود، در نتیجه در انتخاب توابع باید دقت کنید که این توابع قابل اجرا در سطوح IRQL پایین تر از DPC نیز باشند.
مقدار دهی توابع مربوط به Fast I/O
بالاتر در مورد Fast I/O توضیح داده ام. در این مرحله باید عضو داده FastIoDispatch که یک ساختار از نوع FAST_IO_DISPATCH است و داخل ساختار DRIVER_OBJECT قرار دارد را مقدار دهی کنیم.
fastIoDispatch = ExAllocatePoolWithTag( NonPagedPool, sizeof( FAST_IO_DISPATCH ), SFLT_POOL_TAG ); if (!fastIoDispatch) { IoDeleteDevice( gSFilterControlDeviceObject ); return STATUS_INSUFFICIENT_RESOURCES; } RtlZeroMemory( fastIoDispatch, sizeof( FAST_IO_DISPATCH ) ); fastIoDispatch->SizeOfFastIoDispatch = sizeof( FAST_IO_DISPATCH ); fastIoDispatch->FastIoCheckIfPossible = SfFastIoCheckIfPossible; fastIoDispatch->FastIoRead = SfFastIoRead; ... ... DriverObject->FastIoDispatch = fastIoDispatch;
این کد یک حافظه Nonpaged به اندازه ساختار FAST_IO_DISPATCH با استفاده از تابع ExAllocatePoolWithTag تخصیص میکند. و جلوتر این حافظه را با توابعی که قبلا در فایل fastio.c تعریف کرده ایم مقداردهی می کنیم، و در نهایت این مغییر را درون DriverObject->FastIoDispatch قرار می دهیم.
همه توابعی که تعریف کرده ایم یک کار یکسان را انجام می دهند. کاری که این توابع انجام می دهند صدا زدن تابع FastIoDispatch مشابه در درایور پایین Stack خود است . من برای نمونه تابع SfFastIoRead را اینجا قرار داده ام.
BOOLEAN SfFastIoRead ( IN PFILE_OBJECT FileObject, IN PLARGE_INTEGER FileOffset, IN ULONG Length, IN BOOLEAN Wait, IN ULONG LockKey, OUT PVOID Buffer, OUT PIO_STATUS_BLOCK IoStatus, IN PDEVICE_OBJECT DeviceObject ) { PDEVICE_OBJECT nextDeviceObject; PFAST_IO_DISPATCH fastIoDispatch; PAGED_CODE(); if (DeviceObject->DeviceExtension) { ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject )); nextDeviceObject = ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension)->AttachedToDeviceObject; ASSERT(nextDeviceObject); fastIoDispatch = nextDeviceObject->DriverObject->FastIoDispatch; if (VALID_FAST_IO_DISPATCH_HANDLER( fastIoDispatch, FastIoRead )) { return (fastIoDispatch->FastIoRead)( FileObject, FileOffset, Length, Wait, LockKey, Buffer, IoStatus, nextDeviceObject ); } } return FALSE; }
مطلع شدن از Register/Unregister شدن Filesystem ها
بعد همه کارهایی که بالا گفتیم می رسیم به مرحله نهایی که مطلع شدن از Filesystem هایی که روی سیستم رجیستر شده اند یا بعد رجیستر می شوند. این کار را با تابع زیر انجام می دهیم.
status = IoRegisterFsRegistrationChange( DriverObject, SfFsNotification );
به طور دقیقتر ما یک تابع برای سیستم عامل تعریف می کنیم که اینجا تابع ما SfFsNotification است، که هر زمان یک درایور خود را به عنوان یک Filesystem با استفاده از تابع IoRegisterFileSystem رجیستر کند یا خود را با تابع IoUnregisterFileSystem حذف کند، تابعی که تعریف کرده ایم صدا زده می شود. همانطور که قبلا گفتم برای Filesystem هایی که قبلا رجیستر شده اند نیز تابع که تعریف کرده ایم صدا زده می شود.
نکته: استفاده از تابع IoRegisterFsRegistrationChange حتما باید بعد از تمام کارهایی باشد که بالاتر اشاره کردم.
مقدار دهی DriverUnload
مساله DriverUnload چیز جدیدی نیست و در آموزش های پیشین مثال هایی دیده ایم که چطور DriverObject->DriverUnload را مقدار دهی کنیم. اما در مورد فیلتر درایور یک نکته وجود دارد، برای فیلتر درایور مرسوم نیست که تابع DriverUnload تعریف بشود. اگر هم میخواهید تعریف کنید بهتر است فقط برای تست باشد و نه در یک محصول جدی. بر همین اساس کد درایور ما عبارت DBG را بررسی میکند اگر معتبر بود تابع DriverUnload مقدار دهی می شود، و اگر نه که هیچ اتفاقی نمی افتد. این کدی است که ما برای رسیدن به این مقصود نوشته ایم.
#if DBG gSFilterDriverObject->DriverUnload = DriverUnload; #endif
برای تعریف عبارت DBG هم کافی است این خط را به فایل sources اضافه کنیم که من به طور پیش فرض این مقدار را اضافه کرده ام.
C_DEFINES= $(C_DEFINES) -DDBG=1
بررسی کد این تابع را به عهده خودتان می گذارم . واضح است که باید عکس تمام کارهایی را که کرده ایم انجام دهیم. یعنی اول تابع Notification ای که روی سیستم ثبت کردیم برای مطلع شدن از Register/Unregister شدن یک Filesystem را از سیستم حذف کنیم. این کار با تابع IoUnregisterFsRegistrationChange انجام می شود. و شروع کنیم تمام Device Object هایی که Attach کرده ایم را Detach کنیم.
خواندن کلید رجیستری درایور
با اینکه در DriverEntry خواندن رجیستری در اول کد قرار گرفته با این حال من ترجیح دادم این قسمت را آخر توضیح بدهم به دلیل اینکه اینکار اختیاری است و لازم نیست یک فیلتر درایور حتما آن را داشته باشد. چون این قسمت روی اجرای درایور ما تاثیر می گذارد لازم است توضیحی در موردش بدهم.
این کدی است که داخل DriverEntry صدا زده می شود
SfReadDriverParameters( RegistryPath );
در این تابع ما چکار می کنیم؟ از کلید رجیستری زیر
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\sfilter
مقدار DebugFlags را می خوانیم، این مقدار را باید خودمان ایجاد کنیم و آن را با یک عدد مقدار دهی کنیم. که این مقدار از OR کردن مقادیر زیر بدست می آید.
#define SFDEBUG_DISPLAY_ATTACHMENT_NAMES 0x00000001 //display names of device objects we attach to #define SFDEBUG_DISPLAY_CREATE_NAMES 0x00000002 //get and display names during create #define SFDEBUG_GET_CREATE_NAMES 0x00000004 //get name (don't display) during create #define SFDEBUG_DO_CREATE_COMPLETION 0x00000008 //do create completion routine, don't get names
در جاهای مختلف کد بر اساس مقادیری که از کلید رجیسری ذکر شده است خروجی های DbgPrint کنترل می شود. اینطوری می توانیم بگوییم فقط Register/Unregter شدن Filesystem گزارش (در بخش های پایینی در این مورد توضیح داده ام) شود، یا فقط گزارش اینجا/باز کردن فایل ها را داشته باشیم، یا ترکیبی از این مقدار ها باشد (OR بشوند).
تحلیل کد تابع SfFsNotification
کل کاری که این تابع باید انجام دهد این است که بررسی کند آیا درایور مربوط به Filesystem می خواهد خود را Register کند یا Unregister. کد زیر نشان می دهد که چگونه این تابع را پیاده سازی کنیم.
VOID SfFsNotification ( IN PDEVICE_OBJECT DeviceObject, IN BOOLEAN FsActive ) { ... ... if (FsActive) { SfAttachToFileSystemDevice( DeviceObject, &name ); } else {
SfDetachFromFileSystemDevice( DeviceObject ); } }
تحلیل کد تابع SfAttachToFileSystemDevice
برای Attack شدن به Device Object یک درایور چه مراحلی باید انجام شوم؟
اول ایجاد یک Device Object
با تابع IoCreateDevice یک Device Object می سازیم، در واقع به ازای هر Filesystem ای که رجیستر می شود ما باید این کار را انجام دهیم. برای درک بهتر به شکلی که اول آموزش قرار دادم مراجعه کنید، Upper Filter که به رنگ قرمز مشخص شده در واقع همین Device Object ای است که اینجا می سازیم. البته در این مرحله هنوز به Stack مربوطه اضافه نشده است.
PDEVICE_OBJECT newDeviceObject;
...
...
status = IoCreateDevice( gSFilterDriverObject, sizeof( SFILTER_DEVICE_EXTENSION ), NULL, DeviceObject->DeviceType, 0, FALSE, &newDeviceObject ); if (!NT_SUCCESS( status )) { return status; }
ایجا در مورد پارامترها یکسری نکته وجود دارد.
پارامتر دوم DeviceExtensionSize: در آموزش های قبلی این پاراتر را صفر مقدار می دادیم اما این بار اندازه ساختاری به نام SFILTER_DEVICE_EXTENSION را داده ایم. این ساختاری است که درایور نویس میتواند تعریف کند و این تابع فقط حافظه ای برای این ساختار که ما اندازه آن را داده ایم تخصیص می کند. این حافظه از طریق DeviceObject->DeviceExtension قابل دسترسی است. هدف از وجود چنین ساختاری نگهداری یکسری اطلاعات است که درایور نویس میخواهد همیشه همراه یک Device Object باشد. پایین تر وقتی به Attach به یک Device Object برسیم این مساله را بیشتر شرح می دهم.
پارامر سوم DeviceName: در فیلتر درایور دیگر احتیاجی به تعریف نام (Nt Device Name) نداریم. در نتیجه NULL مقدار دهی میکنیم.
پارامتر چهارم DeviceType: نوع Device Object را هم از Device Object مربوط به Filesystem می گیریم.
دوم مقدار دهی Flags
در این مرحله عضو داده Flags مربوط به Device Object ای که ایجاد کرده ایم را با مقدادیر Flags مربوط به Filesystem رجیستر شده مقدار دهی می کنیم.
if ( FlagOn( DeviceObject->Flags, DO_BUFFERED_IO )) { SetFlag( newDeviceObject->Flags, DO_BUFFERED_IO ); } if ( FlagOn( DeviceObject->Flags, DO_DIRECT_IO )) { SetFlag( newDeviceObject->Flags, DO_DIRECT_IO ); } if ( FlagOn( DeviceObject->Characteristics, FILE_DEVICE_SECURE_OPEN ) ) { SetFlag( newDeviceObject->Characteristics, FILE_DEVICE_SECURE_OPEN ); }
سوم Attach شدن به Filesystem
در این مرحله به Stack مربوط به Filesystem اصطلاحا Attach می شویم. این تابع Stack مربوط به Filesystem ای که رجیستر شد را رو به بالا پیمایش می کند و Device Object ای را که ایجاد کرده بودیم را به بالاترین Device Object این Stack اضافه میکند.
devExt = newDeviceObject->DeviceExtension; status = IoAttachDeviceToDeviceStackSafe( newDeviceObject, DeviceObject, &devExt->AttachedToDeviceObject );
تابع بالا بصورت زیر تعریف شده
NTSTATUS IoAttachDeviceToDeviceStackSafe( _In_ PDEVICE_OBJECT SourceDevice, _In_ PDEVICE_OBJECT TargetDevice, _Out_ PDEVICE_OBJECT *AttachedToDeviceObject );
توضیح پارامترها:
SourceDevice: این پارامتر Device Object ای است که ما با IoCreateDevice بعد رجیستر شدن Filesystem ایجاد کردیم.
TargetDevice: این پارامتر Device Object مربوط به Filesystem است. در واقع این تابع از این گره روی Stack شروع به پیمایش می کند تا به بالای Stack برسد.
AttachedToDeviceObject: این پارامتر باید آدرس SourceDevice->DeviceExtension->AttachedToDeviceObject باشد. ما قبلا از طریق تابع IoCreateDevice اندازه ساختار DeviceExtension را داده بودیم و I/O Manager برای ما حافظه به اندازه ساختار مربوطه تخصیص کرده است. چون این ساختار را درایور نویس تعریف میکند هر عضو داده ای میتواند داشته باشد فقط در مورد فیلتر درایور ها این ساختار حتما باید عضو داده ای به نام AttachedToDeviceObject نیز داشته باشد. حالا این تابع چه مقداری داخل این عضو داده نگه می دارد؟ تابع بعد از پیمایش Stack آخرین Device Object ای که روی Stack پیدا کرده را در این عضو داده قرار می دهد. در واقع این Device Object درایور پایین تر ما می شود. در جاهای مختلف درایور خود ما لازم داریم درخواست Irp ای را به درایور پایین خود ارسال کنیم، در نتیجه لازم است Device Object درایور پایین خود را یک جا نگه داری کنیم.
این ساختار به صورت زیر در کد ما تعریف شده است.
typedef struct _SFILTER_DEVICE_EXTENSION { // // Pointer to the file system device object we are attached to // PDEVICE_OBJECT AttachedToDeviceObject; // // Pointer to the real (disk) device object that is associated with // the file system device object we are attached to // PDEVICE_OBJECT StorageStackDeviceObject; // // Name for this device. If attached to a Volume Device Object it is the // name of the physical disk drive. If attached to a Control Device // Object it is the name of the Control Device Object. // UNICODE_STRING DeviceName; // // Buffer used to hold the above unicode strings // WCHAR DeviceNameBuffer[MAX_DEVNAME_LENGTH]; } SFILTER_DEVICE_EXTENSION, *PSFILTER_DEVICE_EXTENSION;
پاک کردن فلگ DO_DEVICE_INITIALIZING
بعد از Attach شدن به Stack لازم است فلگ DO_DEVICE_INITIALIZING از روی Device Object پاک شود. این فلگ را تابع IoCreateDevice روی DeviceObject جدید مقدار دهی می کند. اگر این فلگ بعد از Attach شدن پاک نشود دیگر هیچ درایوری نمی تواند به Stack مربوط به Filesystem ذکر شده Attach شود (اجرای تابع IoAttachDeviceToDeviceStackSafe ناموفق خواهد بود)
برای حل این مساله از کد زیر استفاده کرده ایم.
ClearFlag( newDeviceObject->Flags, DO_DEVICE_INITIALIZING )
Attach شدن به Volume های یک Filesystem
بعد از Attach شدن به Filesystem حالا نوبت به Attach شدن به Volume های Filesystem مربوطه می شود. تابعی تعریف کرده ایم به نام SfEnumerateFileSystemVolumes که این کار را برای ما انجام می دهد.
status = SfEnumerateFileSystemVolumes( DeviceObject, &fsName ); if (!NT_SUCCESS( status )) { IoDetachDevice( devExt->AttachedToDeviceObject ); goto ErrorCleanupDevice; }
تحلیل کد تابع SfEnumerateFileSystemVolumes
این تابع کار اصلیش این است که تمام Device Object هایی که درایور مربوط به Filesystem مذکور ایجاد کرده استخراج کند و بر اساس یکسری شرایط آن هایی که مورد نظر ما هستند به آنها Attach شود.
مراحلی که این تابع انجام می دهد
اول استخراج تمام Device Object های Filesystem
در دو مرحله تابع IoEnumerateDeviceObjectList ار اجرا می کنیم تا لیست Device Object ها بدست آید. این دو بار اجرا کردن به این دلیل است که بار اول تعداد Device Object ها بدست می آید، حافظه تخصیص می شود، و بار دوم اطلاعات مربوط به Device Object ها گرفته می شود.
/* اجرای بار اول برای گرفتن تعداد آبجکت ها */
status = IoEnumerateDeviceObjectList ( FSDeviceObject->DriverObject, NULL, 0, &numDevices); if (!NT_SUCCESS( status )) { ASSERT(STATUS_BUFFER_TOO_SMALL == status); numDevices += 8; //grab a few extra slots
/* تحصیص حافظه */ devList = ExAllocatePoolWithTag( NonPagedPool, (numDevices * sizeof(PDEVICE_OBJECT)), SFLT_POOL_TAG ); if (NULL == devList) { return STATUS_INSUFFICIENT_RESOURCES; }
/* گرفتن اطلاعات از آبجکت ها */ status = IoEnumerateDeviceObjectList ( FSDeviceObject->DriverObject, devList, (numDevices * sizeof(PDEVICE_OBJECT)), &numDevices); if (!NT_SUCCESS( status )) { ExFreePool( devList ); return status; } ... ... }
بعد گرفتن این اطلاعات داخل یک حلقه این Device Object ها بررسی می شوند اگر با یکسری شرایط مطابق بودند بهآن Attach می شویم
شرایط بررسی Device Object ها
اول) بررسی می کنیم در صورت برقرار بودن شرایط زیر از Attach شدن به Device Object صرف نظر می کنیم
- Device Object برابر Device Object مربوط به Filesystem مذکور باشد،
- نوع Device Object با نوع Device Object مربوط به Filesystem مطابق نباشد.
- بررسی میکند که قبلا به Stack مربوط به Device Object گرفته شده Attach شده ایم یا خیر.
if ((devList[i] == FSDeviceObject) || (devList[i]->DeviceType != FSDeviceObject->DeviceType) || SfIsAttachedToDevice( devList[i], NULL )) { leave; }
دوم) اینجا یک تابع استفاده کرده ایم که خروجی به ما یک رشته بر می گرداند. اگر رشته مقدار داشت (طولش بزرگتر از صفر بود) از Attack شدن به Device Object صرف نظر میکنیم، چون Object Device مربوط به Volume که اینجا Enumerate شده قالبا بی نام هستند. حالا تابع SfGetBaseDeviceObjectName چکار میکند؟ اول بگم به سورس کد رجوع کنید و کد کامل این تابع را ببینید. این تابع اول، با استفاده از تابع IoGetDeviceAttachmentBaseRef پایینترین Device Object در Stack مربوط به Device Object ای که Enumerate کردیم را به ما بر می گرداند. (در خیلی از موارد همین Device Object پایین ترین گره در Stack است). دوم، تابع SfGetObjectName را اجرا میکند که این تابع نیز ObQueryNameString را اجرا میکند تا نام Object را بدست آورد. انتهای همین آموزش بخش "کار با WinDbg - قسمت دوم" نشان داده ام با فرمان های WinDbg چطور نام یک Object را بدست آورید.
SfGetBaseDeviceObjectName( devList[i], Name ); if (Name->Length > 0) { leave; }
سوم) بررسی میکنیم اگر هیچ Disk Device Object با این Device Object مرتبط نیست از Attach شدن به Device Object صرف نظر میکنیم. در صورت وجود Disk Device Object این مقدار داخل storageStackDeviceObject قرار می گیرد. به انتهای آموزش بخش "کار با WinDbg - قسمت سوم" بروید تا ببینید دقیقا این تابع چه ساختارهایی را و در نهایت چه مقداری به عنوان Disk Device Object بر می گرداند.
status = IoGetDiskDeviceObject (devList[i], &storageStackDeviceObject ); if (!NT_SUCCESS( status )) { leave; }
دوم Attach شدن به Volume
در نهایت برای Attach شدن به Volume تابع SfAttachToMountedDevice را صدا می زنیم. Attach شدن به یک Volume مانند Attach شدن به Filesystem است که بالا توضیح داده ام. فقط این تابع سعی میکند در ۸ مرتبه اقدام به Attach شدن به Volume کند. چرا؟ چون Mount شدن یک Volume روی یک سیستم کمی زمان می برد.
تحلیل کد تابع SfDetachFromFileSystemDevice
در بخش های قبل تر بررسی کردیم اگر یک Filesystem روی سیستم Register شد چه کارهایی باید انجام دهیم. حالا اگر یک Filesystem از روی سیستم Unregister شود چه باید کنیم؟
در این حالات کار ما خیلی ساده تر است. فقط کافیه از Device Object ای که داریم شروع کنیم به پیمایش و هر جا به Device Object مربوط به درایور خود رسیدیم. تابع IoDetachDevice اجرا و بعد Device Object را نیز با IoDeleteDevice پاک کنیم.
VOID SfDetachFromFileSystemDevice ( IN PDEVICE_OBJECT DeviceObject ) { PDEVICE_OBJECT ourAttachedDevice; PSFILTER_DEVICE_EXTENSION devExt; PAGED_CODE(); ourAttachedDevice = DeviceObject->AttachedDevice; while (NULL != ourAttachedDevice) { if (IS_MY_DEVICE_OBJECT( ourAttachedDevice )) { devExt = ourAttachedDevice->DeviceExtension; ... ... SfCleanupMountedDevice( ourAttachedDevice ); IoDetachDevice( DeviceObject ); IoDeleteDevice( ourAttachedDevice ); return; } DeviceObject = ourAttachedDevice; ourAttachedDevice = ourAttachedDevice->AttachedDevice; } }
کار با WinDbg
قسمت اول
لیست گرفتن Filesystem های رجیستر شده روی سیستم
kd> !object \FileSystem Object: 88e4d260 Type: (84f446a0) Directory ObjectHeader: 88e4d248 (new version) HandleCount: 0 PointerCount: 29 Directory Object: 88e05ed0 Name: FileSystem Hash Address Type Name ---- ------- ---- ---- 00 86b33388 Driver srvnet 8518e670 Driver Ntfs 01 85ab58e8 Driver NetBIOS
...
...
گرفتن اطلاعات از Driver Object مربوط به Ntfs،
kd> !drvobj 8518e670 Driver object (8518e670) is for: \FileSystem\Ntfs Driver Extension List: (id , addr) Device Object list: 85af6020 85a28020 8519b640
خواندن Device Stack یکی از Device Object های Ntfs که از فرمان بالا به دست آمد می گیریم
kd> !devstack 8519b640 !DevObj !DrvObj !DevExt ObjectName 85384620 \Driver\LiveKd 853846d8 856a4d20 \FileSystem\FltMgr 856a4dd8 > 8519b640 \FileSystem\Ntfs 00000000 Ntfs
خواندن Device Stack مربوط به Device Object فایل سیستم Ntfs بعد از نصب فیلتر درایوری که قرار است بنویسیم. فیلتر درایور ما با نام sfilter در بالای stack قرار گرفته. در این مرحله می توانید این نتایج را با تصویری که از Device Stack مروبط به Filesystem در ابتدای آموزش ارائه دادیم مطابقت دهید.
kd> !devstack 8519b640 !DevObj !DrvObj !DevExt ObjectName 86542e78 \Driver\sfilter 86542f30 85384620 \Driver\LiveKd 853846d8 856a4d20 \FileSystem\FltMgr 856a4dd8 > 8519b640 \FileSystem\Ntfs 00000000 Ntfs
قسمت دوم
در قسمت اول همین بخش نشان دادم چطور Object Device های درایور Ntfs را بگیریم. حالا در این قسمت می خواهیم نام مروبط به این Object ها را بدست بیاوریم.
یک بار دیگه اطلاعات در مورد درایور Ntfs را می گیریم. (آدرس Device Object ها با قسمت اول فرق دارد که طبیعی است چون در هر بار روشن خاموش شدن سیستم این آدرس ها تغییر می کند)
lkd> !drvobj 85396670 Driver object (85396670) is for: \FileSystem\Ntfs Driver Extension List: (id , addr) Device Object list: 85cf7020 85c01020 85b4c460
اول اینکه خودتون Stack هر سه Object را بگیرید. می بینید که هر سه در پایین ترین بخش Stack قرار دارند.
حالا با دستورات زیر اطلاعات در مورد Object ها می گیریم. دستور !object
بطور عمومی برای گرفتن اطلاعات از هر نوع Object ای است. ما این اطلاعات را با دستور !devobj
نیز می توانستیم بگیریم که مخصوص Object های از نوع Device بود. دو Object اول بی نام هستند و فقط Object سوم نام دارد که Ntfs است. (نکته: این نام مربوط به یک Device Object است و با درایور Ntfs فرق دارد)
lkd> !object 85cf7020 Object: 85cf7020 Type: (851e7a38) Device ObjectHeader: 85cf7008 (new version) HandleCount: 0 PointerCount: 1
lkd> !object 85c01020 Object: 85c01020 Type: (851e7a38) Device ObjectHeader: 85c01008 (new version) HandleCount: 0 PointerCount: 1
lkd> !object 85b4c460 Object: 85b4c460 Type: (851e7a38) Device ObjectHeader: 85b4c448 (new version) HandleCount: 0 PointerCount: 2 Directory Object: 89005ed0 Name: Ntfs
قسمت سوم
هدف از این بخش این است که نشان بدهیم تابع IoGetDiskDeviceObject چطور عمل میکند. کاری این تابع انجام می دهد خواندن یک ساختار به نام VPB است. این مفهوم مربوط می شود به مساله Volume Manager و Mount Manager در ویندوز که کلا مباحثی جدا از موضوع این آموزش است. در نتیجه اینجا من فقط یکسری دستور نشانتان می دهم که این تابع دقیقا چه مراحلی طی می کند و چه مقداری را بر میگرداند.
در قسمت دوم سه Device Object از درایور Ntfs بدست آمد. من آدرس Device Object اول را برای این مثال آماده کرده ام.
اول) گرفتن اطلاعات از ساختار DEVICE_OBJECT
lkd> dt _DEVICE_OBJECT 85cf7020 nt!_DEVICE_OBJECT +0x000 Type : 0n3 +0x002 Size : 0xf90 +0x004 ReferenceCount : 0n0 +0x008 DriverObject : 0x85396670 _DRIVER_OBJECT +0x00c NextDevice : 0x85c01020 _DEVICE_OBJECT +0x010 AttachedDevice : 0x85cf2a18 _DEVICE_OBJECT +0x014 CurrentIrp : (null) +0x018 Timer : (null) +0x01c Flags : 0x40000 +0x020 Characteristics : 0 +0x024 Vpb : (null) +0x028 DeviceExtension : 0x85cf70d8 Void +0x02c DeviceType : 8 +0x030 StackSize : 8 '' +0x034 Queue : +0x05c AlignmentRequirement : 0 +0x060 DeviceQueue : _KDEVICE_QUEUE +0x074 Dpc : _KDPC +0x094 ActiveThreadCount : 0 +0x098 SecurityDescriptor : (null) +0x09c DeviceLock : _KEVENT +0x0ac SectorSize : 0x200 +0x0ae Spare1 : 1 +0x0b0 DeviceObjectExtension : 0x85cf7fb0 _DEVOBJ_EXTENSION +0x0b4 Reserved : (null)
دوم) خواندن ساختار DEVOBJ_EXTENSION از ساختار قبلی
lkd> dt _DEVOBJ_EXTENSION 0x85cf7fb0 nt!_DEVOBJ_EXTENSION +0x000 Type : 0n13 +0x002 Size : 0 +0x004 DeviceObject : 0x85cf7020 _DEVICE_OBJECT +0x008 PowerFlags : 0 +0x00c Dope : (null) +0x010 ExtensionFlags : 0x800 +0x014 DeviceNode : (null) +0x018 AttachedTo : (null) +0x01c StartIoCount : 0n0 +0x020 StartIoKey : 0n0 +0x024 StartIoFlags : 0 +0x028 Vpb : 0x85bbe408 _VPB +0x02c DependentList : _LIST_ENTRY [ 0x85cf7fdc - 0x85cf7fdc ] +0x034 ProviderList : _LIST_ENTRY [ 0x85cf7fe4 - 0x85cf7fe4 ]
سوم) خواندن ساختار VPB که مخفف Volume Parameter Block است از ساختار قبلی
lkd> dt _VPB 0x85bbe408 nt!_VPB +0x000 Type : 0n10 +0x002 Size : 0n88 +0x004 Flags : 1 +0x006 VolumeLabelLength : 0x1e +0x008 DeviceObject : 0x85cf7020 _DEVICE_OBJECT +0x00c RealDevice : 0x85bbfe20 _DEVICE_OBJECT +0x010 SerialNumber : 0x6e3267e9 +0x014 ReferenceCount : 0x16 +0x018 VolumeLabel : [32] "System Reserved"
تابع مذکور بعد از چک کردن یکسری از عضو داده، عضو داده RealDevice را بر می گرداند. این Device Object در واقع توسط درایور دیگری ایجاد شده و برعکس Device Object ای که بالاتر Enumerate شده بود و بی نام بود، این Device Object دارای نام می باشد که با دستور !devobj
به راحتی به این اطلاعات می توانیم بررسیم.
lkd> !devobj 0x85bbfe20 Device object (85bbfe20) is for: HarddiskVolume1 \Driver\volmgr DriverObject 85ad7328 Current Irp 00000000 RefCount 22 Type 00000007 Flags 00203050 Vpb 85bbe408 Dacl 891a947c DevExt 85bbfed8 DevObjExt 85bbffc0 Dope 85b9b9b0 DevNode 85bccd78 ExtensionFlags (0x00000800) Unknown flags 0x00000800 AttachedDevice (Upper) 85bcc880 \Driver\fvevol Device queue is not busy.
از اطلاعات بدست آمده می توانیم بفهمیم که اولا نام Device Object برابر است با HarddiskVolume1 و توسط درایور volmgr ایجاد شده است.
ابزارهای دیگر
آیا در مورد گرفتن اطلاعات از Stack مربوط به Device Object ها ابزاری ساده تر از WinDbg وجود دارد. بله خوشبختانه . یکی از این ابزارها VrtuleTree نام دارد و کار باهاش خیلی ساده است. تصویری از این برنامه که Device Stack همه درایور های روی سیستم را در آورده است. (برای نمایش بزرگتر تصویر روی آن کلیک کنید)
یه پیغام خصوصی هم واست فرستادم چک بکن..