دی بلاگ

دسته بندی برنامه نویسی

۱۹ اسفند ۱۳۹۶

حملات Stack Overflow

اگر به صورت جدي با زبان‌هاي C/C++‌ برنامه نوشته باشيد، قطعا با خطاي Segmentation Fault‌ در زمان دسترسي خارج از محدوده‌ي آرايه‌ها و يا کار با اشاره‌گر نامعتبر و تلاش براي دسترسي به فضاي حافظه‌ي غير مجاز، مواجه شده‌ايد. همچنين در زمان بکارگيري توابعي مثل gets نيز warningهاي کامپايلر مبني بر آسيب‌پذير بودن استفاده از اين توابع را مشاهده کرده‌ايد، ولي آيا به دليل آسيب‌پذير بودن اين توابع فکر کرده‌ايد؟ آيا ارتباط بين Overflow Buffer و اين توابع را مي‌دانيد؟

در حالت کلي Overflow ‌به معني نوشتن بيش از حد مجاز در محلي از حافظه است و در صورت استفاده‌ي هوشمندانه مي‌تواند باعث تغيير مسير اجراي برنامه، و در نهايت اجراي کد ديگري مثل /bin/bash و دسترسي به Shell شود. در اين مقاله ابتدا با ساختار پروسه‌ها و نگاشت حافظه در آن‌ها آشنا شده و پس از بررسي ساختار پشته در زمان اجراي توابع، به بررسي Stack Overflow خواهيم پرداخت.

نکته: در اين مقاله از نسخه‌ي ۳۲ بيتي Ubuntu 14.04 استفاده شده است، ولي مطالب آن بر روي نسخه‌ي ۳۲ بيتي توزيع‌هاي ديگر نيز قابل پياده‌سازي است.

برنامه‌هاي نوشته شده پس از کامپايل، کد زبان ماشين توليد مي‌کنند که قابل فهم براي CPU بوده و قادر به اجراي آن‌ها مي‌باشد. براي اجراي يک برنامه، بايد کد و داده‌ي آن در حافظه بارگذاري شده و سپس CPU با خواندن کد برنامه از حافظه، شروع به اجراي آن کند. پس براي تمامي عملياتي که در يک برنامه داريم، مثل انتساب‌ها، دستورات شرطي، حلقه‌ها، فراخواني توابع و… بايد چيزي در حافظه وجود داشته باشد که عمليات و داده‌ي مورد نياز آنرا مشخص مي‌کند. به عنوان مثال a=5 بايد مشخص کند که a به چه بخشي از حافظه اشاره کرده و عمليات مورد نظر ما اين است که مقدار ثابتي را در آن قرار دهيم و قصد جمع کردن، انتساب مقدار متغير ديگر در آن و… را نداريم. به داده‌اي که عمليات CPU و کاري که بايد انجام دهد را مشخص مي‌کند، Opcode مي‌گويند که توسط سازنده‌ي CPU و براي تمامي عمليات پشتيباني شده توسط آن تعريف شده و براي نوشتن برنامه از آن استفاده مي‌شود. در زبان‌هاي برنامه‌نويسي، ما با عبارت‌ها و دستورات سطح بالا که درک آن براي انسان ساده‌تر مي‌باشد کار مي‌کنيم. حتي در زبان اسمبلي که سطح پايين‌ترين حالت برنامه نويسي است، ما براي راحتي با عبارت‌هايي مثل mov, add, push, pop کار مي‌کنيم ولي پردازنده درکي از اين عبارات نداشته و در نهايت بايد وظيه‌اش به صورت مجموعه‌اي از Opcodeها مشخص شود.

نکته:‌ مثال‌ّهاي ما بر روي پردازنده‌هاي Intel‌ مي‌باشد. براي اطلاع از Opcode دستورات مختلف، دستورات پشتيباني شده توسط پردازنده، ساختار پردازنده و… به Intel Developer’s Manual مراجعه کنيد.

زبان‌هاي برنامه نويسي مختلف، طرز کار متفاوتي براي توليد کد زبان ماشين (کد قابل فهم توسط CPU) دارند ولي در نهايت نتيجه‌ي کار و خروجي مورد نظر براي CPU يکسان خواهد بود. زبان‌هاي سطح پاييني مثل Assembly, C, C++ پس از کامپايل، مستقيم کد زبان ماشين توليد مي‌کنند ولي زبان‌هايي مثل Java, .Net به اين صورت عمل نکرده و کدي توليد مي‌کنند که توسط ماشين مجازي آن زبان پردازش شده و در نهايت تبديل به کد زبان ماشين شده و اجرا مي‌گردد. داده و کد يک برنامه در بخش‌هاي مختلفي در حافظه قرار مي‌گيرند که امکان تعيين مجوز خواندن، نوشتن، اجرا کردن و دسترسي به داده و کد به صورت مجزا را فراهم مي‌کند.

