تو قسمتهای قبلی (1 و 2 و 3 و 4) درباره قسمتهای مختلف پلتفرم دات نت که در اتباط با CLR هستن بحث شد. تو این قسمت بیشتر راجع به کدهای IL و فرایند بررسی و تایید اون تو CLR بحث میشه.
ساختار زبان IL برپایه stack (پشته) طراحی شده. stack رو میشه به عنوان بخشی خاص از حافظه RAM سیستم تصور کرد. اینکه این زبان stack-based هست به این معنیه که دستورالعملهای اون عملگرهای مورد استفاده رو برای اجرا به stack انتقال میدن و نتایج بدست اومده حاصل از اجرای این دستورات رو هم از همین stack میخونن.
ازاونجاکه زبان IL هیچ دستورالعملی برای دستکاری registerهای پردازنده نداره و بنابراین هیچ وابستگی ای هم به معماری پردازنده های مختلف نداره، بنابراین طراحی و تولید زبان ها و کامپایلرهای جدیدی که درنهایت IL تولید میکنن و CLR رو هدف قرار میدن، آسونه (یا حداقل راحتتر از سایر پلتفرمهاست).
همچنین دستورالعملهای IL به صورت typeless یا فرانوعی (بی نوع یا بدون وابستگی به نوع داده ها) عمل میکنند. یعنی در اجرای این دستورالعملها نوع داده های درگیر مهم نیست. مثلا IL یه دستورالعمل خاص به نام add برای جمع دو تا عدد که تو آخرین قسمتهای حافظه تو stack قرار داده شدن، داره. این دستورالعمل انواع مجزایی برای کار با نوع های 32 یا 64 بیتی اعداد نداره، بلکه در زمان اجرا، این دستورالعمل به نوع داده اعداد موجود تو stack نیگاه میکنه و با توجه به اون عملیات مربوطه رو انجام میده.
یکی از مهمترین مزایای ساختار IL تو CLR، پایداری و ثبات و امنیت اونه. تو کامپایل JIT در زمان اجرا، CLR فرایندی به نام «بررسی کد» یا Verification رو انجام میده. این فرایند تمامی کدهای IL در حال اجرا رو قبل از کامپایل بطور کامل مورد بررسی و تایید اعتبار قرار میده تا از صحت و امنیت فرایندی که قراره اجرا بشه اطمینان حاصل بشه. مثلا CLR چک میکنه تا تمامی متدهایی قراره اجرا بشن تعداد درستی از پارامترها بهشون پاس شده، نوع پارامترهای پاس شده به این متدها درست باشه، مقدار برگشتی تمامی این متدها بدرستی استفاده شده باشه، تمامی این متدها دستورالعمل صحیحی برای اتمام و بازگشت به کد فراخواننده داشته باشن، و از این قبیل موارد. درضمن از متادیتا ماژولهای مدیریت شده، که شامل اطلاعات تمامی متدها و نوع های تعریف شده در ماژول هستن، در این فرایند بررسی کد بشدت استفاده میشه.
تو ویندوز هر پراسس یک فضای آدرس (حافظه) مجازی مختص خودش داره. ازاونجاکه تو دنیای واقعی نمیشه به کد اپلیکیشنها اعتماد کرد، این جداسازی فضای آدرس بین پراسس ها الزامیه. خوندن یا نوشتن تو آدرسهای غیرمجاز از حافظه کاملا امکانپذیره (و متاسفانه امری کاملا رایجه). با قراردادن هر یک از پراسسهای ویندوزی در فضاهای آدرس مجزا، پایداری و امنیت بیشتری برای برنامه ها فراهم میشه، و یه پراسس نمیتونه به صورت غیرمجاز و خرابکارانه پراسس دیگه ای رو تحت تاثیر قرار بده.
در دنیای برنامه های مدیریت شده، فرایند بررسی کد (Verifying) از اینکه یه اپلیکیشن به صورت غیرمجاز به حافظه دسترسی پیدا کنه جلوگیری میشه. بنابراین میشه حتی چندین برنامه رو تو یه بلوک از فضای آدرس اجرا کرد.
همچنین ازاونجاکه تولید و اجرای پراسس ها تو ویندوز منابع زیادی از سیستم رو مصرف میکنه، اجرای تعداد زیادی از اونا میتونه تاثیر منفی قابل توجهی رو کارایی کلی یه کامپیوتر داشته باشه. بنابراین تو پلتفرم دات نت فریمورک راه حلی برای استفاده هرچه بیشتر و بهتر از این منابع با استفاده از مفهومی به نام AppDomain یا Application Domain ارائه شده. با استفاده از این AppDomain ها فضای آدرس یه پراسس رو میشه به چند بخش تقسیم و کرد و تو هرکدوم یه اپلیکیشن رو اجرا کرد. در واقع یه برنامه مدیریت شده به جای اینکه تو یه پراسس اجرا بشه تو یه AppDomain اجرا میشه.
به صورت پیش فرض هر برنامه دات نتی تو یه فضای آدرس مخصوص به خودش اجرا میشه که این فضای آدرس تنها یه AppDomain داره. اما پراسسی که میزبان CLR هست (مثل IIS) میتونه تصمیم بگیره که چندین AppDomain رو تو یه Process سیستم عامل اجرا کنه. درباره این موضوع بعدا بیشتر بحث میشه.
کدهای ناامن
درحالت پیشفرض کامپایلری که مایکروسافت برای #C تولید کرده تنها کدهای امن تولید میکنه. منظور از کد امن کد مدیریت شده ایه که پس از اجرای فرایند بررسی کد IL (یا Verfying)، امن تشخیص داده بشه. اما این کامپایلر اجازه استفاده از کدهای ناامن رو هم به برنامه نویسا میده. کدهای ناامن دسترسی مستقیم به حافظه سیستم دارن و میتونن داده های موجود تو حافظه رو مستقیما دستکاری کنن. این یه ویژگی بسیار قدرتمند و ممتازه که عموما برای برقراری ارتباط با کدهای مدیریت نشده و یا افزایش کارایی در فرایندهای حساس و زمانبر بکار میره.
اگرچه، استفاده از این کدهای ناامن یه ریسک مهم به همراه داره: کدهای ناامن میتونن ساختارهای داده ای موجود رو خراب کنن و یا حتی با توانایی ذاتی ای که دارن میتونن خطرات امنیتی زیادی رو متوجه سیستم مقصد بکنن. به همین دلیل برای اینکه اجازه استفاده از کدهای ناامن توسط کامپایلر #C داده بشه، این کدها باید با کلمه کلیدی unsafe مشخص بشن. علاوه بر این، ماژول مربوطه هم باید با استفاده از سوییچ unsafe/ کامپایل بشه.
در زمان اجرا، هنگامیکه کامپایلر JIT میخواد متدی که حاوی کد ناامنه رو کامپایل کنه، اول بررسی میکنه که اسمبلی مربوطه اجازه اجرای چنین کدی رو داره یا نه.
این عملیات با بررسی یه دسترسی لازم که توسط کلاس خاصی با نام System.Security.Permissions.SecurityPermission اعمال میشه، انجام میشه. این دسترسی لازم با مقدار SkipVerification از یه enum مخصوص تعیین میشه. این enum درواقع System.Security.Permissions.SecurityPermissionFlag هست.
اطلاعات بیشتر درباره نحوه استفاده از این کلاس و بحث امنیت کد تو دات نت، اینجا و اینجا پیدا میشه.
اگه این فلگ تنظیم شده باشه، کامپایلر JIT کد ناامن رو کامپایل و بعد اجازه اجرای اون رو میده. درصورت وجود این دسترسی، CLR به کد مربوطه درباره دستکاری مستقیم مقادیر موجود در حافظه و اینکه خرابکاری ای به بار نمیاره، اعتماد میکنه!
اگه این فلگ ست نشده باشه، کامپایلر JIT یه استثنای System.InvalidProgramException و یا System.Security.VerificationException صادر میکنه و جلوی اجرای متد مربوطه رو میگیره.
مایکروسافت ابزاری به نام PEVerify.exe تولید کرده که معمولا به همراه ویژوال استودیو نصب میشه. این ابزار تمامی متدهای موجود تو یه اسمبلی رو بررسی میکنه و درصورتیکه کد ناامنی پیدا کنه، هشدار میده.
برای اجرای این ابزار نیازه تا از مسیر نصب اون مطلع بود. مثلا تو کامپیوتر من مسیرش اینه:
البته برای راحتی کار میشه از خط فرمان ویژوال استودیو هم استفاده کرد. با اجرای این ابزار بدون هیچ سوییچی راهنمای کامل اون نمایش داده میشه. مثل تصویر زیر:
مثلا برای اعمال بررسی رو یه اسمبلی میشه به صورت زیر عمل کرد:
حالا اگه یه برنامه داشته باشیم که از کد ناامن استفاده کرده باشه نتیجه حاصله متفاوت خواهد بود. مثلا یه اسمبلی رو درنظر بگیرین که متد زیر رو داشته باشه:
private static void TestUnsafeCode() { unsafe { var a = 10; var b = &a; *b = 11; Console.WriteLine(a); } }
این متد بدون هیچ مشکلی اجرا شده و عدد 11 رو تو کنسول چاپ میکنه. اما نتیجه اجرای ابزار PEVerify روی این اسمبلی شبیه تصویر زیره:
اطلاعات بیشتر درباره این ابزار رو میشه از اینجا بدست آورد.
به عنوان یه اصل کلی، در مواقع موردنیاز میشه با استفاده از این ابزار اسمبلی ها و ریفرنسهای اونا رو با استفاده از این ابزار آزمایش کرد تا مثلا از اجرای درست برنامه درصورت بارگذاری از اینترنت مطمئن شد.
البته باید به این نکته هم توجه کرد که عملیات بررسی کد توسط این ابزار نیاز به دسترسی به متادیتای تمامی اسمبلی های درگیر و وابسته داره. بنابراین درهنگام اجرای این آزمون تمامی ریفرنسهای موردنیاز باید دردسترس باشن. چون این ابزار در عمل از CLR برای یافتن و بارگذاری اسمبلی ها استفاده میکنه بنابراین تمامی قوانین و قراردادهایی که CLR برای پیداکردن اسمبلی های وابسته و ریفرنسها استفاده میکنه تو اینجا هم معتبره. درباره این قوانین و قراردها بعدا بیشتر بحث میشه.
حفاظت از کدهای برنامه
با توجه به توضیحاتی که تا حالا در این سری داده شده، میشه حدس زد که با استفاده از کدهای IL که براحتی در اختیار هرکسی میتونه قرار بگیره، میشه عملیاتی برعکس فرایند کامپایل رو انجام داد و با مهندسی معکوس کدهای اصلی برنامه رو به هر زبان دلخواه موجود بدست آورد. درواقع امروز ابزارهایی رایگان (مثل ILSpy) و یا حتی تجاری (مثل Reflector) هم برای اینکار وجود داره. اما برای روشن شدن این موضوع دو نکته رو باید درنظر گرفت.
اول اینکه تو برنامه های سروری (مثل وبسایت و یا وب سرویس و از این قبیل) با توجه به اینکه دسترسی ای به اسمبلی ها برنامه وجود نداره بنابراین این مشکل در این موارد مطرح نیست.
اما نکته دوم راجع به اپلیکیشنهاییه که توزیع میشن، مثل برنامه های کلاینتی که تحت ویندوز اجرا میشن. از اونجا که فایلهای اسمبلی های پروژه در این حالت در اختیار همه قرار میگیره بنابراین خطر لو رفتن کدهای حساس برنامه وجود داره.
تو این موارد با استفاده از یه روش خاص به نام Obfuscation (با تلفط «آبفاسکیشن» به معنی مبهم سازی) میشه امکان اجرای فرایند مهندسی معکوس کد IL رو بسیار کم و یا حتی غیرممکن کرد. در حالت کلی تو فرایند Obfuscate نام اعضای Private نوع های موجود تو یه اسمبلی به عباراتی که تنها توسط CLR قابل استفاده هست تغییر داده میشه. چون برای نامگذاری اجزای مختلف یه برنامه، تمامی زبانهای برنامه نویسی محدودیتهایی بسیار فراتر از محدودیتهای موجود تو CLR برای استفاده از کاراکترهای موجود دارن. درواقع تعداد کاراکترهای مجاز برای نامگذاری تو CLR خیلی بیشتر از کاراکترهای مجاز برای اینکار مثلا تو #C هست. همونطور که قبلا هم اشاره شد، تنها زبانی که میتونه از این امکانات CLR بصورت کامل استفاده کنه IL هست.
برای Obfuscate کردن یه اسمبلی هم ابزارهای رایگان و تجاری متعددی وجود داره. برخی از اونا چنان کار مبهم کردن کد رو به تکامل رسوندن که عملا با استفاده از هیچ ابزاری نمیشه کد رو به یه زبان برنامه نویسی سطح بالا برگردوند.
درهرحال با تمامی این ترفندها، درنهایت کد نسبتا سطح بالای IL در اختیار خرابکاران میتونه قرار بگیره و هرچند که احتمالش کمه، اما کدهای حساس یه برنامه میتونه لو بره. تو این موارد میشه این قسمتهای حساس رو با استفاده از کدهای مدیریت نشده (مثلا ++native C) تولید کرد و با استفاده از امکاناتی که دات نت فریمورک فراهم میکنه، با اونا ارتباط برقرار کرد.
.: در ادامه نحوه استفاده از ابزار NGen.exe شرح داده میشه.
CLR به زبان ساده - قسمت اول: پیشگفتار
CLR به زبان ساده - قسمت دوم: اسمبلی
CLR به زبان ساده - قسمت سوم: بارگذاری
CLR به زبان ساده - قسمت چهارم: اجرا
CLR به زبان ساده - قسمت پنجم: IL و بررسی کد
CLR به زبان ساده - قسمت ششم: NGen.exe
CLR به زبان ساده - قسمت هفتم: FCL و CTS
CLR به زبان ساده - قسمت هشتم: CLS
CLR به زبان ساده - قسمت نهم: کد مدیریت نشده
:.