Press "Enter" to skip to content

تحلیل درایور آموزشی HackSys Extreme Vulnerable Windows Driver

سلام،

HackSys Extreme Vulnerable Driver که به اختصار HEVD صداش میکنیم یک درایور برای ویندوز هست که از قصد آسیب پذیر به باگ های زیر است:

Double Fetch
Pool Overflow
Use After Free
Type Confusion
Stack Overflow
Integer Overflow
Stack Overflow GS
Arbitrary Overwrite
Null Pointer Dereference
Uninitialized Heap Variable
Uninitialized Stack Variable
Insecure Kernel Resource Access

طراحی شده برای برنامه نویس ها، محقق های امنیتی برای اکسپلویت در سطح کرنل.. اطلاعات بیشتر میتونید از زبان خود Ansari برنامه نویس این پروژه بخونید که در کنفرانس Black Hat Arsenal 2016 مطرح شده است.

لینک سلایدها

لینک مقاله

من یک مقاله آماده کردم با عنوان “تحلیل درایور آموزشی Hacksys Extreme Vulnerable Driver” که به شرح زیر است.

تحلیل درایور آموزشی Hacksys Extreme Vulnerable Driver

خلاصه:

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

  • کامپایل، build و اجرای درایور
  • آماده سازی محیط برای دیباگ درایور
  • ارتباط برنامه های سطح کاربر با درایور
  • تحلیل کدهای درایور HEVD
  • ارتباط با تابع درایور

 

کامپایل، build و اجرای درایور

خب برای نوشتن یا کامپایل درایور در ویندوز شما نیاز به بسته ی WDK یا Windows Driver Kit از ماکروسافت دارید که میتونید از سایت ماکروسافت بسته ی مربوط به نسخه ی ویندوز خود را دانلود کنید و نصب کنید، سورس کد درایور که قرار است روی آن کار کنیم دانلود کنید:

https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

برای کامپایل و بیلد کردن درایور راه ساده تر استفاده از اسکریپتی که در پوشه Builder قرار دارد است اگر سیستم عامل شما از نسخه ی x86 استفاده میکند اسکریپت Build_HEVD_Vulnerable_x86.bat را اجرا کنید البته باید مقدار متغیر localSymbolServerPath را تغییر دهید. همچنین میتوانید پروژه داخل پوشه Driver را با ویژوال استدیو باز کنید، اگر همه چیز درست پیش رفته باشد پنجره ی Solution Explorer  شامل همه کدهای داخل پوشه Driver است:

حال شما میتوانید پروژه را بیلد کنید.

 

چطور میتونم این درایور و اجرا کنم؟

برای اجرای درایور در این آموزش من از سیستم عامل ویندوز Xp sp3 استفاده میکنم. چرا که در سیستم عامل های ۶۴ بیتی جدید ماکروسافت که از ویستا به بعد انتشار پیدا کردند، مکانیسمی مبنی بر ثبت درایورهای جانبی به سیستم عامل افزوده شده است. بدین معنی که شما نمیتوانید در سیستم عامل های جدید به سادگی درایور جانبی بارگذاری و سپس اجرا کنید. مگر اینکه درایور را قبلا به صورت دیجیتالی در سیستم ثبت کرده باشید (درایور دارای امضا دیجیتالی باشد). خب داریور تمرینی ما فاقد امضا است در نتیجه از سیستم عامل Xp استفاده میکنم.

حالا چطوری درایور و بارگذاری کنیم؟

میتونیم با استفاده از توابع API به برنامه کوچک برای بارگذاری درایور بنویسیم اما ترجیح میدم که از ابزار آماده ای به نام OsrLoader استفاده کنم که میتونید از وبسایت osronline.com دریافت کنید.

برای بارگذاری درایور فقط کافی هست مسیر درایور که قرار است بارگذاری شود را به فیلد Driver Path بدید سپس دکمه ی Register Service و سپس Start Service را انتخاب کنید اگر همه چیز درست پیش رود با پیغام مبتی بر موفق بودن عملیات مواجه خواهید شد.

آماده سازی محیط برای دیباگ درایور