نکته: مجوز Segmentهاي مختلف قابل تغيير است که در ادامه به توضيح بخشي از آن خواهيم پرداخت.

کد برنامه در text/code segment قرار داده مي‌شود که مجوز خواندن، اجرا کردن دارد. داده‌هاي global برنامه در صورتيکه مقدار اوليه داشته باشند، در data segment‌ و در صورتيکه مقداردهي براي آن‌ها انجام نشده باشد در bss قرار داده مي‌شوند. پارامترهاي ورودي توابع و متغيرهاي تعريف شده در آن‌ها بر روي stack قرار داده مي‌شوند. براي جلوگيري از تزريق کد در برنامه و اجرا کردن آن، بخش stack برنامه‌ها مجوز اجرا نداشته و تنها مي‌توان از آن اطلاعاتي خوانده و بر روي آن نوشت. در صورتيکه حافظه به صورت پويا و در زمان اجراي برنامه اختصاص داده شود، از بخشي از حافظه به نام heap استفاده خواهد شد. اين ساختارها در شکل ۱ مشاهده مي‌شوند.

ProgramMemory

شکل ۱) بخش‌ّ‌بندي حافظه‌ي برنامه‌ها

نکته‌اي که در اين تصوير اهميت بسيار زيادي دارد، نحوه‌ي رشد stack و heap است. دقت کنيد که stack از آدرس‌هاي بالاتر به سمت آدرس پايين‌تر رشد کرده و heap برعکس آن، از آدرس کمتر به سمت آدرس بيشتر رشد مي‌کند.

براي بررسي بخش‌بندي حافظه‌ي برنامه‌ّها و آشنايي دقيق با کاربرد stack‌ در ارسال پارامترها و تعريف متغيرهاي محلي، برنامه‌ي شکل ۲ را در نظر بگيريد. همانطور که مشاهده مي‌شود دو متغير سراسري که يکي داراي مقدار اوليه بوده و ديگري مقداري ندارد به همراه دو متغير محلي عددي تعريف شده و تابع printf نيز براي نمايش مقدار متغيرهاي محلي استفاده شده است.

Program1

شکل ۲) برنامه‌ي شماره يک

براي مشاهده‌ي بخش‌هاي مختلف برنامه‌ها و تحليل آن‌ها در لينوکس مي‌توان از ابزارهايي مثل objdump, readelf استفاده نمود. در شکل ۳ نحوه‌ي کامپايل برنامه‌ي شکل ۲ و بررسي segment‌ مربوط به some_value, channel_id نمايش داده شده است. در ستون چهارم segment دو متغير سراسري مشاهده مي‌شود که some_value که مقداري نداشت در bss و channel_id که مقداردهي شده بود در data قرار داده شده است.

Objdump

شکل ۳) استفاده از objdump براي تعيين offset و segment متغيرها

براي اطلاع از ساختار کلي، هدرها و segmentهاي يک برنامه مي‌توانيم مشابه شکل ۴ از دستور readelf استفاده نماييم. همانطور که در ستون چهارم از راست مشاهده مي‌شود، text داراي X براي اجرا مي‌باشد ولي داراي مجوز W نبوده و امکان تغيير کد در زمان اجرا وجود ندارد. همچنين بخش data, rodata مجوز اجراي کد ندارند.

readelf

شکل ۴) اجراي دستور readelf براي مشاهده‌ي segmentها

با مشاهده‌ي شکل ۵ و نحوه‌ي استفاده از دستور execstack نيز مشخص است که stack داراي مجوز X نبوده و امکان اجراي کد از روي آن وجود ندارد. هر چند مي‌توانيم در زمان کامپايل برنامه و يا همانطور که در شکل ۵ مشخص است، با دستور execstack آنرا تغيير دهيم.

execstack

شکل ۵) اجراي دستور execstack

