مبحث یکم : مقدمه و تاریخچه
مبحث دوم: مراحل نفوذ کردن / جلوگیری از نفوذ
مبحث سوم: حملات شبکه ای
مبحث چهارم - کار عملی
مبحث پنجم - DHCP
مبحث ششم - وب و حملات مطرح در آن
مبحث هفتم - حملات DoS
مبحث هشتم - سیستم عامل
مبحث نهم - مهندسی اجتماعی
مبحث دهم - Vulnerability (آسیب پذیری)

استفاده از آسیب پذیری سرریز بافر

تزریق کد مخرب: مثلا اگر برنامه فایلی را به عنوان ورودی دریافت می‌کند می‌تواند کد مخرب را در آن فایل نوشت و یا با رفتن به محل ذخیره سازی کد مخرب در حافظه می‌توان آن را اجرا کرد. در بعضی روش ‌ها باید محل دقیق ذخیره سازی کد مخرب را بدانیم و آن را درون قسمت “آدرس بازگشت” در پشته قرار دهیم، تا، زمانی که تابع تمام می‌شود به جای آنکه برگشت کند به محل فراخوانی ، به اجرای کد مخرب بپردازد (به محل وجود دستورات کد مخرب رفته و آن ‌ها را اجرا کند).

Format string : در زبان ‌های برنامه نویسی توابعی برای دریافت و نمایش اطلاعات وجود دارند:

fprintf, printf, sprintf, snprintf, vfprintf, vprintf, vsprintf

این توابع از یک سری پارامتر ها (پارامترهای فرمت) استفاده می‌کنند. تابع به سراغ آرگومان ‌های ورودی خود می‌رود و به جای آن داده فراهم شده را جایگذاری می‌کند.

پارامتر نوع خروجی
%d مبنای ده (دسیمال)
%U مبنای ده و بدون علامت (unsigned Decimal)
%x مبنای شانزده (هگزا دسیمال)
%s رشته
%n تعداد بایت ‌‌هایی که تا به حال نوشته شده‌‌اند

اما بر روی تعداد این پارامتر ها و تعداد مسیرهایی که فراهم شده، کنترلی صورت نمی گیرد (اینکه فعلا چند پارامتر دارید و چند متغیر بعد از آن ، چک نمی‌شود).

هدف:

  • جانویسی داده ‌ها برای کنترل روند اجرای برنامه
  • Format string توسط توابع فرمت (format function) مانند printf استفاده می‌شود. اولین آرگومان تابع یک Format string است که تعداد آرگومان ‌ها نیز مبتنی بر همان رشته است.

Printf (“you picked:%d\n”, user_pick);

در شکل 9 بخوبی نمونه ‌ای از استفاده معمول و استفاده خطرناک از تابع printf‌را می بینید.

شکل 9

در شکل 10 مثال دیگری را می بیند که یک استفاده خطرناک را نشان می‌دهد.

شکل 10

اخرین خط در شکل 10، دستوری به زبان Perl است که رشته “A…A”‌داده شده را 200 بار با طول دامنه 8 چاپ می‌کند(شکل 11).

شکل 11

چنانچه به جای یک رشته AA…A از آدرس ‌های حافظه استفاده نماییم می‌توان از این آسیب پذیری جهت خواندن و نوشتن حافظه استفاده نمود.

برای خواندن آدرس ‌های دلخواه حافظه میتوان از پارامتر %s استفاده کرد.

مثال: کد شکل 12 را بررسی کنید. حالت استفاده صحیح و اشتباه از دستور printf را مشاهده می کنید.

شکل 12

در شکل 13 اجرا و نمایش خروجی کد شکل 12 را می بینید.

شکل 13

دستورات شکل 13 ابتدا برنامه شکل 12 را کامپایل کرده و سپس آن را با پارامتر ورودی testing (خط چهارم) اجرا می‌کند. در اینجا با توجه به اینکه داده ورودی، مساله و مشکل خاصی نداشت، هر دو دستور به یک شکل ورودی را نمایش می‌دهند. اما مشکل زمانی ایجاد می‌شود که از پارامترهای فرمت در ورودی خود استفاده کنیم. مثلا به جای testing (در شکل 13) از testing%x  (شکل 14) استفاده کنیم.

شکل 14

همانطور که می بینیم در دستور printf که از پارامتر فرمت استفاده نکرده بود علاوه بر چاپ مقدار ورودی محتویات آدرس حافظه ‌ای که وجود داشته را نمایش داده است. از همین روش می‌توانیم برای خواندن محتوای حافظه هم استفاده کنیم. یک کاربردی که می‌تواند داشته باشد برای بررسی وضعیت پشته می‌توانیم از آن استفاده کنیم (شکل 15).

شکل 15

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

شکل 16

شکل 16 و خروجی آن در شکل 17 نشان دیگری هستند .

شکل 17