برای دیباگ درایور در سطح کرنل ما نیاز به یک ماشین مجازی مثل VMware یا Virtualbox  که بروی آن ویندوز Xp قرا دارد و یک دیباگر سطح کرنل داریم که از Windbg استفاده خواهیم کرد.

برای دیباگ سطح کرنل یک درایور لازم است که ماشین مجازی با دیباگر را لینک کنیم و همچنین قابلیت Debug را در سیستم عامل هدف فعال کنیم به این منظور ویندوز Xp را که در ماشین مجازی قرار دارد را اجرا کنید.

پس از بالا آمدن ویندوز XP از طریق Run فایل System Configuration Utility  یا msconfig را اجرا کنید و به تب BOOT.ini مراجعه کنید و بروی دکمه Advanced Options کلیک کنید و مقدار زیر را وارد کنید و بروی دکمه ی OK کلیک کنید

برای لینک کرن سیستم مجازی با دیباگر راه های مختلفی وجود دارد که ما قصد داریم از پورت ها به این منظور استفاده کنیم.

ویندوز روی ماشین مجازی را Shutdown کنید و از گزینه ی Edit virtual machine setting دستگاه مربوط به Printer را Remove کنید سپس از با زدن دکمه ی Add یک پورت سریال را اضافه کنید

در قسمت بعد نصب پورت گزینه ی Output to named pipe استفاده کنید و در مرحله ی آخر نام Named pipe را انتخاب کنید البته فرمت پیشرفض نام که یک Namespace میباشد را تغییر ندهید نهایتا فقط اسم پورت را تغییر دهید مثلا com_3 یا… البته نام پورت با نامی که در فایل BOOT.ini تعیرف کردید باید یکی باشد.

خب ماشین مجازی را در همین حال رها کنید و دیباگر Windbg را اجرا کنید و از منوی File را گزینه ی Kernel Debug.. را انتخاب کنید و سپس تب COM انتخاب کنید و اطلاعات درخواست شده را همانند تنظیماتی که در بالا انجام داده اید، تنظیم کنید

پس از فشردن دکمه ی OK دیباگر منتظر اتصال پورت سریال میماند:

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

 

ارتباط برنامه های سطح کاربر با درایور

ارتباط یوزر مد با درایور به چند طریق امکان پذیر است:

برنامه های نوشتن شده در سطح کاربر با استفاده از تابع DeviceIoControl در فایل Kernel32.dll میتوانند دادهای مورد نظر را به درایور ارسال میکند. رابط برنامه نویسی DeviceIoControl تنها تابعی نیست که میتواند از فضای کاربر به درایورهای کرنل داده ارسال کند. توابع کار با IO مانند ReadFile، WriteFile ،CreateFile و CloseFile دیگر توابع ای هستند که میتوانند این کار را انجام دهند. البته برنامه ها با Device (دستگاه ها) نه درایورها در تعامل هستند پس ابتدا باید هندل دستگاه درایورِ مورد نظر یعنی HacksysExtremeVulnerableDriver یا به اختصار (HEVD) را بدست آوریم.

برای بدست اوردن هندل میتونید از تابع CreateFile استفاده کنیم، راهنمای MSDN در رابطه با این تابع نوشته است:

خروجی تابع هندل ی است که می تواند برای دسترسی به فایل یا دستگاه برای انواع مختلف  I/Oاستفاده شود.

تابع DeviceIoControl کدهای IOCTL را به دستگاه ارسال میکند در این مورد در قسمت بعدی یعنی تحلیل کد درایور بررسی میکنیم..

 

“تحلیل کدهای درایور HEVD

برای تحلیل کدهای درایور ما از دیس اسمبلر IDA استفاده میکنیم پس فایل HEVD.sys را که در قسمت اول build کردیم را در IDA بارگذاری کنید توجه داشته باشید که فایل HEVD.pdb را در کنار فایل درایور قرار داشته باشید که اطلاعات دیباگ درایور را داشته باشیم. پس از بارگذاری درایور در IDA با این صفحه مواجع میشوید:

قسمتی که هایلایت شده پرش به تابع DriverEntry است که در دایورها DriverEntry حکم EntryPoint را که محل شروع برنامه است را ایفا میکند همانند فایل های dynamic load library که از تابع DllMain استفاده میکند یا تابع Main در زبان سی.

پس از بارگذاری شدن درایور اولین تابعی که اجرا میشود تابع GsDriverEntry است در نتیجه برای شروع تحلیل کدهای درایور بروی قسمت هایلایت شده یعنی

دوبار کلیک کنید. در ابتدای کد نوشته شده است:

این تابع دو پارامتر دارد.

  • پارامتر اول DriverObject است که ساختاری است که به مقادیر متعددی اشاره میکند که یکی از آنها اشاره به تابع DriverUnload است این مقداردر هنگام Unload شدن درایور صدا زده می شود.
  • پارامتر دوم RegistryPath است، این پارامتر مسیر درایور ما در رجیستری ثبت شده را در خود دارد.

در انتهای روتین یک شرط وجود دارد که اگر درست اجرا شود به روتین بعدی منتقل میشود که اطلاعات مفیدی در آن قرار دارد:

تابع RtlInitUnicodeString برای ایجاد رشته های کرنل استفاده میشود که پارامتر دوم این تابع یک رشته wide character string است که به یک کاراکتر NULL از UNICODE_STRING ایجاد شده است. کرنل ویندوز از یک ساختمان UNICODE_STRING استفاده میکند که از رشته های wide character یا کاراکتر گسترده در فضای کاربر متفاوت است.

در تصویر بالا پارامتر دوم اولین فراخوانی تابع RtlInitUnicodeString در متغیری به نام aDeviceHacksyse ذخیره شده است که همانطور که از نام متغیر پیداست یک Device Name یا NT Device Name است که برابر است با:

 

دومین فراخوانی تابع RtlInitUnicodeString در متغیر دیگری به نام aDosdevicesHack ذخیره شده است که شامل DOS Device Name است و برابر است با :

 NT Device Name وDOS Device Name :

در ویندوز برای دسترسی به یکسری شی ها یا دستگاه ها از API های مربوط به فایل استفاده می شود. اگر قبلا با C‎‎‎ ‎‎‎‎‎‎‎‎‎‎‎‎ و API های ویندوز کد سیستمی نوشته باشید احتمالا می دانید که با تابعی مثل CreateFile اول یک هندل از دستگاه مربوطه می گرفیتم بعد با توابعی مثل ReadFile یا WriteFile داده می خواندیم و می نوشتیم.  در واقع برنامه سطح کاربر و درایور های سطح کرنل دیگر دسترسی مستقیم به درایور شما ندارند و برای اینکه دیگران بتوانند به درایور شما درخواستی بفرستند اصطلاحا باید یک Device Name  بسازید این Device Name خود دو نوع است:

NT Device Name که این نام فقط قابل دسترس برای دیگر درایورها و کدهای سطح کرنل است و برنامه های سطح کاربر به این نام دسترسی ندارند. این نام ها با عبارت \Device شروع می شوند.

DOS Device Nameاین نام امکان دسترسی برنامه های سطح کاربر را مهیا می کند. این نامها هم با عبارت \DosDevices شروع می شوند.

در نتیجه ما برای ارتباط  با درایور HEVD باید از نام NT Device Name که شامل مقدار زیر است استفاده کنیم.

در نهایت این روتین که در خط آخر عکس بالا مشاهده میکنید تابعی با نام IoCreateDevice فراخوانی شده است. این تابع یک device object ایجاد میکند که شامل NT Device Name است.

اگر در زمان بارگذاری درایور همه چیز به درستی پیش رفته باشد پس از چک کردن خطاهای احتمالی نهایتا به روتین زیر میرسیم

در این روتین یک فراخوانی وجود دارد که مربوط به تابع IoCreateSymbolicLink است که رشته حاوی نام DOS Device Name و Device Name جزو پارامترهای تابع هستند این دو با هم توسط تابعی به نام IoCreateSymbolicLink متصل می شوند. این کار حتما باید انجام شود یعنی باید نام NT شما ایجاد شده باشد و چون برنامه سطح کاربر دسترسی به این نام ندارد نیاز است نامی تعریف کنیم که در سطح کاربر قابل دسترسی باشد این کدی است که این نام ها را ایجاد میکند و نام های Symbolic Link با عبارت\\.\  شروع می شوند.