همانطور که اشاره شد از stack در فراخواني توابع، ارسال پارامترهاي مورد نياز آن‌ّها، تعيين حافظه براي متغيرهاي محلي و همچنين ذخيره‌ي مقدار رجيسترهايي که در تابع تغيير کرده و مقدار آن‌ها در آينده مورد نياز است، استفاده مي‌شود. در برنامه‌نويسي اسمبلي دو رجيستر BP, SP براي کار با پشته در نظرگرفته شده‌اند. SP‌ هميشه به بالاي پشته و BP به ابتداي فضاي پشته مربوط به تابع فعلي اشاره مي‌کند. در زمان فراخواني يک تابع، براي از بين نرفتن فضاي پشته تابع قبلي (تابع فراخوان يا caller) مقدار BP توسط تابع فعلي (تابع فرخواني شده يا callee) بر روي پشته ذخيره شده و در انتهاي کار تابع و قبل از بازگشت به تابع فراخوان، از روي پشته برداشته شده و در BP قرار مي‌گيرد (تغيير مقدار رجيسترهاي پشته در انتهاي کار توابع توسط دستور LEAVE انجام مي‌شود).

توجه شود که به دليل رشد پشته از آدرس بيشتر به آدرس کمتر، بالاي پشته در پايين حافظه قرار دارد.

نکته: رجيسترهاي BP, SP در حالت ۱۶ بيتي که زمان شروع به کار کامپيوتر بوده و Real Mode ناميده مي‌شود استفاده مي‌شوند. در اين حالت حافظه‌ي قابل دسترس ۱MB است. پس از شروع به کار هسته‌ي سيستم‌عامل، تغيير وضعيتي به Protected Mode انجام مي‌شود که در آن امکان استفاده از حافظه‌ي ۴GB فراهم بوده و رجيسترها نيز ۳۲ بيتي مي‌باشند و به عنوان مثال رجيسترهاي پشته EBP, ESP هستند. در حالت ۶۴ بيتي نيز رجيسترهاي پشته RBP, RSP بوده و البته مدل فراخواني توابع نيز با مدل ۳۲ بيتي متفاوت است که در مقاله‌ي ديگري به آن خواهيم پرداخت.

پارامترهاي تابع از راست به چپ بر روي پشته قرار داده مي‌شوند. اينکار باعث مي‌شود که با قرار دادن EBP به عنوان مبنا، با اضافه کردن به مقدار آن به پارامترهاي تابع و به ترتيب از اولين پارامتر تا آخرين آن‌ها و با کم کردن از EBP به متغيرهاي محلي دسترسي داشته باشيم. در نهايت نکته‌ي آخر در مورد بازگشت از تابع است. انتظاري که مي‌رود اين است که پس از اتمام کار يک تابع، دستوري که بلافاصله پس از محل فراخواني قرار دارد اجرا شده و برنامه ادامه يابد. همانطور که مي‌دانيد رجيستر EIP حاوي آدرس دستور بعدي است که بايد توسط CPU اجرا شود. اگر در زمان فراخواني يک تابع، ابتدا آدرس دستور بعد از فراخواني تابع بر روي پشته قرار داده شده و سپس پرش به ابتداي تابع انجام شده و EIP برابر آدرس ابتداي تابع شود، اجراي تابع شروع خواهد شد (اينکار توسط call انجام مي‌شود) در انتهاي کار تابع نيز، کافي است مقدار آدرس برگشت ذخيره شده، از روي پشته برداشته شده و در EIP قرار داده شود و به اين صورت ادامه‌ي کار از جايي که فراخواني تابع انجام شده بود پيگيري خواهد شد (اينکار توسط دستور ret انجام مي‌شود). توضيحات ارائه شده در مورد فراخواني توابع در شکل ۶ به تصوير کشيده شده‌اند.

callstack

شکل ۶) ساختار پشته در زمان فراخواني توابع

با دقت در شکل ۶ مشاهده مي‌شود که اندازه‌ي هر خانه از پشته برابر ۴ بايت يا ۳۲ بيت است (از %ebp براي دسترسي به پارامترهاي ديگر جمع‌هاي ۴تايي انجام گرفته است). يعني اگر به عنوان مثال يک متغر محلي به صورت char s[10] نيز داشته باشيد، مقدار فضاي تخصيص داده شده براي آن بر روي پشته ۱۲ بايت مي‌باشد و نه ۱۰ بايتي که شما تعريف کرده‌ايد.

با جمع بندي مطالب بيان شده در مورد پشته در زمانيکه در يک تابع قرار داشته باشيد، ساختار پشته از ديد تابع به صورت شکل ۷ مي‌باشد.

callstack_summary

شکل ۷) ساختار کلي پشته در زمان فراخواني تابع

براي بررسي ساختار پشته به صورت عملي برنامه‌ي شکل ۲ را در gdb (GNU Debugger) باز کرده و کد اسمبلي و ساختار اجرايي آنرا بررسي مي‌کنيم. براي باز کردن يک برنامه در محيط gdb کافي است مسير برنامه را به عنوان پارامتر براي آن ارسال نمود: gdb prog

