قسمت اول بررسی استراکچرها در زبان C - فهمایش در سطح دیزاسمبلی
مقدمهای بر فهمایش دیزاسمبلی
در این تحقیق تلاش بنده بر این بوده است که ساختار بلاکهای اسمبلی که کامپایلر MSVC و GNU و Clang برای استراکچرها زبان C در پلتفرم x86 تولید میکنند، مورد بررسی قرار بدهم. شایان ذکر است، خروجی MSVC با استفاده IDA Pro در محیط ویندوز و خروجی GCC و Clang در محیط لینوکس با استفاده از Ghidra مورد بررسی قرار گرفتهاند. استراکچری که دیزاسمبلی آن مورد بررسی قرار گرفته است، به شرح زیر است:
تصویر 1: محتوای استراکچر مورد بررسی
در تصویر 1، محتوای استراکچر نمایش داده شده است. این استراکچر شامل 6 عضو از انواع داده در زبان C میشود. در تصویر 2، نحوه استفاده از این استراکچر نمایش داده شده است. از قبیل اینکه چگونه اعضای این استراکچر که دارای نوع داده اشارهگر به کاراکتر هستند، مقداردهی و مورد فراخوانی قرار گرفتهاند:
تصویر 2: نحوه استفاده از استراکچر در زبان C
بعد از اینکه برنامه بالا نوشته شد، برنامه بالا را با فلگهای زیر توسط MSVC کامپایل کردم. در جدول 1 فلگهای پیشفرض که در MSVC تنظیم شده بود، آورده شده است:
Debug Configuration Compiler Flags: |
/JMC /permissive- /ifcOutput "x64\Debug\" /GS /W3 /Zc:wchar_t /ZI /Gm- /Od /sdl /Fd"x64\Debug\vc143.pdb" /Zc:inline /fp:precise /D "_DEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /RTC1 /Gd /MDd /FC /Fa"x64\Debug\" /EHsc /nologo /Fo"x64\Debug\" /Fp"x64\Debug\Structs.pch" /diagnostics:column |
Release Configuration Compiler Flags: |
/permissive- /ifcOutput "x64\Release\" /GS /GL /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /sdl /Fd"x64\Release\vc143.pdb" /Zc:inline /fp:precise /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oi /MD /FC /Fa"x64\Release\" /EHsc /nologo /Fo"x64\Release\" /Fp"x64\Release\Structs.pch" /diagnostics:column |
جدول 1: فلگهای MSVC برای کامپایل برنامه
برخی از فلگهایی که در مُد Debug و Release برای Visual Studio تعریف شدهاند، در ادامه مورد تشریح قرار گرفتهاند. این فلگ ها به صورت پیش فرض در مُدهای مختلف Visual Studio فعال هستند، با این حال فلگهای دیگر که به صورت سفارشی به منظور تاثیر آنها بر روی خروجی دیزاسمبلی مورد بررسی قرار خواهند گرفت، به صورت مجزا تشریح خواهند شد:
فلگهای فعال در مُد دیباگ:
- فلگ JMC: فلگ Just My Code برای سادهتر کردن دیباگ برنامهها طراحی شده است و با تمرکز بر روی کد خود کاربر، کدهای مرتبط با سیستم و فریمورک را مخفی میکند. وقتی این ویژگی فعال است، پروسه دیباگ میتواند از کد غیرکاربری، مانند کتابخانههای سیستم یا فریمورکهای شخص ثالث عبور کند و این امر باعث میشود که مراحل پیمایش و درک جریان کد آسانتر شود. به عنوان مثال، هنگامیکه در حال دیباگ برنامه با استفاده از رویکرد Step-In هستیم، وارد توابع سیستمی و کتابخانههای جانبی نخواهیم شد. این امر در نهایت موجب میشود، رویکرد دیباگ محدود به کدهایی شود که خود برنامهنویس نوشته است.
- فلگ Permissive: فلگ Permissive به منظور حفظ مطابقت با قواعد نحوی و استانداردهای برنامهنویسی CPP است. وقتی این فلگ را تنظیم میکنید، کامپایلر اصول Compatibility را نادیده خواهد گرفت، و تلاش بر این خواهد شد که طبق استانداردهای جاری مطابقت نحوی (Syntax Conformance) و مطابقت معنایی (Semantic Conformance) برنامه را مورد بررسی قرار بدهد. به عنوان مثال، این گزینه اگر فعال باشد امکان استفاده از توابعی مانند strcpy به ما داده نخواهد شد، و شخص برنامهنویس باید از strcpy_s اضافه کند که نسخه ایمنسازی شده تابع مذکور است.
- فلگ GS: فلگ GS به عنوان یک ویژگی امنیتی برای شناسایی برخی از آسیبپذیریهای سرریز بافر استفاده میشود. وقتی فلگ GS فعال است، کامپایلر MSVC کد اضافی را به اجرایی تولید شده اضافه میکند تا در زمان اجرا بررسیهایی روی برخی از عملیاتهای بافر انجام دهد. فلگ GS به حفاظت در برابر سرریز بافرهای مبتنی بر Stack کمک میکند. این با افزودن یک کوکی امنیتی به نام Security Cookie به فریم Stack توابعی که از بافرهای لوکال استفاده میکنند، عمل میکند. این کوکی امنیتی به عنوان یک مقدار نگهبان عمل میکند و قبل از بازگشت تابع بررسی میشود تا اطمینان حاصل شود که فریم Stack دچار خرابی نشده است. این استفاده از فلگ GS در MSVC باعث ارائه یک لایه اضافی از امنیت برای شناسایی و کاهش آسیبپذیریهای سرریز بافر میشود.
- فلگ ZI: فلگ Zi امکان تولید اطلاعات دیباگینگ را فراهم میکند. هنگام استفاده از این فلگ، کامپایلر اطلاعات اضافی را در فایل اجرایی قرار میدهد که پروسه دیباگ را بهبود میبخشد. فلگ Zi به کامپایلر دستور میدهد تا یک فایل PDB تولید کند که شامل سمبولها و اطلاعات دیباگینگ درباره کد منبع است. این اطلاعات شامل نامهای تابع، نامهای متغیر، شماره خطوط و سایر جزئیاتی است که برای دیباگ و تجزیه و تحلیل برنامه در زمان اجرا مفید است. فایل PDB که با استفاده از فلگ Zi تولید میشود، میتواند توسط ابزارهای دیباگینگ مانند Visual Studio Debugger استفاده شود تا دیباگ سادهتری را فراهم کند. هنگامیکه یک باینری را در IDA Pro دیباگ می کنیم، اگر فایل PDB آن را به دیباگر بدهیم، خروجی با معناتری را به ما ارائه میدهد که صرفا آدرسهای خالی نیستند.
- فلگ Od: این فلگ برای غیرفعالسازی بهینهسازی استفاده میشود و به کامپایلر دستور میدهد که در طول فرآیند کامپایل، تمام بهینهسازیها را غیرفعال کند. زمانی که از فلگ Od استفاده میشود، کامپایلر کد اسمبلی بدون بهینهسازی تولید میکند که به کد منبع اصلی نزدیک است. این مورد در برخی حالتها مفید است، مانند زمانی که برنامه خود را در حالت دیباگ قرار میدهید و میخواهید کد تولید شده به صورت دقیق با کد منبع اصلی همخوانی داشته باشد تا دیباگ و پیمایش کد آسانتر باشد. غیرفعالسازی بهینهسازی با استفاده از فلگ Od منجر به اجرای کد با سرعت کمتر نسبت به کدهای بهینهسازی شده با سایر فلگهای بهینهسازی مانند O1، O2 یا Ox شود. با این حال، برای اهداف دیباگ میتواند مفید باشد زیرا کد تولید شده آسانتر قابل فهم است. شایان ذکر است، به طور پیشفرض، زمانی که کد منبع را بدون مشخص کردن هیچ فلگ بهینهسازی کامپایل میکنید، کامپایلر فرض میکند Od استفاده شده و بهینهسازی را غیرفعال میکند. اگر میخواهید بهینهسازی را فعال کنید، باید یکی از فلگهای بهینهسازی مانند O1، O2 یا Ox را مشخص کنید تا سطوح مختلف بهینهسازی را فعال کنید.
- فلگ Zc:inline: فلگ Zc برای کامپایلر توابع inline را بر اساس استاندارد زبان CPP فعال یا غیرفعال میکند. در زبان CPP، کلیدواژه "inline" برای نشان دادن به کامپایلر استفاده میشود که یک تابع باید در محل فراخوانی به صورت inline گسترش یابد، به جای اینکه به صورت یک تابع جداگانه فراخوانی شود. هنگامیکه این فلگ را استفاده می کنیم، تمامی توابعی که به صورت inline تعریف شده اند، در محل فراخوانی خود expand خواهند شد و پرشی به محلی دیگر از حافظه صورت نخواهد گرفت. در نتیجه به دلیل عدم استفاده از حافظه Stack و دستورات مرتبط با کار با پشته سرعت اجرای برنامه بهینه خواهد شد. اگر این فلگ در نظر گرفته نشده باشد، حتی با وجود واژه inline در امضا توابع، کامپایلر توابع مذکور را به صورت inline مورد پردازش قرار نخواهد داد.
- فلگ fp:precise: فلگ fp:precise در کامپایلر MSVC مدل اعداد ممیز شناور را کنترل میکند. به طور پیش فرض، MSVC از گزینه /fp:precise استفاده میکند که به معنای تولید کد توسط کامپایلری است که به طور دقیق از استاندارد IEEE 754 پیروی میکند. این گزینه کنترل دقیقی را برای حالت گردکردن فراهم میکند و تضمین میکند که نتایج میانی با دقت کامل محاسبه میشوند. فلگ /fp:precise به ویژه در مواردی که نیاز به رعایت دقیق استاندارد IEEE 754 وجود دارد، مانند برنامههای علمی یا محاسبات عددی که دقت و سازگاری بسیار مهم است، مفید است. این فلگ میتواند سازگاری و قابلیت حملونقل کدی که بر روی رفتار خاص اعداد ممیز شناور را بستگی دارد، حفظ کند. بنابراین، در مواردی که عملکرد اولویت اصلی است و رعایت دقیق استاندارد لازم نیست، میتوانید از مدلهای دیگر اعداد ممیز شناور ارائه شده توسط MSVC مانند /fp:fast یا /fp:strict استفاده کنید.
- فلگ Zc:forScope: این فلگ قوانین استاندارد CPP را برای متغیرهای حلقه for فعال میکند. به طور پیش فرض، MSVC از قوانین قدیمی استفاده میکند که متغیر حلقه for در خارج از محدوده حلقه قابل مشاهده است. این موضوع میتواند منجر به مشکلات و رفتارهای ناخواسته شود. زمانی که فلگ Zc:forScope فعال شود، قوانین MSVC برای متغیرهای حلقه for مطابق با استاندارد CPP اعمال میشود و قابلیت دید متغیر حلقه فقط در بدنه حلقه محدود میشود.
- فلگ RTC1: برای فعال کردن بررسیهای خطا در زمان اجرا استفاده میشود. این فلگ مخفف "Run-Time Error Checks Level 1" میباشد. وقتی این فلگ فعال شود، کامپایلر کد اضافی را در برنامه قرار میدهد تا بررسیهایی در زمان اجرا برای انواع مختلف خطاها انجام دهد، مانند بررسیهای سرریز بافر، متغیرهای نامقداردهی شده و استفاده نادرست از اشارهگرها از جمله این خطاها در برنامهنویسی هستند. فعال کردن فلگ RTC1 میتواند به شناسایی و تشخیص خطاهای برنامهنویسی که ممکن است منجر به رفتار تعریف نشده یا خرابی در زمان اجرا شوند، کمک کند. این فلگ هزینه اضافی را به اجرای برنامه اضافه میکند، زیرا بررسیهای زمان اجرا نیازمند کد و محاسبات اضافی هستند. با این حال، در مراحل توسعه و دیباگ، میتواند یک ابزار مفید باشد تا مشکلات احتمالی را در مراحل اولیه شناسایی و رفع کنید.
- فلگ Gd: این فلگ برای تعیین نحوه فراخوانی توابع C استفاده میشود. نحوه فراخوانی تعیین میکند که چگونه پارامترهای تابع منتقل میشوند و چگونه تابع با کد فراخواننده تعامل میکند. وقتی از پرچم /Gd استفاده میشود، نحوه فراخوانی پیشفرض را مشخص میکند که به عنوان نحوه فراخوانی cdecl شناخته میشود. در نحوه فراخوانی cdecl، پارامترهای تابع از راست به چپ روی Stack قرار میگیرند و فراخواننده مسئول پاکسازی Stack پس از بازگشت تابع است. این نحوه فراخوانی به طور معمول در برنامهنویسی به زبان C استفاده میشود. شایان ذکر است، به طور پیشفرض، MSVC از نحوه فراخوانی cdecl استفاده میکند، بنابراین استفاده از فلگ Gd ضروری نیست. با این حال، مشخص کردن صریح /Gd مطمئن میشود که نحوه فراخوانی پیشفرض استفاده میشود.
- فلگ MDd: فلگ MDd برای تنظیمات دیباگ و استفاده از کتابخانههای پیشفرض (CRT) استفاده میشود. این فلگ برای توسعه و دیباگ برنامهها استفاده میشود و به شما امکان میدهد تا از امکانات دیباگ موجود در برنامه استفاده کنید. وقتی از فلگ MDd استفاده میکنید، برنامه شما با استفاده از کتابخانههای پیشفرض (CRT) که به صورت دیباگ شده هستند، کامپایل میشود.
- فلگ FC: فلگ FC برای تعیین نام فایل منبع در خروجی کامپایلر استفاده میشود. این فلگ با اضافه کردن مسیر کامل فایل منبع به خروجی کامپایلر، به تولید پیامهای خطا و اخطار با اطلاعات بیشتر کمک میکند. هنگام استفاده از فلگ /FC، کامپایلر مسیر کامل فایل منبع را به همراه پیام خطا یا اخطار نمایش میدهد. این ویژگی مفید است زمانی که شما چندین فایل منبع در دایرکتوریهای مختلف دارید، زیرا به شما امکان میدهد به سرعت مکان خطا یا اخطار را تشخیص دهید.
- فلگ FA: فلگ FA برای کنترل تولید کد اسمبلی در فرآیند کامپایل استفاده میشود. این فلگ به شما امکان میدهد نوع و فرمت کد اسمبلی تولید شده را مشخص کنید. فلگ FA چندین گزینه دارد که رفتار تولید کد اسمبلی را تعیین میکنند. فلگ FA در هنگام فرآیند کامپایل، یک فایل اسمبلی (.asm) همراه با فایل آبجکت (.obj) تولید میکند. فایل لیستینگ کد اسمبلی شامل کد اسمبلی تولید شده متناظر با کد منبع است. فلگ Fa این گزینه را به ما اجازه میدهد نام و مکان سفارشی برای فایل لیستینگ اسمبلی مشخص کنید. شایان ذکر است، فلگ FAs همزمان فایل لیستینگ کد اسمبلی (.asm) و فایل لیستینگ کد ماشین (.cod) را در هنگام کامپایل تولید میکند. فایل لیستینگ کد ماشین شامل نمایش شمارهای از دستورات ماشین به صورت هگزادسیمال است.
- فلگ EHsc: این فلگ برای تعیین مدل برخورد با اکسپشنها در کد CPP استفاده میشود. این فلگ برای "Exception Handling (Standard C++)" استفاده میشود و براساس استاندارد CPP برخورد با اکسپشنها را فعال میکند. به طور پیش فرض، MSVC از فلگ EHs استفاده میکند که برخورد با اکسپشنهای CPP را فعال میکند اما از مشخصکردن نوع اکسپشن پشتیبانی نمیکند. هنگام استفاده از فلگ EHsc، رفتار EHs را توسعه میدهد تا شامل پشتیبانی از مشخصکردن نوع اکسپشن هم باشد. برخورد با اکسپشن یک مکانیزم در CPP است که به شما اجازه میدهد اکسپشنها را در طول اجرای برنامه کنترل و انتقال دهید. این مکانیزم روش ساختاری برای مقابله با شرایط استثنایی یا خطاهایی که در زمان اجرا ممکن است رخ دهند، فراهم میکند. این فلگ به هر صورت شرایطی را فراهم میکند که کامپایلر به صورت خودکار بتواند شرایط خاص را با استفاده از بلاکهای دستوری try-catch مبتنی بر استاندارد CPP مدیریت و کنترل کند. با این حال، فعالسازی این فلگ موجب میشود که مقداری بر روی کد Overhead آورده شود و Performance برنامه به خاطر کنترلهای اضافی که کامپایلر به باینری اضافه میکند، کاهش پیدا کند.
فلگهای فعال در مُد Release:
- فلگ GL: این فلگ برای فعال کردن Global Optimization استفاده میشود. این بهینهسازی به کامپایلر امکان میدهد تا عملکرد و سرعت اجرای برنامه را بهبود بخشیده و حجم کد تولید شده را کاهش دهد. با استفاده از پرچم GL، کامپایلر MSVC بهینهسازیهایی را اعمال میکند که بهبود عملکرد برنامه را در مقیاس سراسر برنامه فراهم میکند. این بهینهسازیها میتوانند شامل تغییرات در ساختار دادهها، بهینهسازیهای ریاضی و منطقی، حذف Dead Code و ترتیب اجرای کدها باشند. با فعال کردن فلگ GL، میتوانید عملکرد برنامه خود را بهبود بخشیده و زمان اجرا را کاهش دهید. همچنین، حجم کد تولید شده نیز کاهش مییابد که میتواند منجر به افزایش سرعت بارگذاری و اجرای برنامه شود. به طور معمول، برای استفاده کامل از این بهینهسازی، باید فلگ LTCG را در مرحله لینک فعال کنید. این بهینهسازیها ممکن است باعث افزایش زمان کامپایل شود، اما عملکرد و سرعت اجرای برنامه را بهبود میبخشند.
- فلگ Gy: فلگ Gy برای فعال کردن Function Level Linking استفاده میشود. زمانی که در هنگام کامپایل از فلگ Gy استفاده میشود، کامپایلر را به تولید فایلهای آبجکت جداگانه برای هر تابع در کد مجبور میکند. این امر به پیوند کارآمدتر و کاهش حجم فایل اجرایی منجر میشود. با فعال کردن Function Level Linking با فلگ Gy، لینکر قادر است توابع بیاستفاده را از فایل اجرایی نهایی حذف کند و حجم کل باینری را کاهش دهد. این قابلیت به خصوص در پروژههای بزرگ که نه همه توابع استفاده میشوند و نه همه توابع فراخوانی میشوند، مفید است.
- فلگ Gm-: این فلگ به کامپایلر می گوید که تکنیم Incremental Compilation و همچنین استفاده از فایل های PCH را غیرفعال کند. این دو مورد موجب کامپایل برنامه با سرعت بالاتری می شوند اما در برخی شرایط غیرفعال کردن Incremental Compilation و ایجاد فایل .pch میتواند مفید باشد. این مورد مفید است زمانی که در پروژههای بزرگ کار میکنید و هزینه نگهداری و بهروزرسانی فایل .pch بیشتر از فواید Incremental Compilation سریع است.
در حالت کلی، فلگهایی که در بالا آورده شدهاند به صورت پیشفرض در مُدهای Debug و Release در Visual Studio تعریف شدهاند. برنامه C که با رویکرد Structها نوشته شده است، در هر دو مُد مورد بررسی قرار گرفته است تا درک بهتری از ساختار باینری همچنین با فلگهای گوناگون کامپایلر به دست آورده شود.
بررسی دیزاسمبلی x86 در ویندوز
به منظور درک دیزاسمبلی ساختمان داده Struct در زبان برنامه نویسی C ابتدا خروجی تولید شده برای معماری x86 را در دو مُد Debug و Release مورد بررسی قرار خواهیم داد. بعد از بررسی خروجی دیزاسمبلی برای معماری x86، خروجی کامپایلر MSVC برای معماری x64 را مورد بررسی و ارزیابی قرار خواهیم داد.
تصویر 3: کد منبع برنامه مورد بررسی
همانطور که در جدول 1 آورده شده است، خروجی دیزاسمبلی کد منبع در تصویر 3 با فلگهای پیشفرض کامپایلر برای مُد Debug مورد بررسی قرار گرفته است. اولین نکتهای در تحلیل کد منبع نظر من را جلب کرد، نادیده گرفتن inline بودن تابع MyOperation بود. با اینکه این تابع را به صورت inline تعریف کرده بودم، و همچنین فلگ Zc:inline هم فعال بود، با این حال کامپایلر تصمیم گرفته است که این تابع را به صورت inline در محل فراخوانی خود گسترش ندهد. از همین روی در تابع main، یک دستور call داریم. وقتی این دستور فراخوانی میشود، آدرس 00C11A66 را در پشته قرار میدهد تا وقتی دستور ret فراخوانی شد، جریان اجرای باینری به مسیر صحیح خود بازگردد و ادامه اجرای برنامه را بعد از فراخوانی تابع ادامه بدهد. در تصویر 4، خروجی دیزاسمبلی اجرای این دستور در محیط Visual Studio نمایش داده شده است. وقتی دستور Call اجرا شده است، آدرس دستوربعد بر روی بالا پشته قرار گرفته است که با ارجاع به مقدار ESP-4 در پانل Memory میتوان مقداری که در بالای پشته قرار گرفته است، مشاهده کرد که این مقدار با رنگ آبی نمایش داده شده است.
تصویر 4: خروجی اجرای دستور Call در دیباگر Visual Studio
در ادامه همین مسئله در دیزاسمبلر IDA Pro مورد بررسی قرار گرفته است. وقتی باینری مذکور را در دیزاسمبلر IDA Pro باز کنیم، و وارد دیباگر این دیزاسمبلر شویم، موقعی که دستور Call فراخوانی شود، آدرس دستور بعدی را بر روی پشته قرار میدهد که دستور Ret بتواند فرایند اجرای باینری را به مسیر صحیح خود بازگرداند. در پانل Stack View این باینری مقداری که درون پشته قرار گرفته است، قابل نمایش است.
تصویر 5: خروجی اجرای دستور Call در دیباگر IDA Pro
در تصویر 5، دیزاسمبلی باینری را در مُد Release درون دیباگر IDA Pro مشاهده میکنیم. همانطور که در تصویر 5 قابل مشاهده است، وقتی دستور Call اجرا شده است، آدرس بعدی خود را درون پشته قرار داده است که بعد از اجرای دستورالعمل ret اجرای باینری بتواند به مسیر اجرایی خود بازگردد. بعد از اینکه وارد تابع MyOpertion شویم، خروجی دیزاسمبلی تعریف و مقداردهی Struct در زبان C قابل مشاهده است. در تصویر 6، دیزاسمبلی تابع MyOperation نمایش داده شده است:
تصویر 6: خروجی دیزاسمبلی تابع MyOperation
سه دستور اول اصطلاحا Prologue تابع MyOperation هستند. این سه دستور موجب ایجاد یک فریم جدید و ایجاد فضا برای ذخیرهسازی متغیرهای محلی تابع میشوند. دستور push ebp موجب ذخیره سازی فریم تابع قبلی بر روی پشته خواهد شد. دستور mov ebp, esp موجب شکل گیری یک فریم جدید برای تابع MyOperation خواهد شد. دستور sub esp, 66Ch موجب کم کردن مقدار 66C از پشته به منظور ذخیره سازی متغیرهای محلی درون تابع خواهد شد. به هر صورت، این دستورالعمل فضایی برای متغیرهای محلی و دادههای دیگری که توسط تابع نیاز است، در پشته اختصاص میدهد. در ادامه سه دستور PUSH داریم که به نظر می آید به این دلیل انجام میشوند که قبل از استفاده از رجیسترهای ebx، esi و edi مقادیر قبلی آنها بر روی پشته ذخیره شود تا در ادامه مقادیر آنها قابل بازیابی بانشد. دستور بعدی آدرس مؤثر [ebp-42Ch] را محاسبه کرده و در رجیستر EDI ذخیره میکند. دستور بعدی مقدار 10Bh (هگزادسیمال) را به رجیستر ECX منتقل میکند. مقدار درون رجیستر ECX به عنوان میزان تکرار برای دستور rep stos است که در گام بعد آورده شده است. دستور rep stos مقداری که درون EAX قرار دارد، به آدرسی که توسط EDI اشاره می کند، کپی خواهد کرد.
0 دیدگاه
نظرهای پیشنهاد شده
هیچ دیدگاهی برای نمایش وجود دارد.