خب وقت آن رسیده است که از دیباگر Windbg برای ادامه کار استفاده کنیم. یک نقطه ی توقف یا BreakPoint ایجاد کنید تا بتوانیم دستورات مورد نظر را اجرا کنیم.

دستور lm تمامی ماژول های بارگذاری شده بروی ویندوز Xp ما را نمایش میدهد:

خب حال نیاز است که درایور HEVD را در ویندوز بارگذاری کنیم در نتیجه با دستور g دیباگر را متوقف میسازیم سپس از منوی View گزینه Verbose Output را فعال کنید در این حالت اگر درایوری بروی ویندوز Xp ما بارگذاری شود ما اطلاعات درایور را در دیباگر مشاهده میکنیم. خب سراغ برنامه ی OSR Loader میرویم و درایور HEVD را در ویندوز Xp بارگذاری میکنیم. اگر همه چیز به درستی پیش رفته باشد اطلاعات داریور HEVD در دیباگر نمایش داده میشود.

حال برای بدست آوردن اطلاعات از درایور مورد نظر یک bp (نقطه توفق) ایجاد کنید. برای پیدا کردن شی درایور (Driver Object) از دستور !drvobj استفاده میکنیم که پارامتر این دستور نام دایور است خب در تصویر بالا نام درایور ما HEVD است پس دستور به صورت کامل به این صورت است:

Driver Object  چیست؟

هر درایوری که در سیستم لود شده باشد از دید سیستم عامل یک Driver Object است در واقع در سیستم عامل ویندوز خیلی چیزها مثل فایل، پوشه، فرایند،‌ نخ و… را به صورت Object می بیند. هر کدام از این آبجکتها برای خود ساختار مشخصی دارند. برای آبجکت درایور در ویندوز ما ساختاری به نام _DRIVER_OBJECT داریم.

خروجی دستور (عکس بالا) شامل آدرس شی است که به وسیله دستور dt میتوانیم به ساختار آن نگاهی بیاندازیم، دستور به این صورت است:

تابع راه انداز اولیه هنگامی که درایور بارگذاری میشود در آدرس ۰xedfe91b6 (که مقدار فیلد DriverInit است) قرار دارد اگر بیاد داشته باشید نام تابع GsDriverEntry در اول آموزش به عنوان نقطه ورود (EntryPoint) داریور بود. نکته ی مهم دیگر مقدار فیلد MajorFunction است.

MajorFunction چیست؟

هر درایور برای اینکه با دنیای بیرون ارتباط داشته باشد (این ارتباط می تواند از طرف درایورهای دیگر یا از طرف برنامه های سطح کاربر باش) ملزم است آرایه MajorFunciton را مقدار دهی کند. بسته به نوع و کارکرد درایور شما مقداردهی این آرایه متفاوت خواهد بود، اگر دقت کنید همه این تعاریف با عبارت IRP شروع می شوند IRP مختصر شده I/O Request Packet است. تمام درخواست هایی که به درایور میرسد به بصورت یک ساختار IRP است.
برنامه های سطح کاربر با فراخوانی یکسری API ها می توانند درخواست خود را به درایور بفرستند. و تابع مربوط به درخواست در درایور اجرا شود در پایین لیست API و در مقابل در خواستی که تولید می شود نشان میدهم:

خب برای توضیح بهتر MajorFunction مجبور هستیم به سورس کد درایور نگاهی بیاندازیم، فایل HackSysExtremeVulnerableDriver.c در پوشه Driver را باز کنید در خط ۱۰۷ کد زیر نوشته شده است:

در این درایور سه درخواست کنترل می شود که دو تا از این درخواست ها توسط یک تابع بررسی می شود. یکسری از درخواست ها هستند که کنترل برخی درخواست ها برایشان اجباری است. در واقع وقتی سیستم عامل درخواستی که مربوط به درایور ما می شود، برای ما میفرستد ( یا در واقع Major Function مربوط به آن درخواست را صدا می زند) انتظار دارد به این درخواست پاسخی از طرف درایور به سیستم عامل داده شود.