اين محيط به صورت پيش فرض از مدل اسمبلي AT&T استفاده مي‌کند که نسبت به مدل اينتل کمي عجيب است!!! در شکل ۸ ساختار تابع main برنامه‌ي شکل ۲ به صورت اسمبلي نمايش داده شده است. همانطور که در خط +۰ مشاهده مي‌شود، اولين کاري که در تابع انجام شده است، ذخيره کردن مقدار EBP است و پس از آن در خط +۱ مقدار ESP (بالاي پشته در ابتداي تابع) در EBP قرار گرفته و به اين شکل EBP مرز بين پارامترهاي تابع و آدرس برگشت با متغيرهاي محلي خواهد شد.

main_assembly

شکل ۸) ساختار اسمبلي تابع main در gdb

خط +۳ باعث مي‌شود که Alignment پشته به صورت مضربي از ۱۶بايت قرار داده شود (کاري که gcc انجام داده است و علت آن اين است که CPU در هر مرحله خواندن از حافظه ۱۶ بايت بارگذاري مي‌کند، هر چند اين عدد قابل تغيير است) و در خط بعدي يعني +۶ فضايي برابر ۳۲ بايت براي پشته‌ي داخل تابع main در نظر گرفته مي‌شود. اگر به ميزان فضاي مورد نياز دقت کنيم، مي‌بينيم که در اين تابع دو متغير a, b تعريف شده‌اند که ۸ بايت براي آن‌ها بر روي پشته مورد نياز است. از طرف ديگر پس از آن، يک فراخواني تابع printf وجود دارد که سه پارامتر براي آن ارسال شده است. براي پارامتر اول که يک رشته است، آدرس رشته ارسال مي‌شود (۴ بايت) و دو پارامتر ديگر که عددي مي‌باشند مقدارشان بر روي پشته کپي مي‌شود (در نهايت ۱۲ بايت براي پارامترهاي printf) که در مجموع ۸+۱۲=۲۰ بايت در اين تابع بر روي پشته نياز است و در خط +۶ به اندازه‌ي نزديکترين عدد مضرب ۱۶ يعني ۳۲ بايت فضا رزرو شده است.

خطوط +۹, +۱۷ براي مقداردهي اوليه به متغيرهاي a, b استفاده شده‌ و از خط +۲۵ تا خط +۴۱ براي قرار دادن پارامترهاي تابع printf بر روي پشته استفاده شده است. همانطور که مشاهده مي‌شود بلافاصله زير EBP روي پشته (خط +۱۷ و آدرس esp+0x1c) فضاي حافظه براي متغير b بوده و بعد از آن متغير a (آدرس esp+0x18) بر روي پشته قرار داده مي‌شود. يعني مشابه قرار دادن پارامترها که از راست به چپ بر روي پشته قرار داده مي‌شوند، متغيرهاي محلي نيز هرچه ديرتر تعريف شوند، زودتر بر روي پشته قرار داده مي‌شوند. از خط +۲۵ تا +۴۱ نيز پارامترهاي تابع printf بر روي پشته قرار داده مي‌شوند. در اين خطوط دقت شود که به دليل عدم امکان تبادل اطلاعات بين دو قسمت از حافظه، ابتدا مقدار متغيرها از حافظه خوانده شده، در رجيستر eax قرار داده شده و سپس در محل ديگر حافظه که مربوط به پارامترهاي printf مي‌باشد قرار داده مي‌شوند. از راست به چپ، ابتدا از آدرس esp+0x1c مقدار متغير b، سپس از esp+0x18 مقدار متغير a و درنهايت آدرس رشته‌ي format قرار داده مي‌شوند. در شکل ۹ ساختار پشته قبل از فراخواني تابع printf نمايش داده شده است.

stack_before_printf

شکل ۹) ساختار پشته قبل از فراخواني printf

براي بررسي آدرس بازگشت main مي‌توانيم در بخشي از کد يک break-point گذاشته و با اجرا کردن برنامه محتويات حافظه در آدرس EBP+4 را مشاهده کرده و آنرا disassemble کنيم. با انجام اينکار مشاهده مي‌شود که تابع main پس از اجرا شدن به کتابخانه‌ي libc باز مي‌گردد و شروع اجراي آن از اين کتابخانه بوده است. در شکل ۱۰  يک break-point پس از اجراي تابع printf قرار داده شده و محتويات آدرس ارسال شده به عنوان پارامتر اول printf و آدرس بازگشت main نمايش داده شده است. دستور x در محيط gdb محتويات بخشي از حافظه را نمايش مي‌دهد. دستور x/s براي نمايش به صورت رشته و x/wx براي نمايش يک کلمه (۳۲ بيت) به صورت عدد مبناي ۱۶ بکار مي‌رود.

