اکسپلویت سرریز بافر (Buffer Overflow) — از صفر تا صد طراحی اکسپلویت برای یک سرویس شبکه

۳۲۷ بازدید
آخرین به‌روزرسانی: ۱۰ خرداد ۱۴۰۲
زمان مطالعه: ۱۹ دقیقه
اکسپلویت سرریز بافر (Buffer Overflow) — از صفر تا صد طراحی اکسپلویت برای یک سرویس شبکه

اکسپلویتها، یا همان کدهای مخرب، برنامه‌ها و کدهایی هستند که توسط یک یا چند هکر یا محقق امنیتی برای اثبات یا استفاده از آسیب‌پذیری امنیتی خاصی در یک نرم‌افزار، سیستم‌عامل یا سخت‌افزار خاص نوشته می‌شوند. این برنامه‌ها لزوماً برای خرابکاری نوشته و منتشر نمی‌شوند؛ بلکه در مواردی اهداف تحقیقاتی و آموزشی را نیز دنبال می‌کنند، هرچند استفاده از این کدها برای نفوذ به سیستم‌های کاربران امری متداول است.

در این نوشته که به تکنیک‌های پیشرفته اکسپلویت اختصاص دارد، با استفاده از یک اکسپلویت نگاهی به نخستین سطح از دیباگر (GNU (GDB خواهیم داشت و برای چالش‌های بزرگی آماده می‌شویم. یکی از بخش‌های جذاب این نوشته اجرای آنالیز کد خواهد بود، اما وقتی آن را یاد بگیرید، مسلماً کار آسانی به نظرتان می‌رسد. پس تا انتهای این نوشته با ما همراه باشید.

گام اول: نصب فیوژن

نصب فیوژن تا حدود زیادی شبیه نصب پروتواستار (Protostar) است. تنها یک تفاوت کلیدی وجود دارد و آن این است که فیوژن باید به عنوان یک دستگاه اوبونتو 32 بیتی در محیط مجازی‌سازی پیکربندی شود. در حالی که در پروتواستار محیط مجازی به صورت یک باکس لینوکس عمومی تنظیم می‌شد. ولی باید آگاه باشید که اگر در مورد فیوژن، محیط مجازی خود را به صورت اوبونتو 32 بیتی تنظیم نکنید، با مشکلاتی مواجه خواهید شد.

فیوژن را می‌توانید از این آدرس دانلود کنید. زمانی که فیوژن را دانلود و نصب کردید، یک دستگاه مجازی راه‌اندازی کنید، نقشه جنگ را در مقابل خود قرار دهید و آماده یک نبرد بزرگ باشید!

گام دوم: آنالیز سورس کد

سورس کدهای همه مراحل فیوژن را می‌توان به صورت آنلاین یافت. این که می‌توان سورس کد فیوژن را به طور کامل آنالیز کرد، باعث صرفه‌جویی زیادی در زمان ما خواهد شد. بدین منظور باید کمی بیشتر تلاش کنیم. ابتدا به تابع اصلی (main) نگاهی انداخته و تک‌تک توابع را بررسی می‌کنیم.

زمانی که سورس کد را در برابر آسیب‌پذیری‌ها آنالیز می‌کنیم، دو مسئله نخستی که در جستجویان هستیم یکی جایی است که برنامه ورودی‌ها کاربر را دریافت می‌کند و دیگری متغیرهای برنامه است. ما به این دلیل به ورودی کاربر اهمیت می‌دهیم که تنها راهی است که می‌توانیم بر روی برنامه تأثیر بگذاریم. اگر ورودی کاربر وجود نداشته باشد، در این صورت کار اکسپلویت کردن، اگر نگوییم غیرممکن، بسیار دشوارتر می‌شود.

با این وجود با فرض اینکه کاربر ورودی دارد، لازم است که بدانیم ورودی‌های کاربر در کدام متغیرها ذخیره می‌شوند و همچنین چه اطلاعاتی در نزدیکی ورودی‌های ما ذخیره می‌شوند. دانستن این که چه اطلاعاتی در جوار ورودی‌ها ذخیره می‌شود به این دلیل مفید است که می‌توانیم در نهایت این اطلاعات را سرریز کنیم و بدین ترتیب بر روی منطق برنامه به چند روش مختلف علاوه بر بازنویسی EIP تأثیر بگذاریم.

بنابراین با نگاه کردن به تابع اصلی، دو متغیر را می‌بینیم: یک متغیر صحیح که fd نام دارد و یک اشاره‌گر به یک کاراکتر به نام p. به نظر نمی‌رسد که هیچ یک از این متغیرها نقطه ورودی کاربر باشند. در خط 41 می‌بینیم که متغیر fd برای ذخیره‌سازی خروجی تابع serve_forever(PORT) استفاده می‌شود. با این که ما کد این تابع را نداریم؛ اما به نظر می‌رسد که این تابع آن چیزی است که برنامه را به عنوان یک سرویس شبکه تعریف می‌کند.

با در نظر گرفتن همه آنچه که گفتیم به نظرمی رسد که fd چندان به درد ما نخواهد خورد. به طور مشابه متغیر p نیز کارایی چندانی ندارد. در واقع این متغیر در هیچ جای دیگر کد مورد اشاره قرار نگرفته است. اگر در حال بررسی کد یک برنامه‌نویس دیگر بودیم، بهتر بود که از وی می‌پرسیدیم در واقع انگیزه وی از تعریف این متغیر چیست؟

اما با این حال با بررسی تابع main زمان خود را به هدر نداده‌ایم. در خط 44 تابع parse_http_request فراخوانی می‌شود. ما کد این تابع را داریم و بنابراین به نظر می‌رسد که بررسی آن ایده بدی نباشد.

اینک می‌بینیم که به جاهای خوبی رسیده‌ایم. به نظر می‌رسد که سه متغیر داریم که درون این تابع تعریف شده‌اند. یک آرایه کاراکتری که buffer نامیده شده است و اندازه 1024 بایت دارد، یک اشاره‌گر کاراکتر که path نامیده شده و اشاره‌گر کاراکتر دیگر که q نامیده می‌شود.

همچنین لازم به ذکر است که در خط 17 یک نشت اطلاعاتی عمدی داریم. برنامه‌نویس به ما لطف کرده است و گفته است که متغیر بافر در کجای حافظه قرار دارد. این مسئله در ادامه بسیار برای ما مفید خواهد بود.

با حرکت به جلو یک سری دستورات if عجیب می‌بینیم. در خط 19 برنامه بررسی می‌کند که آیا می‌تواند اتصال ریموت را بخواند یا نه. اگر نتواند یک خطا چاپ می‌کند و از برنامه خارج می‌شود. این کار هدف تابع errx است. این نکته همچنین نخستین نقطه ورودی کاربر را برای ما مشخص می‌کند: هر چیزی که کاربر از طریق اتصال شبکه ارسال کند در buffer ذخیره می‌شود. پس شاید فکر کنید که سرریز بافر ما همین‌جا قرار دارد. اما چنین نیست. توجه داشته باشید که آخرین دستور تابع read به صورت sizeof(buffer) است. این بدان معنی است که تنها 1024 بایت اول ورودی کاربر در بافر کپی می‌شوند و بنابراین قابل بهره‌برداری نیست. پس به بررسی خود ادامه می‌دهیم.

دستور if در خط 20 کاملاً دستور جذابی است. دستور errx در انتهای این خط در صورتی که شرط برابر با 0 نباشد، عبارت «Not a GET request» را چاپ می‌کند. این نکته یکی از مهم‌ترین بخش‌های اکسپلویت کردن را که فرمت بندی است روشن می‌کند.

در اغلب موارد سرویس‌های شبکه نیازمند این هستند که داده‌ها در فرمت خاصی دریافت شوند. اگر داده‌ها در فرمت مورد انتظار نباشند، برنامه به سادگی آن‌ها را دور می‌اندازد. این دقیقاً همان اتفاقی است که در خط 20 رخ می‌دهد. شرط دستور if، نخستین چهار بایت بافر را در برابر رشته «GET» (با یک فاصله در انتها) بررسی می‌کند. اگر چهار بایت نخست بافر معادل رشته «GET» نباشند، پروسه متوقف می‌شود. این‌ها برای ما به چه معنی هستند؟ معنی آن این است که چهار بایت نخست اکسپلویت ما بهتر است «GET» باشد، چون در غیر این صورت نمی‌توانیم کار خود را تا جای زیادی پیش ببریم.

سپس در خط 22 متغیر path انتساب می‌یابد. اما این متغیر به چه چیزی انتساب یافته است؟ این همان نقطه‌ای است که اطلاعات ما از اشاره‌گرها به کمکمان می‌آید. همان‌طور که احتمالاً می‌دانید متغیرهای اشاره‌گر در واقع یک آدرس حافظه را در خود نگه‌داری می‌کنند. این مقدار آدرس حافظه که ذخیره می‌شود معادل نوع اشاره‌گر خواهد بود. بنابراین اگر یک اشاره‌گر به char داشته باشیم بدین معنی است که اشاره‌گر، یک کاراکتر یا یک سری از کاراکترها را در خود ذخیره کرده است. این همان نوعی است که به متغیر path داده شده است. علامت & که قبل از buffer قرار گرفته است به کامپایلر می‌گوید که ما می‌خواهیم آدرس کاراکتر buffer را در اندیس 4 به path انتساب دهیم. ما به خود کاراکتر نیازی نداریم و تنها موقعیت کاراکتر را می‌خواهیم.

همه این‌ها در چارچوب برنامه به چه معنی است؟ به خاطر داریم که چهار کاراکتر نخست buffer (اندیس‌های 0 تا 3) برای نگهداری عبارت «GET» رزرو شده‌اند. این بدان معنی است که متغیر به صورت مستقیم به کاراکتر پس از رشته «GET» اشاره می‌کند. این مسئله اساساً به آن معنی است که path یک آدرس است که به طور کامل پس از رشته «GET» به ورودی buffer اشاره می‌کند.

در مورد متغیر q نیز مسئله مشابهی را مشاهده می‌کنیم. تابع strchr آدرس نخستین فاصله‌ای که در رشته path وجود دارد را به متغیر q انتساب می‌دهد. سپس در خط 25 این فاصله خالی (white space) به یک بایت نال (null byte) تبدیل می‌شود. این بدان معنی است که متغیر path تنها به کاراکترهای بین عبارت «GET» و این بایت نال جدید اشاره می‌کند، در حالی که q آدرس کاراکترهایی که بلافاصله پس از بایت نال می‌آیند را ذخیره کرده است.

سپس عبارت if/errx دیگری را می‌بینیم که بیان می‌کند اگر q به چیزی اشاره نمی‌کند (یا به عبارت دیگر اگر فاصله خالی دیگری پس از path وجود نداشته باشد) در این صورت مقدار «Invalid protocol» را در خروجی ارائه دهد. خب پس می‌بینیم که پروتکلی وجود دارد، اما چگونه باید بفهمیم این پروتکل چیست؟ البته با بررسی خطوط بعدی این را خواهیم فهمید. در خط 26 رشته‌ای که q به آن اشاره می‌کند در برابر رشته «HTTP/1.1» مقایسه شده است. می‌توان حدس زد که این سومین فیلد اکسپلویت ما خواهد بود.

جمع‌بندی آنچه آموختیم

خب تا اینجا صرفاً به آنالیز کردن پرداختیم. خوشبختانه این آنالیز به ما کمک کرد تا بفهمیم برنامه چه می‌کند و چرا این گونه انجام می‌دهد.

بنابراین باید موارد زیر را بدانیم:

  • چهار کاراکتر نخست درخواست ما باید به صورت «GET»، همراه با یک فاصله در انتها باشد.
  • پس از «GET»، برنامه آدرس باقی ورودی کاربر را در یک متغیر به نام Path ذخیره می‌کند.
  • وقتی این کار انجام شد، متغیر سوم به نام q همه کاراکترهای پس از نخستین فاصله خالی را در path ذخیره می‌کند و آن فاصله را با یک بایت نال عوض می‌کند. همچنین q باید شامل رشته پروتکل «HTTP/1.1» باشد.

بنابراین چارچوب درخواست مخرب ما چیزی شبیه زیر خواهد بود:

GET <path string> HTTP/1.1

اگر با پروتکل HTTP آشنا باشید، می‌دانید که این فرمت واقعی یک درخواست GET اچ‌تی‌تی‌پی به یک وب‌سرور است.

با اطلاعات فوق به نظر می‌رسد که متغیری که باید زیر نظر بگیریم متغیر path است. این تنها بخشی از رشته است که تغییر می‌یابد. در خط 28 می‌بینیم که این متغیر به صورت واقعی به تابع دیگری به نام fix_path ارسال می‌شود و حدس می‌زنید بعد چه می‌شود؟ می‌توانیم نگاهی به آن تابع بیندازیم. بله آخرین و کوتاه‌ترین تابع این برنامه را جدا می‌کنیم.

در نهایت یک آسیب‌پذیری یافتیم!

پس fix_path شبیه چیست؟

در خط 5 یک متغیر محلی جدید به نام resolved داریم که اندازه آن 128 است. این متغیر همراه با متغیر path از درخواست تجزیه اچ‌تی‌تی‌پی (parse_http_request) به تابعی به نام realpath ارسال می‌شود. با یک گوگل سریع با یا نگاه کردن به صفحه‌های راهنمای لینوکس در می بیابیم که تعریف تابع فوق به صورت زیر است:

()realpath همه لینک‌های نمادین را بسط داده و ارجاع به کاراکترهای /./، /../ و '/' اضافی را در رشته‌های منتهی به کاراکتر null که نامشان path است، تجزیه می‌کند تا یک نام مسیر مطلق را تولید کند. نام مسیر حاصل شده به صورت رشته‌های منتهی به null با طول حداکثر به اندازه PATH_MAX بایت ذخیره می‌شوند و در بافر به وسیله resolved_path مورد ارجاع قرار می‌گیرند. مسیر حاصل هیچ لینک نمادینی، یا اجزایی مانند /./ یا /../ ندارد.

این بدان معنی است که تابع realpath() متغیر path را به مسیر قابل استفاده لینوکس ترجمه می‌کند و آن را به درون متغیر resolved کپی می‌کند. با این که بر اساس تعریف بایت‌ها تا حداکثر PATH_MAX کپی می‌شوند ولی ما هرگز حداقلی را تعریف نکرده‌ایم. این بدان معنی است که می‌توان مسیری را که بزرگ‌تر از 128 بایت است نیز به resolved کپی کرد. خب اینک سرریز بافر خود را یافتیم!

گام سوم: درک این که چرا چنین کردیم

جای پنهان‌کاری نیست که آنالیز کردن سورس کد، کار چندان افتخارآمیزی محسوب نمی‌شود. در واقع می‌توان آن را با ترجمه یک کتاب درسی به زبانی دیگر تشبیه کرد. اما واقعیت این است که آنالیز سورس کد برای پرورش قابلیت توسعه اکسپلویت کاملاً ضروری است. با این که سورس کد را چندان با دقت آنالیز نکردیم؛ اما نخستین نیت ما این بود که کاراکترهای خیلی زیادی را در متغیر buffer وارد کنیم تا دچار خطای segmentation بشویم که احتمالاً هرگز رخ نخواهد داد.

حتی با این که می‌دانیم در ابتدای رشته باید عبارت «GET» و در بخش سوم «HTTP/1.1» را وارد کنیم اما تلاش برای وارد کردن هر رشته‌ای بزرگ‌تر از 1024 بایت با شکست مواجه خواهد شد. تنها از طریق شناسایی متغیرهای آسیب‌پذیری مانند resolved و دانستن این که ورودی resolved از کجا می‌آید است که می‌توانیم دریابیم اکسپلویت ما چگونه راه‌اندازی خواهد شد. چارچوب نهایی اکسپلویت ما چیزی شبیه زیر خواهد بود:

GET <path larger than 128 bytes> HTTP/1.1

اینک می‌دانیم که به یک متغیر path بزرگ‌تر از 128 بایت نیاز داریم در حالی که اندازه کلی بافر همچنان باید کمتر از 1024 بایت باقی بماند. بنابراین تلاش می‌کنیم موضوع را بهتر تشریح کنیم.

گام چهارم: ورود به فیوژن

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

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

ssh fusion@<ip of virtual machine>

ما از ترمینال اوبونتو با ساب سیستم ویندوز برای لینوکس استفاده می‌کنیم. اگر شما از ابزار SSH ویندوز استفاده می‌کنید یک سشن ثانویه SSH نیز برای توسعه اکسپلویت ایجاد کنید. می‌توانید کد اکسپلویت را مستقیماً در دایرکتوری home کاربر فیوژن بنویسید.

زمانی که وصل شدید از شما یک رمز عبور خواسته می‌شود. رمز فیوژن godmode است.

دو ترمینال بهتر از یک ترمینال است

گام پنجم: بررسی برنامه در GDB

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

پیش از آن که به GDB برگردیم باید بدانیم که شناسه پروسس level00 چیست. برای یافتن آن به صورت زیر عمل می‌کنیم:

ps -A | grep level00

با اجرای دستور فوق یک خروجی مشابه تصویر زیر به دست می‌آید. در این مورد شناسه پروسس level00 برابر با 1485 است. با این حال شناسه‌ای که شما به دست می‌آورید به احتمال زیاد متفاوت خواهد بود.

 

اینک ما شناسه پروسس level00 را داریم و می‌خواهیم برنامه باینری را درون GDB بود کنیم. برای انجام این کار دستور زیر را تایپ می‌کنیم:

sudo gdb /opt/fusion/bin/level00

علی‌رغم این که ما مشغول رصد کردن یک پروسس از قبل اجرا شده هستیم؛ ولی می‌خواهیم GDB بداند که کدام برنامه را نگاه می‌کنیم. این کار به ما اجازه می‌دهد که نقاط توقف (breakpoint) را در برخی خطوط مشخص کد قرار دهیم. از این رو مسیر کامل برنامه‌ای که مشغول بررسی آن هستیم را نیز وارد می‌کنیم. از آنجا که پروسس از سوی کاربر دیگری اجرا می‌شود، ما باید دسترسی‌های root برای اتصال به آن داشته باشیم. با استفاده از دستور sudo می‌توانیم این مجوزها را به دست آوریم. از شما خواسته خواهد شد که رمز عبور فیوژن را که godmode است دوباره وارد کنید، چون ما خواستار این مجوزهای سطح بالاتر هستیم.

اینک در GDB هستیم و چند چیز هست که باید به آن‌ها دقت کنیم. نخست این که می‌خواهیم به پروسس level00 که اینک اجرا شده است، الصاق شویم. برای انجام این کار دستور زیر را وارد کنید. البته توجه کنید که به جای 1485 شناسه پروسس را که در مرحله قبل به دست آورده‌اید وارد کنید.

attach 1485

زمانی که این کار را کردید GDB پیام‌هایی را در خروجی ارائه می‌کند که نشان می‌دهد به طور موفقیت‌آمیزی به پروسس الصاق شده است. اگر GDB نتواند به پروسس الصاق شود، باید مطمئن شوید که Sudo را هنگام باز کردن اولیه GDB وارد کرده‌اید.

اینک نکته جالب در مورد برنامه level00 این است که در واقع درون یک پروسس اجرا نمی‌شود؛ این برنامه پروسس‌های فرزند دیگری را باز می‌کند. این کار نشان می‌دهد که احتمالاً یک وب‌سرور است که بی‌شک انتظار می‌رود همزمان چندین اتصال را مدیریت کند. با این وجود این مسئله کمی ما را به دردسر می‌اندازد زیرا GDB به طور پیش‌فرض تنها پروسس والد را پیگیری می‌کند. با این حال می‌توانیم این حالت را تغییر دهیم. برای این که GDB پروسس‌های فرزند را پیگیری کند دستور زیر را وارد کنید:

set follow-fork-mode child

این دستور به GDB می‌گوید که برنامه یک پروسس جدید فورک کرده است و باید پروسس جدید را پیگیری کند.

سپس باید یک نقطه توقف تعیین کنیم که برای ما مفید خواهد بود. از آنجا که به لطف آنالیز عمیق سورس کد، از قبل ایده‌ای کلی در مورد این که چگونه باید این برنامه را اکسپلویت کنیم داریم در این مرحله از GDB صرفاً جهت برآورد این که سرریز ما تا چه حد باید بزرگ باشد استفاده می‌کنیم. از آنجا که سرریز ما در تابع fix_path رخ می‌دهد بهتر است که نقطه توقف را در آنجا تعیین کنیم. ترجیح بر این است که پس از اعلان متغیر resolved اکسپلویت خود را آغاز کنیم. خط 6 محل خوبی به نظر می‌رسد. برای تعیین این نقطه توقف، دستور زیر را تایپ می‌کنیم:

break 6

کار آسانی بود! اینک آماده هستیم که این پروسس را مورد هجمه قرار دهیم و آن را مجدد اجرا نماییم. برای این کار باید دستور زیر را وارد کنیم:

c

این دستور فقط یک حرف است. واقعاً کاری از این آسان‌تر وجود ندارد!

ارسال ورودی تست برای پروسس

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

بنابراین چرا باید آن را منتظر بگذاریم؟ در ترمینالی که برای توسعه اکسپلویت استفاده می‌کنیم از netcat استفاده کرده و یک اتصال جدید به سرویس برقرار می‌سازیم. برای این کار تایپ می‌کنیم:

nc <ip address of Fusion> 20000

با اجرای دستور فوق خروجی مشابه زیر خواهیم داشت:

توسعه‌دهنده برنامه آن‌قدر مهربان بوده است که آدرس آغازین بافر را برای ما ارسال کرده است. این وضعیت بسیار عالی است، چون دیگر نیاز نیست در مورد ساخت یک NOP sled نگران باشیم. اگر نمی‌دانید NOP sled چیست باید توضیح دهیم که یک NOP یا همان No Operation دستوری است که به برنامه می‌گوید هیچ کاری نکن.

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

GDB هنوز با هیچ نقطه توقفی برخورد نکرده است، چون ما هنوز هیچ ورودی ارائه نکرده‌ایم. ما هنوز قصد نداریم یک سرریز بافر را اجرا کنیم. پس فقط یک ورودی ایجاد می‌کنیم که با فرمت خواسته شده که قبلاً کشف کردیم مطابقت داشته باشد. این رشته تست باید چیزی شبیه زیر باشد:

GET /test HTTP/1.1

این رشته الزامات برنامه را که چهار کاراکتر نخست به صورت «GET» و سومین فیلد به صورت «HTTP/1.1» باشد، تأمین می‌کند. اینک آن را به برنامه ارسال می‌کنیم تا ببینیم چه رخ می‌دهد.

خب همه چیز همان‌طور که انتظار داشتیم پیش رفت. GDB به پروسس فرزند سوئیچ کرد و در نقطه توقف خط 6 متوقف شد. اینک تنها به دنبال یک چیز هستیم: در حال حاضر ما تنها به دنبال این هستیم که تخمین اولیه‌ای از مقداری که برای سرریز کردن متغیر resolved برای بازنویسی EIP نیاز داریم را دریابیم. دو دستور هستند که باید اجرا کنیم. نخست تایپ می‌کنیم:

p &resolved

در دستور فوق p به معنی پرینت است. علامت & می‌تواند به معنی آدرس باشد و resolved به معنی این است که می‌خواهیم به آدرس متغیر resolved ارجاع دهیم. همه آن‌ها به این معنی هستند که «آدرس متغیر resolved را چاپ کن.»

آدرس هگزادسیمال حافظه برای متغیر resolved به رنگ قرمز مشخص شده است. به خاطر بسپارید که آدرس شما مطمئناً متفاوت خواهد بود. اینک که می‌دانیم از کجا آغاز کنیم باید بدانیم که به کجا می‌رویم. برای انجام این کار به آدرس EIP نیاز داریم. برای یافتن آن تایپ می‌کنیم:

info frame

این دستور اطلاعات زیادی در مورد فریم استک کنونی ارائه می‌دهد. ولی ما تنها به آدرس EIP علاقه‌مند هستیم.

همان‌طور که انتظار داشتیم EIP چندان از resolved دور نیست. ما می‌توانیم با تایپ دستور زیر فاصله دقیق آن را محاسبه کنیم:

p 0xbffff8dc - 0xbffff860

با تایپ دستور فوق نتیجه 140 نمایش مس‌یابد. این دان معنی است که برای سرریز کردن EIP به 140 کاراکترنیاز داریم. در ادامه اکسپلویت را می‌سازیم تا ببینیم نتیجه کار چه خواهد بود.

گام ششم: ساخت اکسپلویت

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

ساختار کد اکسپلویت خود را خط به خط بررسی می‌کنیم:

در ابتدا یک شی‌بنگ (shebang به معنی کاراکترهای #! است) داریم. این شی‌بنگ به Shell می‌گوید که فایل را با چه برنامه‌ای تفسیر کند. در مورد خودمان می‌خواهیم فایلمان به عنوان یک اسکریپت پایتون تفسیر شود و از این رو مسیر کامل مفسر پایتون را وارد کرده‌ایم.

سپس دستورات ایمپورت را وارد کرده‌ایم. ما به بسته sys برای آرگومان‌های خط فرمان پروسس، بسته Struct برای بسته‌بندی آدرسی که می‌خواهیم EIP را بازنویسی کند و همچنین به socket برای ایجاد اتصال به هدف ریموت نیاز داریم. ببینیم در ادامه چه رخ می‌دهد.

پیش از این که هر کار دیگری انجام دهیم باید شیء سوکت خود را ایجاد کنیم. این کار را درون تابع جدیدی به نام exploit انجام می‌دهیم که نمی‌خواهیم هیچ آرگومانی بگیرد. در خط نخست به متغیر host یک رشته انتساب می‌دهیم که نخستین آرگومان خط فرمان عرضه شده به برنامه است. این رشته آدرس آی‌پی هدف ما خواهد بود. سپس به متغیر port دومین آرگومان خط فرمان را انتساب می‌دهیم. این متغیر نیز به طور بدیهی شماره پورتی خواهد بود که سرویس آسیب‌پذیر روی آن اجرا می‌شود.

سپس یک شیء سوکت جدید تعریف می‌کنیم که evilSock نام دارد. البته ما تنها انتظار داریم که از این اکسپلویت برای مقاصد آگاهی بخشی و آموزشی استفاده شود و نه در مقاصد خرابکاری. با این حال مسلماً نمی‌توانیم از وسوسه ایجاد متغیری به نام evilSock خودداری کنیم و مسلماً آن را توجیه کرده‌ایم.

نخست evilSock را به عنوان یک شیء جدید سوکت از نوع socket.socket انتساب می‌دهیم. سپس تابع connect آن شیء را فراخوانی می‌کنیم تا متغیرهای host و port را به عنوان آدرس و پورتی که باید به آن متصل شوند به آن ارسال کنیم. به دلیل نحوه عملکرد تابع connect این متغیرها باید به شکل یک چندتایی به آن ارائه شوند. یک چندتایی از چند مقدار تشکیل می‌شود که با علامت کاما از هم جدا شده‌اند و این فرمت برای تابع connect الزامی است. یک چندتایی درون پرانتز قرار می‌گیرد و به همین دلیل ما نیز از پرانتز استفاده می‌کنیم.

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

این تابع جدید به نام getMsg نامیده می‌شود و یک شیء سوکت را به عنوان پارامتر می‌گیرد. البته ما آن را به دلایل بدیهی متغیر aSock می‌نامیم. این تابع خود تابعی ساده محسوب می‌شود. ما پیامی را با استفاده از تابع recv از هدف دریافت می‌کنیم. آرگومان ارسال می‌شود، 1024 به معنی بیشینه تعداد بایت‌هایی است که می‌خواهیم دریافت کنیم. می‌دانیم پیامی که اولین بار ارسال می‌شود آدرس آغازین بافر است و چندان بزرگ نیست. 1024 بایت محدوده کاملاً امنی است. زمانی که پیام را دریافت کردیم متغیر ذخیره شده درون آن را چاپ می‌کنیم.

برنامه‌نویسان باتجربه‌تر ممکن است متوجه شوند که متغیر mesg در واقع غیرضروری است و این که این تابع می‌تواند با تایپ کردن «print(aSock.recv(1024))» کاملاً ساده‌تر شود و این نیز نکته صحیحی است. با این حال به دلیل افزایش خوانایی برای افرادی که با کتابخانه سوکت به تازگی آشنا شده‌اند ما این مراحل را تا جایی که توانستیم به طور مجزا نوشته‌ایم.

در این مرحله آماده هستیم که تابع exploit خود را تمام کنیم و ظاهر نهایی آن شبیه تصویر زیر خواهد بود:

در این تصویر می‌بینید که دو فراخوانی به تابع getMsg داشته‌ایم یکی پس از اتصال به هدف و دیگری پس از ارسال درخواست. در بین این دو یک متغیر ایجاد می‌کنیم که payload نام دارد و شامل سرریز بافر است و محتوای آن شامل 144 تا کاراکتر A خواهد بود. با این که تست ما نشان داد که resolved و EIP تنها 140 بایت از هم دور هستند؛ اما همیشه بهتر است که مطمئن شویم سرریز EIP به طور کامل انجام می‌گیرد و خطای segmentation دریافت می‌شود.

پس از این که payload را ایجاد کردیم آن را از طریق فرمی که برنامه می‌پذیرد اجرا می‌کنیم. مطمئن می‌شویم که «GET» و سپس یک فاصله در فیلد نخست وجود دارد. سپس در فیلد دوم payload قرار دارد و در نهایت «HTTP/1.1» سومین فیلد ارسالی است. زمانی که درخواست ساخته شد، می‌توانیم آن را با استفاده از تابع Send به شیء evilSock خود ارسال کنیم.

همچنین مطمئن شوید که ()exploit را در انتهای اسکریپت فراخوانی می‌کنید، چون در غیر این صورت اجرا نخواهد شد.

اینک آماده تست این اسکریپت هستید. مراحلی که برای باز کردن مجدد GDB لازم است را مجدد اجرا کنید و اکسپلویت را با تایپ کردن دستورات زیر اجرا کنید.

chmod +x exploit00.py
./exploit00.py <ip address of Fusion> 20000

باور کنید یا نه همه زحماتی که در مرحله آنالیز کد تا اینجا کشیدیم نتیجه داده است! اینجا یک خطای segmentation داریم. این بدان معنی است که بازنویسی EIP موفقیت‌آمیز بوده است.

گام هفتم: برنامه‌ریزی برای اکسپلویت کامل

اینک بر روی بالاترین قله ایستاده‌ایم و با موفقیت EIP را بازنویسی کرده‌ایم. با این همه قدرت چه کار می‌خواهیم بکنیم؟ البته بدیهی است که می‌خواهیم برخی shell ها را اجرا کنیم.

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

  • می‌توانیم Shellcode را مستقیماً پیش از EIP در متغیر resolved ذخیره کنیم.
  • می‌توانیم shellcode را به طور مستقیم پس از EIP در متغیر resolved ذخیره کنیم.
  • همچنین می‌توانیم shellcode را در پشت متغیر buffer ذخیره کنیم.

مهم‌ترین نکته‌ای که هنگام انتخاب یک shellcode باید توجه کنیم این است که چه اندازه‌ای دارد. بافر resolved تنها 128 بایت است در حالی که هنوز 800 بایت در بافر داریم. اگر بخواهیم shellcode را پس از EIP قرار دهیم اندازه اهمیت چندانی نخواهد داشت. اما این‌ها برای ما چندان مهم نیستند، زیرا آدرس بافر به ما داده شده است و می‌توانیم به راحتی محاسبه کنیم که اندازه shellcode چقدر باید باشد.

از آنچه گفتیم نتیجه می‌شود که اکسپلویت نهایی ما چیزی شبیه تصویر زیر خواهد بود:

اکسپلویت با رشته الزامی «GET» آغاز می‌شود. سپس متغیر resolved را با 139 بایت که آن را تا لبه EIP می‌برد سرریز می‌کنیم و در نهایت آدرس shellcode قرار دارد. سپس عبارت الزامی که نشان‌دهنده استفاده از پروتکل HTTP/1.1 است را داریم و در انتها با استفاده از shellcode خود اکسپلویت را می‌بندیم.

گام هشتم: نوشتن اکسپلویت

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

ما دو تغییر در اکسپلویت قبلی ایجاد کردیم: نخست این که یک متغیر به نام address اضافه کردیم که شامل آدرس shellcode است. اما این آدرس را چگونه به دست آوردیم؟

بدین منظور باید به پیامی که برنامه به ما فرستاد توجه کنیم. برنامه به ما می‌گوید که آدرس متغیر buffer به صورت 0xbffff8f8 است. می‌دانیم که «GET» 4 بایت است و payload ما نیز 139 بایت است، آدرس 0xbffff8f8 ما 4 بایت است و عبارت «HTTP/1.1» با یک فاصله در ابتدا نیز 9 بیت خواهد بود. با اندکی محاسبات هگزادسیمال در GDB با استفاده از دستور پرینت نتیجه زیر حاصل می‌شود:

p/x 0xbffff8f8 + 4 + 139 + 4 + 9

البته مشخص است که حرف p به معنی پرینت است. همچنین دستور x/به معنی این است که می‌خواهیم نتیجه محاسبه به صورت هگزادسیمال نشان داده شود. دستور فوق آدرس زیر را پرینت می‌کند: 0xbffff994

بخش بزرگ کد ما در واقع shellcode است که به آن اضافه کردیم. این shellcode را از وب‌سایت shell-storm دریافت کرده‌ایم که اختصاصاً برای میزبانی shellcode ها استفاده می‌شود. این shellcode خاص یک shell را به پورت 1337 دستگاه هدف متصل می‌کند. سپس می‌توانیم با استفاده از netcat به این پورت وصل شویم و دستورات خود را اجرا کنیم.

گام نهم: لذت بردن از Shell عالی که نوشته‌ایم

زمانش رسیده است. باید اکسپلویت خود را تست کنیم. این بار shellcode خود را با دستور زیر اجرا می‌کنیم:

./exploit00.py <ip address of Fusion> 20000

اجرای دستور فوق صفحه‌ای شبیه تصویر زیر ایجاد می‌کند.

شاید شگفت‌زده باشید که چه رخ داده است. به ترمینال سوئیچ خود بر روی ماشین مجازی فیوژن مراجعه می‌کنیم تا دریابیم چه اتفاقی افتاده است.

با بررسی این که پورت 1337 در عمل استفاده می‌شود یا نه می‌توانیم بررسی کنیم که آیا shellcode ما اجرا شده است یا نه. برای بررسی این نکته می‌توانیم از دستور nertcat به صورت زیر استفاده کنیم:

sudo netstat -tulpn

اجرای دستور فوق تصویر زیرا ایجاد می‌کند:

آیا این خط فرمان زیبا را می‌بینید؟ همه آنچه که امید داشتیم و دعا می‌کردیم رخ داده است، یک پروسس level00 اینک بر روی پورت 1337 اجرا می‌شود! اینک به ترمینال توسعه اکسپلویت سوئیچ می‌کنیم و آن را بررسی می‌کنیم.

البته اکسپلویت ما هنگ کرده است. برای خاتمه دادن آن دکمه‌های Ctrl+C را همزمان می‌زنیم. این کار برنامه‌ای که اینک اجرا می‌شود را متوقف می‌کند. برای اتصال به shell جدید باید دستور زیر را وارد کنیم:

nc <ip address of Fusion> 1337

تا زمانی که یک دستور لینوکس تایپ نکنید چیزی در خروجی نخواهید دید. برای نمونه این نتیجه تایپ کردن دستور ls است:

چه صحنه زیبایی است! shell کوچک ما کار می‌کند. گرچه چندان کامل نیست ولی کار می‌کند. اینک یک اکسپلویت داریم که کار می‌کند.

گزارش ماوقع نبرد

همان‌طور که انتظار داشتیم یک اکسپلویت برای level00 فیوژن نوشیم که نسبت به level های پروتواستار به کار کمی بیشتر نیاز داشت. این برنامه اندکی پیچیده‌تر بود و این که می‌توانیم سورس کد را به طور مجزا بررسی کنیم، باعث صرفه‌جویی زمانی زیادی می‌شود.

البته در این مورد نیز مطمئن هستیم که جمله قبلی در همه موارد عبارت صحیحی محسوب نمی‌شود. ممکن است با خود بگویید که مطالعه کامل سورس کد همیشه جواب می‌دهد. اما پاسخ ما این است که «خونسرد باشید!» این کار برای شما مفید است ولی در نهایت ما کار خود را با حدس‌های زیادی در مرحله توسعه اکسپلویت پیش بردیم. علی‌رغم این که چند متغیر داشتیم که می‌توانستیم از میان آن‌ها انتخاب کنیم، بی‌درنگ توانستیم آن متغیری را که آسیب‌پذیر بود را شناسایی کنیم. همچنین توانستیم فرمتی که اکسپلویت نیاز داشت را بدون هیچ زحمتی رمزگشایی کنیم.

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

==

بر اساس رای ۲ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
null-byte.wonderhowto
۱ دیدگاه برای «اکسپلویت سرریز بافر (Buffer Overflow) — از صفر تا صد طراحی اکسپلویت برای یک سرویس شبکه»

با سلام

داخل مطلب بالا نام کاربری فیوژن قرار داده نشده است

کاربران برای لاگین از نام کاربری fusion
و رمز عبور قید شده در بالا (godmode) استفاده کنند

نظر شما چیست؟

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