تابع  IrpCreateCloseHandlerوظیفه کنترل درخواست های IRP_MH_CREATE و IRP_MJ_CLOSE  را دارد. کد مربوط به این تابع را در زیر می بینید:

اگر به کد دقت کنید می بینید تقریبا کاری انجام نمی دهد. در واقع به خاطر اجباری بودن پاسخ به این درخواست مجبوریم به این صورت عمل کنیم. به صورت کلی این کد به سیستم عامل می گوید که اولا درخواست مربوطه توسط درایور بررسی شد و خطایی رخ نداد، با این کد:

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

حالا میرسیم به تابع IrpDeviceIoCtlHandler که می شود گفت مهمترین قسمت از کد این درایور است در این تابع درخواستی که از سمت کاربر می آید بررسی می شود و متناسب با در خواست کاری را انجام می دهد:

مقدارهایی که برای هر دستور case مشخص شده فرمان هایی است که از طرف کاربر می آید.

به دیباگر برگردیم، پس در نتیجه نقطه ی ورودی تابع MajorFunction در این ساختار یک اشاره گر به اولین آیتم از جدول MajorFunction است. این جدول توابع در هر سطر ایندکس دارد. هر ایندکس یک نوع متفاوتی از درخواست ها را نمایش می دهد. شایان ذکر است، ایندکس ها در فایل wdm.h با IRP_MJ_ شروع میشوند. به عنوان مثال، هنگامی که ما قصد درایم متوجه شویم هنگامی که یک برنامه در فضای کاربر DeviceIoControl را فراخوانی میکند، کدام آفست در جدول فراخوانی میشود، باید به ایندکس IRP_MJ_DEVICE_CONTROL نگاه کنیم. در این حالت، IRP_MJ_DEVICE_CONTROL یک مقدار ۰xe دارد و جدول MajorFunction در آفست ۰x038 از شروع شی درایور آغاز میشود. برای شناسایی تابعی که درخواست DeviceIoControl را کنترل میکند از فرمان زیر استفاده کنید:

در دستور بالا، آدرس ۰x038 آفست شروع جدول است و آفست ۰xe ایندکس IRP_MJ_DEVICE_CONTROL است که با مقدار عددی چهار ضرب میشود زیرا هر اشاره گر چهار بایت است، در پایان پارامتر L1 مشخص میکند که ما فقط میخواهیم یک خروجی DWORD مشاهده کنیم. دستور قبل نشان می دهد که تابع فراخوانی شده در فضای کرنل در آدرس edfe7150 است همچنین ما میتوانیم با استفاده فرمان u بررسی میکنیم آیا دستورالعمل ها در آن آدرس معتبر هستند یا خیر، در عکس پایین مشاهده میکنید دستور العمل های موجود معتبر هستند، اما اگر معتبر نباشید، معنی آن این است که ما در محاسبه آدرس اشتباه کرده ایم.

اگر محاسبات اشتبا باشد با تصویر زیر رو به رو میشوید:

پس متوجه شدیم تابعی که درخواست DeviceIoControl را کنترل میکند IrpDeviceIoCtlHandler است که پیشتر در سورس کد درایور با آن مواجه شدیم که دارای یک دستور case بود که بنا به هر درخواست کاربر یک تابع مجزا را صدا میزد.

خب زمان آن رسیده است که نگاهی به تابع IrpDeviceIoCtlHandler در دیس اسمبلر IDA بیاندازیم.

تابع KeGetCurrentIrql که بدون پارامتر هست IRQL ها فعلی را بازمیگرداند. اگه شرط آخر روتین بالا درست باشید به روتین زیر میرویم دراین روتین تابع IoGetCurrentIrpStackLocation را مشاهده میکنیم که به عنوان پارامتر متغیر irp را دریافت میکند. خروجی تابع یک اشاره گر به ساختار IO_STACK_LOCATION است که بروی متغیر IrpSp ذخیره میشود.