main_return_address

شکل ۱۰) نمايش آدرس بازگشت تابع main

اميدوارم تا اينجاي بحث خسته نشده باشيد و جذابيت بحث براتون حفظ شده باشه، چونکه تازه پس از اين مقدمه‌ي طولاني داريم به مبحث Stack Overflow نزديک مي‌شيم!‌ 😀

بحث Stack Overflow در اين خلاصه مي‌شود که با نوشتن بيش از حد در يک بخش از حافظه، آدرس بازگشت تابع را تغيير داده و باعث اجراي کد ديگري شد. به عنوان مثال برنامه‌ي شکل ۱۱ را در نظر بگيريد.

Program2

شکل ۱۱) برنامه‌ي شماره دو

در اين برنامه به صورت خيلي ساده يک آرايه‌ي کاراکتري تعريف شده و با استفاده از تابع gets اطلاعاتي از ورودي دريافت شده و در اين آرايه قرار داده مي‌شود. در صورت کامپايل اين برنامه با خطايي مبني بر منسوخ شدن و خطرناک بودن تابع gets مواجه خواهيد شد. علت اين است که همانطور که در شکل مشخص است، اين تابع اندازه‌ي ورودي را چک نمي‌کند. ساختار پشته (البته با درنظر گرفتن boundary پشته برابر ۴ بايت بجاي ۱۶ بايتي که در مثال قبلي مشاهده کرديم) براي اين تابع main و فضايي که براي آرايه در نظر گرفته شده است، در شکل ۱۲ مشاهده مي‌شود. دقت کنيد که اسم آرايه ابتداي آدرس ذخيره کردن داده را مشخص کرده و با اضافه کردن عددي به آن، به خانه‌هاي بعدي آرايه دسترسي پيدا کرده و به سمت آدرس‌هاي بالاتر پيش خواهيم رفت و اين به معني اين است که در صورت نوشتن بيش از حد در اين آرايه (بيش‌تر از ۱۲۸ کاراکتر) امکان نوشتن در محل ذخيره‌ي EBP و آدرس بازگشت وجود دارد. آدرس بازگشت از main، نسبت به ابتداي آرايه در str+132 قرار دارد.

stack_array

شکل ۱۲) ساختار پشته براي آرايه

براي کامپايل کردن اين برنامه به صورت زير عمل مي‌کنيم:

gcc

در اين دستور کامپايل چند نکته وجود دارد:

  • -fno-stack-protector مکانيزمي به نام canary براي تشخيص Overflow وجود دارد که با اين سوئيچ آنرا غير فعال مي‌کنيم.
  • -zexecstack براي دادن مجوز اجرا به پشته است.
  • -mpreferred-stack-boundary=2 باعث مي‌شود که alignment پشته بجاي ۱۶ بايتي برابر ۴ بايت باشد.

با اين توضيحات بياييد تست کنيم و ببينيم اگر بجاي آدرس بازگشت BBBB قرار دهيم چه اتفاقي رخ خواهد داد. براي اينکار بايد ۱۳۶ کاراکتر (۱۲۸ کاراکتر براي آرايه، ۴ کاراکتر براي مقدار EBP و در نهايت ۴ بايت براي آدرس بازگشت) در بافر بنويسيم که چهار کاراکتر آخر آن BBBB بوده و ۱۳۲ کاراکتر ابتدايي آن اهميتي ندارد. براي توليد اين رشته از پايتون به صورت زير استفاده مي‌کنيم:

python

اين دستور ۱۳۲ کاراکتر A و چهار کاراکتر B را پشت سرهم قرار داده و در فايل /tmp/inp ذخيره مي‌کند. در محيط gdb از اين فايل به عنوان ورودي استفاده کرده و نتيجه را در شکل ۱۳ مشاهده مي‌کنيم.

changed_return_address

شکل ۱۳) تغيير آدرس بازگشت main

واقعا اين نتيجه لذت بخش نيست؟؟! 😀 با اجراي برنامه در محيط gdb امکان مشاهده‌ي آدرس بازگشت وجود دارد و همانطور که در تصوير مشخص است، اين آدرس برابر ۰x42424242 است که ASCII همان BBBB مي‌باشد. به دليل اينکه اين آدرس به جاي درستي اشاره نکرده و پس از اتمام کار main امکان بازگشت به محل خاصي وجود ندارد، پس از اجراي برنامه Segmentation Fault داده مي‌شود.

