استفاده کنندگان از قفل سخت افزاری همیشه به دنبال امن کردن نرم افزار خود برای جلوگیری از کپی شدن و Crack شدن آن هستند و برای رسیدن به منظور خود با استفاده از راههای متنوع، سعی بر استحکام بخشیدن به امنیت نرم افزار خود دارند. اما آیا واقعاً می توان ادعا کرد که با استفاده از قفل سخت افزاری امنیت به طور صد در صد رعایت شده است؟ پاسخ منفی است. کارشناسان این امر معتقدند که هیچ روش قطعی برای امنیت صد در صد نرم افزار حتی با استفاده از قفل سخت افزاری وجود ندارد اما می توان با استفاده از روشهایی این امنیت را تا حد زیادی بالا برد به نحوی که Crack کردن نرم افزار امری بسیار دشوار، پیچیده، پرهزینه و زمان گیر شود. در اینجا سعی بر این داریم که این روشها را بررسی کنیم. همچنین بایستی متذکر این نکته شویم که هرچند روشهای مورد بحث در این متن تقریباً اثبات شده و جزء اصول کار به حساب می آیند، اما همواره ممکن است روشهای جدیدی نیز در این زمینه بوجود آیند.
قبل از شروع لازم است نگاهی کلی به کد اجرایی نرم افزار داشته باشیم. کدهای اجرایی موجود در سیستم عامل ویندوز که توسط محیط های برنامه سازی ایجاد می شوند را می توان در حالت کلی و از نظر نوع اجراشدن به دو دسته کلی تقسیم کرد: کدهای بومی و غیر بومی. کدهای بومی که عموماً توسط زبانهای برنامه سازی از قبیل Visual C ، Delphi و … تولید می شوند، کدهای قابل اجرایی هستند که توسط کامپایلر به نحوی ترجمه شده اند که به زبان اسمبلی بسیار نزدیک اند و دستورات سطح بالا از آنها حذف شده و به صورت مستقل بر روی کامپیوتر اجرا می شوند و معمولاً در هنگام اجرا به طور مستقل عمل کرده و نیازی به نرم افزار خاصی در زمان اجرا ندارند. Crack کردن این گونه کدها به دلیل سطح پایین بودن، مشکل تر است؛ چرا که اطلاعات سطح بالای برنامه نویسی و توابع سطح بالا از آنها حذف شده است و کدها به فرم اسمبلی و زبان ماشین درآمده اند و پیداکردن نقاط مورد نظر و بحرانی در آنها دشوار بوده و Cracker برای رسیدن به منظور خود بایستی تسلط کافی بر زبان اسمبلی داشته باشد. اما ناگفته نماند که نرم افزارهایی وجود دارند که می توانند تا حدودی این گونه نرم افزارها را Decompile کرده و به Cracker در رسیدن به منظور خود کمک کنند. اما در صورت رعایت کردن برخی نکات که در همین متن ذکر می شوند، می توان تا حدود زیادی این گونه کدها را امن کرد. در مقابل دسته وسیع کدهای بومی، کدهای غیربومی قرار دارند. این گونه کدهای اجرایی که معمولاً توسط یک کامپایلر خطاگیری شده و به یک کد میانی غیر اجرایی تبدیل می شوند که برای اجرای خود نیاز به یک محیط نرم افزاری جداگانه دارند. مشهورترین این نوع کدها را می توان کدهای جاوا و کدهای .net دانست. کدهای جاوا توسط ماشین مجازی جاوا و کدهای .net توسط .net framework اجرا می شوند. به دلیل نهایی نبودن این گونه کدها، اطلاعات برنامه نویسی زیادی در آنها وجود دارد؛ بنابراین نرم افزارهای زیادی برای برگرداندن Source این گونه کدها طراحی شده است که در مواردي بسیار موفق عمل کرده و Cracker به راحتی می تواند کار خود را بر روی این کدها انجام دهد. برای جلوگیری از Crack شدن این گونه کدها نیز راهکارهایی وجود دارد که Encrypt کردن کد یکی از آن راه حل هاست.
استفاده از نرم افزارهای Encryptor
توصیه می شود پس از تنظیم قفل سخت افزاری بر روی برنامه، برای جلوگیری از Crack ، از نرم افزارهای Encryptor استفاده کنید. این گونه نرم افزارها برای جلوگیری از دسترسی مستقیم و تغییر برنامه های کامپایل شده تولید شده اند. یک نرم افزار Protector ، یک Application را رمز کرده و مانند یک پوسته آن را در برگرفته و باعث می شود برنامه در مقابل حمله ها امن بماند. هنگامی که یک برنامه محافظت شده توسط سیستم عامل اجرا می شود، ابتدا نرم افزار Protector وارد عمل شده، کنترل CPU را بدست می گیرد و به دنبال ابزارهای Crack (از قبیل Disassemblerها و De-Compilerها) می گردد و در صورت مناسب بودن اوضاع، برنامه اصلی را دیکد کرده و کنترل CPU را به برنامه اصلی می دهد. اما برنامه های Encryptorنیز با همه قدرتشان از حمله ی Cracker ها محفوظ نمانده اند چرا که از بدو تولدشان، شاهد بوجودآمدن نرم افزارهای ضد Encryption شدند. این گونه ابزارها در مواردی به درستی عمل کرده و عمل Encryptor را خنثی می کنند. مشکل اصلی Encryptor ها این است که از تکنیکهای شناخته شده ای استفاده می کنند که توسط ابزارهای حمله کننده قابل تشخیص اند. یک مشکل دیگر آنها این است که باید در حالت عادی و با مجوزهای معمولی (و نه سطح بالای سیستم عامل) اجرا شوند در صورتی که نرم افزارهای حمله کننده ی به آنها، این امکان را دارند که در سطح نزدیک به سطح سیستم عامل اجرا شوند؛ بنابراین به راحتی می توانند زمان اجرا و خروج Encryptor را مانیتور کرده و در زمان مناسب به آنها حمله کنند. یکی دیگر از اشکالات نرم افزارهای Encryptor این است که پس از Encrypt کردن کد تغییراتی در آن بوجود می آورند که این تغییرات ممکن است توسط آنتی ویروسها خطرآفرین تشخیص داده شوند و آنتی ویروس جلوی اجرای نرم افزار نهایی را بگیرد و یا حتی آن را قرنطینه یا حذف کند. در این گونه موارد بایستی تنظیمات Encryptor را طوری تغییر داد تا نرم افزار تولید شده نهایی از طرف آنتی ویروسها خطرناک تشخیص داده نشود.
با تمام معایبی که در مورد نرم افزارهای Encryptor گفته شد، استفاده از آنها همیشه توصیه می شود. در مورد کدهای غیر بومی (نظیر برنامه هایی که با .net نوشته می شوند،) این کار اجتناب ناپذیر است.
نکات قابل توجه هنگام کدنویسی
همان گونه که می دانید، استفاده ی صِرف از قفل سخت افزاری و توجه نکردن به اصول امنیتی، کمک شایانی به محافظت کد شما در مقابل حملات نمی کند؛ بنابراین باید هنگام کدنویسی و استفاده از قفل سخت افزاری نکاتی را رعایت کنید. بعضی از این موارد در زیر به اختصار شرح داده می شوند:
- داده هایی که مربوط به امنیت نرم افزار شما می شوند را هیچ گاه به صورت رشته معمولی در برنامه خود وارد نکنید. زيرا cracker براحتي مي تواند رشته را در کد شما جستجو كرده و محل چک کردن قفل سخت افزاری را در برنامه شما پیدا کند. برای ذخیره کردن پیغامهایی از قبیل : “قفل سخت افزاری یافت نشد “ ، “رمز عبور را به صورت صحیح وارد کنید” و از این دست پیغامها، از الگوریتمهای رمزنگاری قدرتمند استفاده کرده و در هنگام لزوم آنها را دیکد کنید. در این گونه موارد از الگوریتمهای رمزنگاری متقارن (نظیرRijndael ، AES ، DES و …) استفاده کنید. این گونه الگوریتمها داده ها را با داشتن کلیدهای نسبتاً طولانی، با سرعت بالا و مصرف حافظه کم، به نحو مطلوبی رمزنگاری می کنند؛ Source Code این الگوریتمها را برای هر زبان برنامه سازی می توانید به راحتی پیدا کنید.
- کلیدهای الگوریتمهای رمزنگاری و رمزعبور قفل سخت افزاری را در زمان اجرا تولید کنید و هیچ گاه آنها را به صورت خام در برنامه وارد نکنید. بسیاری از قفل های سخت افزاری داری توابع امنیتی می باشند که برای فراخوانی آنها نیاز به رمز عبور دارید؛ همچنین در بعضی قفل های سخت افزاری توابعی وجود دارد که داده ها را با استفاده از یک کلید رمز می کنند. در همه این گونه موارد، رمز عبور یا کلید رمزنگاری به عنوان داده هایی شناخته می شوند که ذخیره سازی آنها کار بسیار خطرناکی است؛ چرا که با داشتن رمز عبور و یا کلیدهای رمزنگاری و یا کلیدهای توابع امنیتی، امنیت نرم افزار به راحتی به مخاطره می افتد. به همین دلیل سعی کنید در هیچ جا این داده ها را به صورت خام ذخیره نکنید و در هنگام اجرای برنامه هر زمان که به آنها نیاز داشتید، آنها را به صورت موقتی و روی هوا (on the fly ) و با استفاده از یک الگوریتم پیچیده محاسباتی ایجاد کنید. به عنوان مثال فرض کنید که در قفل سخت افزاری تابعی وجود دارد که یک رشته متنی را روی قفل ذخیره می کند و این تابع برای اجرا شدن نیاز به یک رمز عبور عددی دارد.
صرف نظر از اینکه خود این رشته نیز بهتر است رمز شده باشد، به نحوه ارسال رمز عبور به این تابع توجه کنید. فرض کنید که این تابع امضای زیر را داشته باشد:
intwrite_string(char *str, intpassword);
برای ارسال رمز عبور به تابع فوق مثلاً می توانید مشابه کد زیر عمل نمایید:
write_string(mystr, int(sqrt(p۱*p۲/a)));
که فرمول فوق طوری طراحی شده است که نتیجه آن رمز عبور تابع write_string است. با این روش، برای تولید رمز عبور در زبان اسمبلی کد زیادی تولید می شود که خود باعث سردرگمی Cracker خواهد شد و او برای کشف رمز باید متحمل صرف وقت زیادی شود.
- متغیرهای کلیدی را فقط هنگام نیاز بسازید و به سرعت آنها را از بین ببرید.
-
بین عملیات خواندن مقادیر از قفل سخت افزاری و استفاده از آنها فاصله بیندازید.مثلاً هنگامی که یک رشته داده را از قفل سخت افزاری می خوانید، بلافاصله از آن استفاده نکنید و یا بلافاصله آن را با مقادیری مقایسه نکنید.
-
هیچ گاه برای چک کردن قفل سخت افزاری از تایمر استفاده نکنید؛ چرا که Cracker می تواند به راحتی به تایمر شما Handle گرفته و آن را از کار بیاندازید.
- در زمانی که قفل سخت افزاری بر روی کامپیوتر یافت نشده است، تا حد امکان پیغام عدم وجود قفل سخت افزاری را به کاربر نمایش ندهید. استفاده از کد خطا می تواند در این جور مواقع راهگشا باشد.در یک نقطه خاص از برنامه هنگامی که می خواهید قفل را چک کنید، سعی کنید این کار را به صورت تصادفی انجام دهید.برای انجام این منظور می توانید از تابع randomاستفاده کنید. یک عدد تصادفی تولید کرده و در صورت قرارداشتن این عدد در بازه ای خاص، قفل سخت افزاری را چک کنید. این کار باعث می شود که پیدا کردن نقاط چک کردن قفل سخت افزاری برای Cracker دشوار شود.
- استفاده صحیح و موثر از فضای ذخیره سازی قفل سخت افزاری. سعی کنید از فضای داده ای قفل سخت افزاری برای ذخیره کردن داده های مهم و حیاتی برنامه استفاده کنید. کد مشتری، شماره سریالها، متغیرهای کلیدی برنامه، موارد مناسبی برای این کار هستند. در صورتی که قفل سخت افزاری حاوی اطلاعات کلیدی نرم افزار باشد، عدم وجود آن باعث متوقف شدن برنامه یا عدم کارکرد صحیح آن می شود.
- اجتناب از فراخوانی های ساده و قابل تشخیص قفل سخت افزاری. بسیاری از برنامه نویسان فقط هنگام ورود به برنامه قفل سخت افزاری را چک می کنند و در طول اجرای برنامه کمتر از قفل سخت افزاری استفاده می کنند؛ این نحو فراخوانی نه تنها باعث می شود استفاده کننده نهایی (با اجرا کردن کپی های متعدد با استفاده از یک قفل سخت افزاری) امکان سوء استفاده از نرم افزار را داشته باشد، بلکه کرک کردن چنین نرم افزاری هم به راحتی انجام شود.
- فراخوانی های متعدد و توزیع شده بهتر از فراخوانی های مجتمع هستند. برای فراخوانی توابع موجود در قفل سخت افزاری، سعی کنید فراخوانیها را به صورت توزیع شده در تمام کد پخش کنید و از اجتماع آنها در یک نقطه خودداری کنید. تجمع توابع امنیتی در یک یا چند نقطه خاص کار را برای Cracker بسیار ساده می کند و برعکس پراکنده کردن آنها در طول برنامه کار را بسیار دشوار خواهد کرد.
- عدم استفاده از کدنویسی ساخت یافته در استفاده از قفل سخت افزاری. نوشتن کد به صورت ساخت یافته، استفاده از کلاسها، اشیاء و نوشتن کدهای مجزا در unit های جداگانه و تفکیک کردن آنها بر اساس کاری که انجام می دهند، عملی است که در برنامه نویسی به آن توصیه اکید شده است؛ اما در مورد توابع امنیتی و چک کردن قفل سخت افزاری، به آن توصیه نمی شود. سعی کنید برای توابع امنیتی یک شیء خاص بوجود نیاورید که کار آن چک کردن قفل سخت افزاری باشد، یا اینکه توابع امنیتی و استفاده از قفل سخت افزاری را در یک unit مخصوص قرارندهید و تا می توانید کار بررسی قفل سخت افزاری را پراکنده کرده و در سطح کد پخش کنید. حتی اگر مجبورید برای چک کردن قفل سخت افزاری یک تابع خاص نوشته و آن را بارها فراخوانی کنید، سعی کنید این تابع را به دفعات و با امضاهای متفاوت و کدهای متفاوت نوشته و سپس در جاهای مختلف از این توابع استفاده کنید.
- نوشتن کدهای هرز. می توانید برای گمراه کردن هرچه بیشتر Cracker ، مقادیر تصادفی را در قفل ذخیره کرده و در جاهای دیگری این مقادیر را خوانده و بدون داشتن هدف خاصی نتایج را در فرمولهایی وارد کرده و در نهایت آنها را به حال خود رهاکنید. یا به دفعات توابع موجود در قفل سخت افزاری را اجرا کرده و خروجی آنها را نادیده بگیرید.
- چک کردن صحت نرم افزارهای رابط قفل سخت افزاری. در بیشتر موارد شرکت های تولید قفل سخت افزاری یک رابط نرم افزاری به صورت یک فایل کتابخانه ای پویا (DLL یا OCX ) در اختیار شما قرار می دهند که با استفاده از توابع موجود در آنها می توانید از قفل سخت افزاری استفاده کنید. ممکن است Cracker ها پس از انتشار نرم افزار شما این فایلها را دستکاری کنند. برای حصول اطمینان از صحت فایلهای رابط، بایستی از عدم دستکاری یا جایگزینی این فایلها مطمئن شوید. بدین منظور می توان از الگوریتم تولید CRC استفاده کرد.
- وقت خود را برای مخفی کاری با استفاده از کدهای کثیف تلف نکنید. بعضی از برنامه نویسها برای اینکه قسمتهای امنیتی و مهم کدهای خود را مخفی کنند، به نوشتن کدهای نامفهوم و ناخوانا اقدام می کنند؛ تعریف متغیرهای غیر مورد نیاز، حلقه های تو در تو و مبهم، عملیات انتساب بیهوده و … از نمونه های این گونه کد نویسی به شمار می روند. اما باید بدانیم که کامپایلرها معمولاً به هنگام بهینه سازی کد، این گونه کدهای غیر ضروری را حذف کرده و کدهای اسمبلی واضحی را تولید می کنند.
- سعی کنید حتی الامکان در برنامه نهایی خود سویچ های مخفی برای امکانات امنیتی برنامه نداشته باشید؛ چرا که در صورت کشف آنها کل امنیت نرم افزار شما به خطر می افتد.
- طمینان از عدم وجود اطلاعات Debug در کد نهایی برنامه. بسیاری از محیط های توسعه نرم افزار، برای سهولت در امر دیباگ برنامه، اطلاعات دیباگ را در کد اجرایی وارد می کنند. به خاطر داشته باشید آخرین باری که برنامه را کامپایل می کنید، گزینه های کامپایلر خود را طوری تنظیم کنید که این گونه اطلاعات در کد اجرایی نهایی قرار نگیرد.
- پرهیز از انجام مقایسه های صحیح و ساده. بعد از خواندن متغیری از حافظه قفل سخت افزاری، برای مقایسه آن از عبارتهای پیچیده اعشاری به جای مقایسه صحیح استفاده کنید. مثلاً فرض کنید می خواهید، مقایسه متغیری را با عدد ۱۲۱ انجام دهید:
if (t == ۱۲۱) …
به جای آن می توانید از کد زیر استفاده کنید:
if ((t + ۱۵۴) / ۵ == sqrt(t) * ۴ + ۱۱) …
استفاده از چندین عملیات اعشاری ممیز شناور، ساختارهای پیچیده ای از کد ماشین بوجود می آورد که درک آن را برای Cracker بسیار دشوار می کند.
عملیاتی که پس از کشف یک حمله می توان انجام داد:
می توانید در ذهن خود حالتی را تصور کنید که در نرم افزار خود راهکاری را در نظرگرفته اید که در صورت بروز یک حمله از طرف Cracker این حالت را تشخیص می دهد؛ قطعاً در این گونه موارد به دنبال راهکاری برای مقابله هستید؛ عکس العملی که نرم افزار شما انجام خواهد داد بستگی به سیاست کاری شما خواهد داشت، اما توجه به موارد زیر می تواند در این زمینه به شما کمک کند:
واکنش دیرهنگام:
تا حد امکان واکنش در قبال عمل Crack را به تأخیر بیاندازید به عنوان مثال واکنش شما می تواند سکوت در موقعیت فعلی و اجرا نشدن برنامه در دفعه بعد باشد.
مخفی کردن:
مخفی کردن رابطه بین علت و تأثیر، ایده جالب و کارآمدی است که می توانید پس از کشف حمله انجام دهید. در این روش، برنامه را طوری طراحی کنید که یک تغییر کوچک در برنامه شما یا در روند چک کردن قفل سخت افزاری، باعث تغییر پارامترهای متنوع دیگری شود که در ادامه کار، یکی از آنها باعث لو رفتن عملیات Crack می شود. به عنوان مثال متغیرهایی را از قفل سخت افزاری خوانده و ذخیره می کنید و یا بر روی آنها عملیات انجام می دهید که ظاهراً استفاده خاصی از آنها نمی شود؛ اما همین متغیرهای نه چندان مهم در جاهای دیگر برنامه باعث بروز تأثیراتی بر روند اجرای برنامه می شوند. مثلاً متغیری را در حافظه قفل نوشته و در آغاز برنامه آن را از قفل خوانده و در جایی ذخیره می کنید. این متغیر تنها زمانی استفاده می شود که می خواهید فرم خاصی را نمایش دهید یا قصد پرینت گرفتن را دارید؛ در اینجاست که مقدار این متغیر بکار می آید و می توانید از تأثیر مقدار آن متغیر استفاده کنید. مسلم است که در برنامه Crack شده، چون این متغیر قبلاً از قفل سخت افزاری خوانده نشده است، این تأثیر به خوبی بروز می کند و برنامه در اینجا به درستی کار نخواهد کرد.
تحریف نتایج:
برنامه نباید بلافاصله پس از آشکار شدن یک حمله از حالت اجرا خارج شود یا پیغامی مبنی بر Crack شدن صادر کند؛ بلکه باید به کار خود ادامه داده و در جاهای مختلف فقط نتایج را بی معنی کند. مثلاً باعث شود جمع مبلغ یک فاکتور غیرواقعی شود.
محدود سازی عملیات برنامه:
مقابله با حمله می تواند به از کارافتادن برخی از امکانات نرم افزار محدود شود. مثلاً عملیات Print ، Save و یا Backup را غیر فعال کند. این کار باعث می شود کشف محدودیتهای بوجود آمده تا دفعات بعدی که کاربر از آن استفاده می کند، به تأخیر بیفتد.