در حالت کلی کدهای تصویر بالا برای گرفتن کد IOCTL که از سمت برنامه کاربر ارسال میشود است. اگر تابع IoGetCurrentIrpStackLocation با موفقیت اجرا شود نتیجه آن میبایست مقداری به جز صفر باشد یعنی روتین بعدی در واقع باید جایی باشد که شرط نادرست باشد به آن پرش کند. در نتیجه روتین بعدی عکس زیر میباشد:

از کامنت هایی که خود  IDA به صورت خودکار نوشته است میتوانیم متوجه شویم که چک میکند که کدی (IOCTL) که کاربر از سطح کاربر برای درایور ما ارسال کرده است معتبر است یا خیر، پس اگر شرط آخر عکس بالا درست باشد به ما به قسمت default دستور case میرویم پرش میکنیم که با استفاده از تابع DbgPrint پیغام خطای زیر را چاپ میکند:

اما اگر پرش درست نباشد ما به روتین زیر پرش میکنیم:

از خط های زیر روتین میتوان حدس زد که کد IOCTL ارسال شده از کاربر معتبر میباشد وهمچنین برابر با مقدار یکی از case است. خب در این آموزش ما قرار است درایور HEVD را با استفاده از آسیب پذیری Stackoverflow اکسپلویت کنیم. پس به سراغ یکی از case ها که آسیب پذیری Stackoverflow را پیاده سازی کرده است میرویم.

درابتدای این روتین با استفاده از تابع DbgPrint رشته ی زیر چاپ میشود:

سپس تابعی اجرا میشود که توسط برنامه نویس درایور نوشته شده است اجرا میشود و پارامترهای آن شامل IrpSp و Irp است. با دوبار کلیک بروی تابع StackOverflowIoctlHandler  وارد تابع میشویم، مهمترین قسمت این تابع تصویر زیر است که داده ی ارسالی از سمت کاربر و اندازه ی آن را از اشاره گر IrpSp به تابع TriggerStackOverflow ارسال میکند، خب میتوان حدس زد که تابع آسیب پذیر TriggerStackOverflow است (خسته نباشم J) همچنین چک میکند که اگر کاربر داده ی ارسالی را نفرسته باشد پیغام خطایی را چاپ کند و دیگر سراغ تابع TriggerStackOverflow نرود.

خب حال نگاهی به تابع TriggerStackOverflow میاندازیم..

در دستور بالا متغیر KernelBuffer با طول ۲۰۴۴ میسازد که با استفاده از تابع memset مقدار اولیه متعیر KernelBuff را صفر میکند، درایور با استفاده از تابع  ProbeForRead برای چک کردن دسترسی خواندن به بافرهایی که در فضای کاربر ایجاد شده استفاده میکند همچنین تابع ProbeForRead میبایست در بلوک try/except قرار گیرد که اگر استثنایی رخ داد، درایور بتواند با یک پیغام خطا درخواست IRP را جواب دهد که مانع از کرش شدن سیستم میشود (BSOD).

سپس درایور اطلاعاتی از مقادیر ارسال شده توسط کاربر را با استفاده از تابع DbgPrint نمایش میدهد. و در نهایت با استفاده از تابع memcpy تعداد مشخصی که در واقع همان متغیر Size که پارامتر تابع TriggerStackOverflow است و برابر با طول متغیر UserBuffer است، از کاراکترهای از متغیر UserBuffer در KernelBuffer کپی میکند، اینجا دقیقا جایی است که اگر طول محتویات UserBuffer از KernelBuffer بیشتر باشد آسیب پذیری رخ میدهد:

ارتباط با تابع درایور

برای نوشتن اکسپلویت ابتدا نیاز داریم که با دستگاه درایور ارتباط برقرار کنیم برای این منظور ما میتوانیم از زبان سی برای نوشتن کد اکسپلویت استفاده کنیم ولی ترجیح میدهم که با کتابخانه ctypes در پایتون کدهایی با نحو زبان سی بنویسیم که آشنایی با کتابخانه ctypes هم محسوب میشود.