در شکل 18 (انتهای آن) به جای آدرس، محتوای آن آدرس نمایش داده شده است.

شکل 18

برای نوشتن دو آدرس دلخواه حافظه از پارامتر فرمت %n می‌توان استفاده کرد . این پارامتر مشخص می‌کند که تا به اینجا در چند خانه حافظه نوشته شده است (شکل 19).

شکل 19

آسیب پذیری ‌های سر ریز پشته و رشته فرمت (format string) اغلب به دلیل ویژگی ‌های زبان برنامه نویسی C هستند. آرایه ‌ها در زبان C به صورت اشاره گر هستند و تشخیص امکان سرریز برای کامپایلر مشکل است. توابع رشته فرمت در زبان C تعداد آرگومانشان در زمان کامپایل مشخص نمی‌شود و تنها در زمان اجرا با بررسی رشته فرمت تعداد آرگومان ‌ها تعیین می‌شود.

راه حل: دقت در برنامه نویسی و کنترل کد. همیشه حالت ‌های غیر محتمل را در نظر بگیرید چون ممکن است کاربر عمداً یا سهواً مقادیر غیر منتظره و غیر محتمل را وارد کند. ممکن است یک نرم‌افزار 10 سال درست کار کند اما یک نفر با وارد کردن یک داده غیر متعارف در آن ، منجر ‌شود به اینکه نرم‌افزار رفتار غیر معمول از خود نشان دهد.

//پایان ارائه // شروع توضیحات استاد.

گفتیم که هر برنامه ‌ای که در سیستم اجرا می‌شود باید در حافظه (RAM)‌ باشد. برنامه در قالب بخش ‌های (Segment)‌مختلفی در RAM‌ قرار می‌گیرد ؛ یک قسمت کد برنامه وجود دارد که (Instruction Pointer)IP به آن اشاره می‌کند و اجرا می‌شود. در یک قسمت دیگر دیتاهای برنامه قرار دارد. فرضا می‌خواهیم یک عبارت را در برنامه پرینت کنیم (مثلا یک hello world!‌چاپ کنیم). این مقدار درون قسمت Data segment حافظه می رود.

در سیستم لینوکس برای کامپایل یک کد برنامه به زبان C و مثلا با نام of.c از دستور زیر است.

gcc –o of of.c

پس از اجرای دستور بالا یک فایل اجرایی به نام of ایجاد می‌شود.

دستور cp /temp/test.txt . باعث می‌شود تا فایل test.txt از پوشه /temp به محل جاری (0) کپی شود.

لینوکس مبتنی بر پسوند نیست. مثلا فایل اجرایی پسوند خاصی ندارد (با توجه به ساختار فایل نوع فایل را تشخیص می‌دهد).

gcc یک نوع مجموعه کامپایلر (Complier collection) ‌های gnu که در لینوکس استفاده می‌شود است.

با دستور cat می‌توان محتوای یک فایل را دید. یک برنامه با نام of.c داریم که آن را با استفاده از دستور  /cat of.cنمایش می‌دهیم (شکل 20).

شکل 20