الان که موفق به اجراي موفقيت آميز Overflow Stack شديم، بياييد يک برنامه را اجرا کنيم. براي اجراي يک برنامه‌ي خارجي معمولا بهترين گزينه /bin/bash مي‌باشد که امکان دسترسي به کليه‌ي دستورات را فراهم مي‌کند. براي اينکار بايد اين برنامه در جايي از حافظه بارگذاري شده و سپس آدرس آن بجاي آدرس بازگشت از main قرار گيرد. با کمي دقت مشخص است که بهترين محل ذخيره‌ي برنامه‌ي جديد، در ادامه‌ي پشته و بعد از آدرس بازگشت مي‌باشد. برنامه بايد به صورت باينري که همان Opcode است ذخيره شود. مثلا در صورت نياز به NOP (که البته نياز هم خواهد شد!) بايد ۰x90 و به عبارت دقيق‌تر “\x90” بر روي پشته قرار داده شود. اما سوال مهم اين است که آدرسي که بايد به آن پرش صورت بگيرد چه آدرسي است؟ براي پيدا کردن اين آدرس، در ابتداي main و پس از قرار دادن مقدار ESP در EBP مي‌توانيم اين مقدار را برداشته و از آن براي آدرس بازگشت جديد استفاده نمود. ولي هنوز يک مشکل باقي است! براي اطلاع از اين مشکل من در شکل ۱۴ آدرس stack را در سه بار اجراي يک برنامه نمايش داده‌ام. عدد بعد از /proc شناسه يا PID پروسه است و maps نگاشت حافظه را دارد.

ASLR

شکل ۱۴) آدرس stack در سه بار اجراي يک برنامه

همانطوري که مشاهده مي‌شود، بخشي از حافظه که stack در آن قرار دارد، در هر اجرا متفاوت است.

در سيستم‌عامل‌ّهاي فعلي براي جلوگيري از نفوذ به برنامه‌ها و تشخيص آدرس دقيق ساختارهاي پروسه‌ها از ASLR (Address Space Layout Randomization) استفاده مي‌شود. اين مورد باعث مي‌شود که ساختارهاي برنامه در هر اجرا، آدرس‌هاي (البته آدرس‌دهي مجازي است که شيوه‌ي کار آن خود داستاني دارد!) متفاوتي داشته باشد. در لينوکس سه حالت براي آن وجود دارد:

  • ۰: غير فعال
  • ۱: فعال بودن براي کتابخانه‌ها و پشته
  • ۲: فعال بودن براي کتابخانه‌ها، پشته و heap

براي راحتي کار، اين مورد را به صورت زير غير فعال مي‌کنيم.

ASLR_proc

از طرف ديگر به دليل اينکه اجراي برنامه‌ّهاي مختلف متغير بوده و پشته در هر بار اجراي يک برنامه ساختار دقيقا يکساني ندارد، و ممکن چند بايت کمتر يا بيشتر از دفعه‌ي قبل بر روي آن قرار داشته باشد، پس نمي‌توانيم يک آدرس دقيق براي ابتداي برنامه‌ي مورد نظر خود (همان /bin/bash) در نظر بگيريم. براي رفع اين مشکل نيز قبل از داده‌ي مربوط به /bin/bash يکسري NOP قرار داده و به وسط آن‌ها اشاره مي‌کنيم. به اين صورت اجراي برنامه مختل نشده و با اجرا کردن تعداد کمتر يا بيشتري از دستورات NOP به ابتداي shellcode خواهيم رسيد. به دليل اينکه نوشتن يک Shellcode براي اجراي /bin/bash توضيحات جداگانه‌اي لازم دارد فعلا در مورد اين قسمت توضيحاتي ارائه نشده و مي‌توانيد يک Shellcode آماده از يکي از سايت‌هاي shell-storm.org و يا www.exploit-db.com دانلود کنيد.

براي بدست آوردن آدرس بازگشت از روي پشته، برنامه را در gdb باز کرده، پس از تغيير ebp يک break-point گذاشته و آنرا اجرا مي‌کنيم. آدرس $ebp+4 همان محل ذخيره‌ شدن آدرس بازگشت از main  که با اضافه کردن عددي به آن، به وسط دستورات NOP مي‌رسيم.

return_location

شکل ۱۵) بدست آوردن محل آدرس بازگشت از main

با کنار هم قرار دادن موارد ذکر شده در يک اسکريپت پايتون امکان اجراي Shellcode وجود خواهد داشت. در  شکل ۱۶ اسکريپت نوشته شده مشاهده مي‌شود.

shellcode

شکل ۱۶) اسکريپت نوشته شده براي اجراي shellcode