برنامه که ای قرار است بنویسیم به چند بخش جزیی تقسیم میکنیم که نوشتن و درک آن آسانتر شود.

  1. بدست آوردن هندل دستگاه
  2. پیدا کردن کد متناسب IOCTL برای تابع StackOverflowIoctlHandler
  3. ساخت بافری با طول ۲۰۴۸ بایت
  4. ارسال بافر به تابع آسیب پذیر StackOverflowIotclHandler

 

بدست آوردن هندل دستگاه

برای کسب هندل دستگاه از تابع CreateFile استفاده میکنیم که ساختار تابع به صورت زیر است:

هر IDE که برای نوشتن کدهای پایتون استفاده میکنید، را باز کنید شخصا از Pycharm استفاده میکنیم و همچنین ورژن ۲٫۷ پایتون. خب لازم است که در ctype تابع CreateFile را فراخوانی کنیم. تابعی با نام دلخواه برای بدست آوردن هندل دستگاه ایجاد کنید، سپس پارامترهایی که برای فراخوانی تابع CreateFile نیاز داریم را تعریف میکنیم مثلا پارامتر … حاوی GENERIC_READ و GENERIC_WRITE است dwDesiredAccess که دسترسی خواندن و نوشتن را درخواست میکند، به ترتیب مقدار ماکروی GENERIC_READ برابر است با ۰x80000000 و GENERIC_WRITE با مقدار ۰x40000000 برابر است همچنین مقدارهای ماکرو OPEN_EXISTING و FILE_ATTRIBUTE_NORMAL را در اول سورس خارج از تابع تعریف کنید.

حال نیاز به فراخوانی تابع CreateFile داریم البته رشته ها به صورت پیشفرض در پایتون Unicode هستند پس ما باید از تابع CreateFileW استفاده کنیم، تابع را به صورت زیر تعریف میکنیم، توجه داشته باشید که نام دستگاه همان Symbolic Link ایجاد شده در قسمت تحلیل کد درایور است.

همچنین چک میکنیم که خروجی تابع که در متغیر handle ارجاع داده میشود حاوی مقدار معتبری باشد پس داریم:

پیدا کردن کد متناسب IOCTL برای تابع StackOverflowIoctlHandler 

ساختار IOCTL

ioctl  یک کد که در واقع یک عدد است که ما آن را درسمت سطح کاربر میسازیم و از طریق آن با درایور خود ارتباط برقرار کنیم. ساختاری که این کد ۳۲ بیتی دارد در شکل زیر نمایش داده شده است.

ioctl را با این ماکرو ایجاد می کنند که IOCTL_Device_Function نامی اختیاری است.

 

پارامترها:

  • DeviceType: از آنجایی که درایور ما برای سخت افزار خاصی نیست کاری با مقدارهای تعریف شده نداریم. مقادیر که می دهیم باید مقداری بیشتر از ۰x8000 داشته باشد چون مقادیر زیر ۰x8000 برای ماکروسافت رزرو شده اند
  • FunctionCode: این مقدار در واقع عملیاتی است که قرار است اتفاق بیافتد خود ما مقدار آن را مشخص میکنم و این عدد باید بیشتر از ۰x800 باشد چون مقادیر کمتر رزرو شده اند
  • TransferType: نوع دسترسی به حافظه داده که شامل این مقادیر می تواند باشد

METHOD_BUFFERED

METHOD_IN_DIRECT

METHOD_OUT_DIRECT

METHOD_NEITHER

  • RequiredAccess: میزان دسترسی که در هنگام هندل گرفتن از دستگاه باید وارد کنیم ما در درایور خود همه دسترسی ها را گرفتیم (FILE_ANY_ACCESS) یعنی هم خواندن و هم نوشتن. در فایل h کد IOCTL برای تابع StackOverflowIoctlHandler به این صورت تعریف شده است

ما میتوانیم خط بالا را در کد خود کپی پیست یا اینکه ماکرو CTL_CODE برای ساخت کد منحصر به فرد IOCTL پیاده سازی کنیم. برای پیاده سازی ماکرو CTL_CODE تابعی با نام ctl_code ایجاد میکنیم و دستورات زیر را در آن مینویسیم.