همانطور که در شکل 20 می بینید: در این برنامه دو تابع تعریف شده است: تابع main و تابع do_copy. تابع main( )‌ به عنوان نقطه شروع برنامه در نظر گرفته می‌شود. به محض اینکه تابع main( )‌اجرا می‌شود ، تابع do_copy( ) را فراخوانی می‌کند و یک آرگومان هم به تابع do_copy ( )‌پاس داده شده است. به مقدار داخل پرانتز، آرگومان می گویند. argv[1] یعنی آن چیزی که از طریق خط فرمان به برنامه داده می‌شود (به عنوان مثال مشابه: یک بار از دستور ls‌ به تنهایی استفاده می‌کنیم یک وقت هم به صورت ls /tmp که در این حالت /tmp به عنوان آرگومانی که برای ls فراهم شده است. در تابع do_copy ( ) هم یک آرایه از نوع کاراکتر با سایر 50 تعریف می‌کند و نام آن را هم بافر قرار می‌دهد (یعنی یک فضایی به اندازه 50 کاراکتر گرفته می‌شود که نام آن فضا buffer است) و با استفاده از تابع (string copy) strcopy(  ) مقدار متغیر srt (که چیزی است که کاربر به عنوان آرگومان در خط فرمان وارد کرده بود) را درون متغیر buffer کپی می‌کند. بعد هم با دستور fprintf( ) یک پیغام نمایش می‌دهد. بعد هم تابع تمام شده، کنترل اجرایی برنامه به تابع main‌بر می‌گردد و …

stderr جایی است که قرار است پیغام کپی شود. stderr یعنی روی standard error کپی را انجام شود. نمونه دیگر خروجی در سیستم لینوکس standard outputمی باشد. غالبا خروجی برنامه ‌ها روی standard output چاپ می‌شود و غالبا error ‌برنامه هم روی standard error . می‌توان سیستم لینوکس را طوری تعریف کرد که مثلا error ‌ها را روی پرینتر چاپ کند، خروجی command‌را هم روی مانیتور نمایش دهد (الان هر دو یکسان است و روی ترمینال شما نمایش می‌دهد)

هر خط از برنامه که قرار است اجرا شود آدرس آن درون (Instruction Pointer) IP قرار می‌گیرد. زمانی که وارد یک تابع می شویم آدرس دستورات آن تابع وارد IP‌می‌شود. این را توجه دارید که ما یک رجیستر IP داریم. زمانی که CPU در حال اجرای تابع است و زمانی که تابع تمام می‌شود کنترل برنامه دوباره باید برگردد به همان جایی که تابع فراخوانی شده تا دستور بعد از فراخوانی اجرا شود. اما سوال اینجاست که آن آدرس (آدرس برگشت) را از کجا می‌آورد. چون درحال حاضر آدرس آخرین دستور تابع درون IP‌است؟ راه ‌های مختلفی می‌تواند وجود داشته باشد اما راه حلی که انجام می‌شود را بیان می کنیم. در فضای حافظه ساختاری به نام stack‌داریم. Stack ‌فضایی است برای ذخیره اطلاعات. زمانی که شما در برنامه خود متغیر محلی تعریف می کنید آن متغیر درون stack می‌رود (مثلا آرایه buffer که در برنامه تعریف کردیم). یعنی یک تفکیکی بین فضاهای مختلف انجام دادند به طوری که در یک قسمت کد برنامه، در یک قسمت متغیرهای برنامه و … وجود دارند. ساختار stack ‌به اصطلاح LIFO (Last In First out) است. یعنی آن مقداری که در انتها وارد شده، ابتدا می‌تواند خارج شود (در نظر بگیرید چند ظرف را روی هم بچینید. آخرین ظرفی که قرار داده اید، ابتدا می توانید بردارید). برای ذخیره آدرس بازگشت هم کاری که انجام می‌شود این است: به محض اینکه تابع ‌do_copy() فراخوانی شد، ‌مقدار آدرس برگشت را (فرضاٌ شماره ‌های شکل روبرو آدرس دستورات هستند) که 5 است را درون stack‌قرار می‌دهد

()main

}

        — —— – – – – -1

        — —— – – – – -2

        — —— – – – – -3

        ;()4 do_copy

        — —— – – – – -5

        — —— – – – – -6

{

حال زمانی که دستورات تابع do_copy تمام شد (به انتهای تابع رسید) مقدار stack، pop می شود (یعنی برداشته می‌شود که در حال حاضر 5 است) و درون رجیستر IP‌ ریخته می‌شود. حال اگر مقداری که درون stack‌است خراب شده باشد (یعنی یک مقدار دیگر قرار داده شده باشد) همان مقدار خراب شده درون IP‌ قرار می‌گیرد. این یک ساختاری است که مورد استفاده قرار می‌گیرد اصطلاحاً به آن call in convention (روش فراخوانی) گفته می‌شود که این مسأله را طراحان CPU شرکت ‌ها (مثل اینتل) طراحی کرده ‌اند.

علاوه بر آدرس بازگشت (Return Address) که در stack ذخیره می‌شود یک سری مقادیر دیگر هم درون stack قرار می‌گیرد. مثلاً متغیر buffer  هم درون stack قرار می‌گیرد. آرگومان ‌هایی که به تابع پاس می‌شوند هم می‌توانند در stack قرار می‌گیرند. زمانی که به خطر char buffer [50] می رسیم روی stack‌فضایی به اندازه 50 کاراکتر گرفته می‌شود و به متغیر buffer اختصاص داده می‌شوند . شکل stack به صورت زیر تبدیل می‌شود.

در دستور بعدی مقدار متغیر str درون فضای buffer ریخته می‌شود و سپس دستور print اجرا می‌شود و زمانی که به } می‌رسد یعنی تابع تمام شده و باید برگردد به دستور بعدی از جایی که فراخوانی شده است. این شرایط در صورتی است که داده ‌ای که کاربر به عنوان ورودی می‌دهد 50 بایت یا کمتر از آن باشد.

حال این برنامه را بنویسید و اجرا کنید (البته قبلا باید آن را کامپایل کرده باشید). برای اجرای برنامه شکل 20 دستور زیر را در ترمینال وارد کنید.

./of hello

of‌ نام فایل اجرایی حاصل از کامپایل است. Hello ‌هم که کمتر از 50 بایت است را به برنامه پاس می‌دهیم. خروجی را در شکل 21 می بینید.

شکل 21

حالا به جای hello، تعداد کاراکتر بیشتری (بیشتر از 50 کاراکتر) را وارد کنید و نتیجه را ببینید.

اسکرول به بالا