در اين اسکريپت ۳۰۰ بار NOP قبل از shellcode قرار داده شده و آدرس بازگشت از main برابر با ۲۰۰ بايت بعد از محل آدرس بازگشت تنظيم شده است. در شکل ۱۷ فضاي پشته پس از ارسال خروجي اين اسکريپت به عنوان ورودي برنامه‌ي شکل ۱۱ مشاهده مي‌شود.

shellcode_stack

شکل ۱۷) ساختار پشته پس از ارسال خروجي پايتون به عنوان ورودي برنامه دوم

با ذخيره کردن خروجي پايتون در فايل /tmp/inp_shell و اجراي برنامه‌ي دوم در محيط gdb و با اين ورودي، مي‌توانيم اجرا شدن /bin/bash را مشابه شکل ۱۸ مشاهده نماييم.

shellcode_execution_gdb

شکل ۱۸) اجرا شدن موفقيت آميز Shellcode

سوالي که ممکن است برايتان پيش آيد اين است که خب اين چه مشکل امنيتي مي‌تواند داشته باشد؟ و با اجرا شدن اين bash چه اتفاقي رخ خواهد داد؟ براي پاسخ به اين سوال و اتمام اين مقاله، فرض کنيد که نرم‌افزاري داريد که setuid بر روي آن تنظيم شده و owner فايل آن نيز root باشد (يا حتي از آن ساده‌تر، کلا با کاربر root اجرا شده باشد!!!!). معني اين مورد اين است که Effective User در زمان اجرا شدن اين برنامه کاربر root بوده و برنامه تحت مجوزهاي آن اجرا مي‌شود. اگر چنين فايلي آسيب پذير بوده و امکان نفوذ به آن فراهم شده باشد، شما مي‌توانيد به يک root shell دسترسي پيدا کرده و کنترل کامل سيستم را پيدا کنيد. براي تست اين موضوع من setuid را بر روي برنامه‌ي دوم تست خودمان فعال کرده و Stack Overflow براي اجراي bash را خارج از gdb  اجرا مي‌کنم. اين مورد در شکل ۱۹مشاهده مي‌شود.

shellcode-execution

شکل ۱۹) تنظيم کردن setuid و اجراي shellcode

در شکل مشخص است که انجام Overflow با خطاي Segmentation Fault مواجه نشده و برنامه اجرا شده است، ولي چرا Shell نمايش داده نشد؟!

موضوع اين است که به دليل بسته شدن پروسه‌ي پدر که همان برنامه‌ي ovf2 مي‌باشد، bash فرزند نيز بسته شده است! براي جلوگيري از اين مورد مي‌توانيم از دستور cat استفاده کنيم (اين دستور ورودي دريافت شده را به خروجي ارسال کرده و تا زمان عدم دريافت signal با خواهد ماند) و با باز نگه داشتن يک stream امکان تايپ دستور و دريافت نتيجه را داشته باشيم. در شکل ۲۰ نحوه‌ي استفاده نمايش داده شده است. دقت کنيد که امکان استفاده از تمامي دستورات لينوکس وجود داشته و کاربر نيز root گزارش داده مي‌شود! عالي نيست؟!!! 😀

root-shellcode

شکل ۲۰) اجراي موفقيت آميز shellcode خارج از gdb

در اين مقاله سعي کردم حملات Stack Overflow را به صورت کامل و با پيش‌نيازهايي که براي درک نحوه‌ي کار آن لازم است ذکر کرده و مثالي عملي از نحوه‌ي اجراي آن نمايش دهم.

اميدوارم مفيد بوده باشه، موفق باشيد.

آدرس کانال تلگرام

۴ بهمن ۱۳۹۶

تعریف مجدد عملگرها در ++C

قبل از اینکه توضیح بدم توی این مقاله قصد بررسی چه موضوعی رو داریم، بگذارید یک سوال ساده‌ی برنامه نویسی مطرح کنم! به نظرتون شرط زیر اجرا میشه یا نه؟ آیا می‌تونیم عبارت مورد نظر رو توی خروجی ببینیم؟

1

 

چند روز پیش به یک سوال جالب روی stackoverflow برخوردم که چنین چیزی رو پرسیده بودند ولی خب البته توی JavaScript و نه C++، و از اونجایی که من رابطه‌ی خوبی با این زبان دارم 😀 همون سوال رو توی قالب C++ مطرح کرده و بهش می‌پردازم. موضوع اینه که در نگاه اول و با فرض کردن اینکه نوع x هم عدد است، به سرعت و با اطمینان می‌گیم که این دیگه چه سوال مسخره‌ایه و معلومه که چنین چیزی ممکن نیست! ولی با کمی تامل و دقت و اگه با Operator Overloading‌ آشنایی داشته باشیم می‌تونیم کلاسی تعریف کنیم که توی اون، عملگر == تعریف جدیدی داشته و این شرط درست در بیاد.

تعریف مجدد یا Overloading‌ عملگرها یعنی اینکه برای انواع داده‌ای جدیدی که خودتون می‌نویسید معنی جدیدی تعریف کرده و عمکرد خاصی داشته باشید. برای انجام اینکار از کلمه‌ی کلیدی operator‌ استفاده می‌کنیم و اسم متدی که قرار است وظیفه‌ی عملگر را بر عهده بگیرد از ترکیب این کلمه و خود عملگر تشکیل می‌شود. مثلا برای تعریف معنی جدیدی برای عملگر + از عبارت operator+ برای اسم کلمه استفاده می‌کنیم. اینکه مقدار ورودی متد از چه نوعی باشه بستگی به این داره که اشیای ساخته شده از نوع جدید شما با چه انواع داده‌ای دیگری قصد جمع و ضرب و… شدن را دارند. همچنین نوع بازگشتی متد می‌تونه void باشه و چیزی را بر نگردونه و یا اینکه از نوع خود داده‌ی جدید یا هر نوع دیگه‌ای بسته به کاربرد شما باشه. برای یک مثال ساده فرض کنید کلاسی تعریف کرده‌ایم که دو متغیر عددی داشته و قصد تعریف کاربرد جدیدی برای + داریم. برای اینکار می‌تونیم به صورت زیر عمل کنیم:

2

به این صورت در صورت اجرا کردن کد زیر مقادیر a==3, b==8 در خروجی نمایش داده می‌شود.

3

در این اجرا در واقع عملگر + اجرای متد)  t1.operator+( t2 را شبیه سازی کرده‌است. به همین دلیل است که نوع ورودی متد operator باید با نوع داده‌ای که در سمت راست قرار می‌گیرد مطابقت داشته باشد.

نکته: این امکان در تمامی زبان‌های برنامه نویسی وجود نداشته و نمی‌تونیم چنین چیزی رو انجام بدیم. به عنوان مثال در Java چنین قابلیتی وجود نداشته و قابلیت‌های این‌چنینی توسط Interface و با Override کردن متدها انجام می‌پذیرد و شبیه‌سازی متدی وجود نداشته و باید موقع نیاز به یک عملیات، متدی را فراخوانی نماییم.

با این توضیحات بر می‌گردیم به سراغ سوالی که ابتدای کار مطرح کردیم. اگر نوع جدیدی ایجاد کرده و در آن تعریف جدیدی برای عملگر == بنویسیم که در آن مقدار فعلی یک متغیر داخلی به عنوان نتیجه ارسال شده و پس از آن یکی به مقدار متغیر اضافه کند، آیا شرط true بر می‌گرداند؟ خب فرض کنید مقدار اولیه‌ی این متغیر داخلی برابر ۱ باشد، پس از اولین اجرا یکی به این مقدار افزوده شده و مقدار جدید ۲ در آن قرار می‌گیرد. به دلیل true بودن اولین مقایسه، اینکار ادامه یافته و دومین مقایسه اجرا می‌شود. در این حالت مقدار فعلی این متغیر برابر ۲ بوده و مجدد به آن اضافه می‌شود و این روال ادامه می‌یابد. در این شرایط اگر این مقایسه تا  x == 1000‌نیز ادامه می‌یافت باز شرط برقرار بوده و عبارت مورد نظر چاپ می‌شد.

در کد زیر این شرایط برقرار می‌باشد:

4

نکته‌ی آخری که در این کد وجود دارد طرز کار return value++ می‌باشد. برای درک این بخش، تکه کد زیر را در نظر بگیرید. به نظر شما مقدار متغیرهای b, c چند است؟

5

در صورت استفاده از عبارت a++ ابتدا مقدار فعلی متغیر برای نتیجه در نظر گرفته شده و سپس به آن یکی اضافه می‌شود ولی در صورتیکه از ++a استفاده شود، ابتدا جمع انجام شده و سپس مقدار نهایی a به عنوان نتیجه ارسال می‌شود. پس با این توضیحات مقدار b==1‌ و c==3 نتیجه‌ی نهایی این کد خواهد بود.

اميدوارم مفيد بوده باشه، موفق باشيد.

کانال تلگرام

کپی رایت © 2018 دی بلاگ

طراحی توسط Anders Norenبالا ↑