البته مقادیر FILE_DEVICE_UNKNOWN، FILE_ANY_ACCESS و METHOD_NEITHER را باید دستی وارد کنیم یا آن ها را در ابتدای سورس تعریف کنیم.

برای امتحان تابع بالا کد زیر را مینویسیم.

خروجی دستور بالا مقدار عددی ۰x222003 است. نکته ای که در این قسمت وجود داره اینکه در این کیس ما سورس کد درایور داریم و میتونیم کد  IOCTL را به راحتی بخوانیم اما اگر سورس کد در اختیار ما نبود چطور میتونسیم کد IOCTL معتبر را پیدا کنیم؟ خب نگاهی به تابع IrpDeviceIoCtlHandler در IDA میاندازیم.

همانطور که میبینید کدی در این قسمت قرار دارد ۰x22201B اما شرط آخر کد ما را به تابع (StackOverflowIoctlHandler) که قرار است بافر را به آن ارسال کنیم، نمیبرد! ادامه ی کد را بررسی میکنیم.

همانطور که مشاهده میکنید با عملیات ریاضی بروی مقدار ۰x22201B به کدهای IOCTL دیگر میرسیم.

 

ساخت بافری با طول ۲۰۴۸ بایت

خوشبختانه کتابخانه ctypes یک تابع برای ساخت بافر به نام create_string_buffer دارد، با کد زیر بافر خود را میسازیم.

ارسال بافر به تابع آسیب پذیر StackOverflowIotclHandler

برای ارسال دستور IOCTL به همراه بافر مورد نظر به دستگاه از تابع DeviceIoControl استفاده میکنیم، ساختار تابع DeviceIoControl به صورت زیر است.

پارامترها:

  • hDevice: هندل خروجی که از CreateFile گرفته ایم را اینجا باید قرار دهید
  • dwIoControlCode: کد IOCTL ما که پیشتر ساخته ایم اینجا قرار میگیرد
  • lpInBuffer: بافری است که ما می خواهیم به درایور بفرستیم این بافر می تواند هر فرمتی مثلا یک رشته یا struct باشد
  • nInBufferSize: طول بافر ورودی
  • lpOutBuffer: بافری که درایور قرار است اطلاعاتی در آن قرار دهد
  • nOutBufferSize: طول بافر خروجی
  • lpBytesReturned: اندازه اطلاعاتی که در بافر خروجی ریخته می شود. در واقع اندازه واقعی ممکن از کو چکتر از اندازه بافرباشد که این پارامتر مشخص می کند

پس تابع ای با نام trigger میسازیم که کد IOCTL و بافر ما را به دستگاه ارسال کند.

قصد داریم کدی که نوشته ای را آزمایش کنیم، در نظر داشته باشید که امکان دارد ویندوزی که حامل درایور HEVD است پس از اجرای برنامه ما کرش کند (BSOD)، در نتیجه دیباگر Windbg را به ماشین مجازی خود متصل کنید و در انتهای کد پایتون تابع trigger را به این صورت صدا بزنید.

در عکس زیر وضعیت دیباگر Windbg پس از اجرای اکسپلویت:

وضعیت رجیسترها:

و در نهایت خروجی دستور !analyze -v:

واقعا دیباگر Windbg عملکرد خوبی داره و اطلاعات جامعی در اختیار محقق های امنیتی قرار میده، به پایان مقاله رسیدیم.

البته این مقاله قسمت دومی هم خواهد داشت ولی از آنجایی که شخص خودم از اکسپلویت کردن درایور ها به صورت کامل اطلاع ندارم، قصد دارم اول خودم مطالعاتی در این زمینه انجام بدهم و بعد قسمت دوم مقاله که حاوی نوشتن اکسپلویت برای اجرای شل کد در سطح کرنل است را خواهم نوشت.

 

منابع

بسیاری از جملات مقاله عیناً از منابع زیر استفاده شده که از نویسنده ی آنها کمال تشکر را دارم

 

لینک دانلود مقاله (نسخه PDF)

 

 

 

Be First to Comment

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *