کرک ویندوز ۹۵ با ghidra
![](https://mrpythonblog.ir/wp-content/uploads/2023/04/2-43-880x586-1.webp)
یکی از ملموس ترین کاربرد های مهندسی معکوس برای هر ایرانی ، کرک کردن نرم افزار ها و سیستم عامل هاست . تو این قسمت آماده ایم که برگردیم به دوران ویندوز ۹۵ و ببینیم امنیت این سیستم عامل در اون زمان در چه حد بوده . نهایتا میبینیم که چطوری میشه با نرم افزار قدرتمند ghidra الگوریتم بررسی صحت سریال ویندوز ۹۵ رو دور زد و اون رو فعال کرد . با ما همراه باشید …
گیدرا ، یک خفن به تمام معنا
نرم افزار گیدرا ( به قول امیرحسین قیدرا 🙂 ) یک مجموعه کامل برای مهندسی معکوس نرم افزاره که به زبان جاوا توسعه داده شده . یکی از نکات بحث برانگیز این ابزار ، سازنده اونه . این نرم افزار توسط آژانس امنیت ملی آمریکا (NSA) توسعه داده شده . در ابتدا به صورت داخلی از اون استفاده میکردند ولی از یه جایی به صورت عمومی و متن باز این نرم افزار رو منتشر کردند .
برای نصب این نرم افزار میتونید از آموزش های موجود روی اینترنت استفاده کنید . خیلی پیچیدگی نداره . یعنی اصلا نداره فقط باید نسخه جاوا مورد نیازش رو داشته باشید روی سیستمتون . به راحتی میتونید روی لینوکس ، ویندوز یا حتی مک داشته باشیدش . برای دانلود گیدرا به سایت رسمی اون مراجعه کنید
ویندوز ۹۵
به نقل از ویکی پدیا ، ویندوز ۹۵ در سال ۱۹۹۵ منتشر شد و اگه بخوایم این سیستم عامل رو نصب کنیم ، نیازمند یک سریال به نام Product Key هستیم در غیر این صورت این سیستم عامل نصب و فعال سازی نمیشه . هدف ما تو این پست آموزشی اینه که بتونیم الگوریتم تشخیص کلید سریال این ویندوز رو پیداکنیم و در نتیجه بفهمیم چطوری میتونیم کلید هایی تولید کنیم که ویندوز ۹۵ اون ها رو معتبر میدونه .
ویندوز ۹۵ در ورژن های مختلفی منتشر شد (ویکی پدیا) :
- Windows 95 – original release
- Windows 95 A – included Windows 95 OSR1 slipstreamed into the installation
- Windows 95 B (OSR2) – included several major enhancements, Internet Explorer (IE) 3.0 and full FAT32 file system support
- Windows 95 B USB (OSR2.1) – included basic USB support
- Windows 95 C (OSR2.5) – included all the above features, plus IE 4.0; this was the last 95 version produced
نصب و اجرای ویندوز ۹۵
برای نصب ویندوز ۹۵ از مجازی ساز استفاده میکنیم . این سیستم عامل خیلی سبکه و به راحتی به ۶۴ مگابایت حافظه ram و ۲ گیگابایت حافظه جانبی راه میفته بنابراین نگران این موضوع نباشید .
برای اینکار من از نرم افزار Virtual Box استفاده میکنم ولی شما میتونید از Vmware هم استفاده کنید . نکته ای که باید بدونید اینه که اکثر iso های ویندوز ۹۵ به خودی خود قابل بوت شدن نیستند . درواقع ویندوز ۹۵ خودش این قابلیت رو نداره که به تنهایی بتونه خودشو بوت کنه بلکه باید از طریق یک سیستم عامل دیگه (معمولا DOS) بیایم و دستی هارد دیسک مون رو فرمت کنیم و فایل های ویندوز ۹۵ رو کپی کنیم داخلش تا بتونه بوت بشه . برای اینکه درگیر این دردسرا نشیم ، من یه نسخه ای ازش رو پیدا کردم که خودش خودکار بوت میشه . از این لینک دانلود کنید :
بعد از اینکه فایل رو دانلود کردیم ، یک ماشین مجازی جدید داخل Virtual Box تعریف میکنیم
شروع فرایند مهندسی معکوس
خب تا اینجا تونستیم ویندوز ۹۵ رو بالا بیاریم و برسیم به صفحه ای که ازمون سریال میخواد . حالا کار اصلی ما شروع میشه . پیدا کردن الگوریتم بررسی سریال .
خب چی داریم ؟ یه فایل iso از ویندوز ۹۵ داریم . چطوری شروع کنیم ؟ ما میدونیم که فایل های iso خودشون مثل یک فرمت آرشیو هستند . یعنی تو دلشون پر از فایلای دیگس . پس بیاین اول این فایل iso ویندوز ۹۵ رو استخراج کنیم ببینیم چی توشه . برای اینکار میتونیم از winrar یا ابزار های مشابه دیگه استفاده کنیم . من با winrar اینکارو میکنم . خیلی راحت نرم افزار winrar رو باز میکنیم و سپس فایل iso رو drag & drop میکنیم داخلش و گزینه Extract روی نوار ابزار winrar رو میزنیم :
با اجرای دستور بالا ، فایل iso در مسیر /media/win95_extracted استخراج خواهد شد . همچنین میتونین از ابزار 7zip استفاده کنید (اگه نصبه رو لینوکستون) :
7z x win95_2.iso
بعد از اینکه فایل استخراج شد با چنین محتوایی رو به رو میشیم :
خب نیازی به توضیح نیست که اول باید ای فایل های cab رو استخراج کنیم تا dll فایل ها و فایل های اجرایی داخلشون نمایان بشن . الگوریتم بررسی صحت سریال احتمالا تو یکی از همین فایل ها پیاده سازی شده .
برای استخراج کردن فایل های cab اگر داخل ویندوز هستیم ، Winrar عزیز باز کارمون رو راه میندازه ولی میتونیم از ابزار cabextract داخل لینوکسمون استفاده کنیم . برای نصب این ابزار از مدیریت بسته لینوکسمون استفاده میکنم . چون من Ubuntu دارم از دستور apt برای نصب استفاده میکنم :
sudo apt install cabextract
حالا برای استخراج تمام فایل های cab داخل پوشه WIN95 از دستور زیر استفاده میکنیم :
cabextract *
بعد ازعملیات استخراج متوجه میشیم که پوشه WIN95 پر میشه از فایل های EXE و DLL . ما میدونیم فایل های EXE فایل های اجرایی داخل ویندوز هستند . بنابراین داخل این فایل ها میتونه توابع یا به طور کلی کد هایی نوشته شده باشه که کارشون بررسی صحت سریاله .
همینطور میدونید فایل های DLL در ویندوز ، شامل یک سری توابع از پیش تعریف شده هستند که نرم افزار های دیگه میتونن حین اجرای خودشون این فایل های DLL رو import کنن تا از توابع داخل اونا استفاده کنن ( یه چیزی مثل همون کتابخونه ها هستند ) . بنابراین یک احتمال دیگه اینه که الگوریتم بررسی صحت سریال داخل یکی از فایل های DLL پیاده سازی شده و حین فرایند نصب برنامه SETUP.EXE از این فایل DLL استفاده میکنه تا صحت سریال ورودی رو بررسی کنه .
این صحبتا در حد حدسیات خوبه ولی چجوری مطمئن بشیم ؟
یکی از تکنیک های پرکاربرد در فرایند مهندسی معکوس ، استخراج رشته ها (String) از فایل های مورد هدف هستش.همینطور که کمی بالاتر دیدید ، داخل ماشین مجازی ما تا این پنجره در نصب ویندوز ۹۵ پیش رفتیم :
به عنوان بزرگ بالای پنجره دقت کنید :
Certificate of Authenticity
همین متن میتونه سرنخی باشه برای ما تا برسیم به فایلی که پیاده سازی الگوریتم صحت سریال رو داخلش داره . پنجره ای که مشاهده میکنید جوریه که اگر سریال اشتباه وارد کنیم و گزینه “< Next” رو بزنیم ، همون موقع به ما یه خطا نشون میده که آقا این سریال شما اشتباهه . خب مسلمه که کد هایی که حین نمایش این مرحله از نصب دارن اجرا میشن ، به شکلی الگوریتم بررسی صحت سریال رو فراخونی میکنن و از این طریق میفهمن سریال ما صحیح بوده یا نه .
یادتونه همه ی محتوای پوشه WIN95 رو استخراج کردیم ؟ بیاید داخلش بگردیم ببینیم توی کدوم یکی از فایل هاش عبارت “Certificate of Authenticity” وجود داره . اون فایل میتونه یه ارتباطی با تابع مورد نظرمون داشته باشه .
برای جستوجو داخل فایل ها از ابزار grep در لینوکس استفاده میکنیم (میتونید داخل ویندوز هم نصبش کنید) . فکر میکنم کم پیش میاد کسی با این ابزار آشنا نباشه ولی grep میتونه برای ما داخل فایل ها دنبال الگوهایی بگرده که با عبارات با قاعده (Regular Expression) هایی که ما وارد میکنیم همخونی داشته باشن . برای جستوجوی عبارت مورد نظر همینجور که در مسیر پوشه WIN95 هستیم از دستور زیر استفاده میکنیم :
grep -ilr "certificate of authenticity"
سوییچ i مخفف ignore case مشخص میکنه که جستوجو حساس به حروف کوچیک بزرگ نباشه . l مشخص میکنه در خروجی فقط اسم فایل هایی که عبارت مورد نظر داخلشون پیدا شد رو بنویسه و r مشخص میکنه عمل جستوجو رو به صورت بازگشتی انجام بده به این معنی که اگه پوشه ای در این مسیر وجود داره داخل اون پوشه رو هم ببینه و اگر پوشه هایی داخل اون پوشه هه بودن داخل اونارم چک کنه و همینجور بره جلو .
خروجی به صورت زیر خواهد بود :
طبق چیزی که خروجی میگه جستوجوی ما محدود شد به ۴ فایل زیر :
license.hlp
setupx.dll
suwin.exe
MSAGENT.EXE
با کمی بررسی متوجه میشیم license.hlp به دردمون نمیخوره چون فایل اجرایی و dll نیست اصلا . در نتیجه کد اجرایی نمیتونه داخلش باشه . ما دنبال فایلی میگردیم که الگوریتم توش پیاده شده باشه .
بیاین با بررسی فایل suwin.exe شروع کنیم .
شروع کار با ghidra
بعد از اینکه ghidra رو اجرا کردیم یک پروژه جدید تعریف میکنیم . برای اینکار از منوی File گزینه new project رو انتخاب میکنیم یا کلید های میانبر CTRL + N رو فشار میدیم .
در پنجره اول انتخاب میکنیم که میخوایم یک پروژه بدونه اشتراک گذاری داشته باشیم :
حالا پروژه ما آمادس . باید فایل هایی که میخوایم روشون کار کنیم رو وارد پروژه کنیم (import) . برای اینکار میتونیم از منوی فایل گزینه Import File رو بزنیم یا اینکه کلید میانبر i رو فشار بدیم .
بعد از اینکه فایل suwin.exe رو انتخاب کردیم ، ghidra خودش میاد حدس میزنه فرمت فایل چیه و معماری پردازنده و این حرفاش چجوریه . طبق معمول درست تشخیص داده و بدونه تغییر چیزی OK میزنیم :
در مرحله بعد وارد Code Browser گیدرا میشیم . Code Browser شامل طیف وسیعی از ابزار های مورد نیاز برای مهندسی معکوس (Decompiler , Disassembler , Hex Editor , …)هست .ازمون میپرسه که فایل شما آنالیز نشده میخواین آنالیز بشه ؟ روی Yes کلیک میکنیم و تنظیمات پیشفرض خودشو انتخاب میکنیم تا آنالیزش شروع بشه :
نوار پیشرفت گوشه پایین سمت راست Code Browser نشون میده چقدر از آنالیز انجام شده . صبر میکنیم تا آنالیز کاملا به اتمام برسه .
خب حالا میتونیم بررسی مونو شروع کنیم . یه راه اینه که بیایم دوباره عبارت “Certificate of Authenticity” رو داخل فایل جستوجو کنیم و ببینیم چجوری ازش استفاده شده تا با بررسی کد های فایل ببینیم با چه فراخونی و به کجا تونسته صحت سریال رو بررسی کنه . ولی خب این راه خیلی طولانی میشه الان بخوایم بریم . یه راه ساده تر انتخاب میکنیم و اونم یه راه آزمون خطایی 🙂
بیاین حدس بزنیم اسم تابعی که بررسی میکنه سریال صحت داره یا نه چیه و اون حدسمون رو داخل فایل جستوجو کنیم ببینیم وجود داره یا نه . داخل Code Browser یک پنجره ای هست به نام Symbol Tree . کاری که گیدرا میکنه اینه که میاد تمام توابع ، کلاس ها ، برچسب ها ، اسم توابعی که از کتابخونه های مختلف import شدن , … رو تلاش میکنه تا تشخیص بده . هرچی از این اسمارو که تونسته تشخیص بده میتونیم داخل پنجره Symbol Tree مشاهده کنیم :
همینطور که میبینید این پنجره اومده اسم های تشخیص داده شده توسط گیدرا رو براتون دسته بندی کرده . مثلا داخل دسته Functions شما توابعی رو میبینید که گیدرا تشخیص داده . یا داخل imports توابعی رو مشاهده میکنید که این فایل از DLL فایل های دیگه import کرده . اگه دقت کنید پایین پنجره Symbol Tree یک نوار جستوجو وجود داره که میتونیم ازش استفاده کنیم تا با آزمون و خطا اسم تابعی که سریال رو چک میکنه رو بررسی کنیم .
عبارت هایی مثل Check , validate , … رو جستوجو میکنیم .
اگر اینکارو انجام بدیم متوجه میشیم که تابعی وجود داره به نام PIDValidate :
همینطور که پنجره Symbol Tree به ما نشون داد ، تابع PIDValidate از فایل SETUPX ایمپورت شده . اگه یادتون باشه داخل اون فایلایی که جستوجو کردیم یه فایلی بود به اسم setupx.dll . این همونه . بنابراین تا اینجا تابع بررسی صحت سریال رو پیدا کردیم و حتی میدونیم تو کدوم فایل هست .
فایل setupx.dll رو دقیقا به روش قبل داخل گیدرا import میکنیم و داخل code browser اونو آنالیز میکنیم .
داخل Code Browser به دو پنجره نیاز داریم برای تحلیل الگوریتم . یکی پنجره Decompiler که میاد کدهای داخل فایل رو به زبان سی دیکامپایل میکنه و پنجره Functions که یه لیستی از توابع داخل فایل رو به ما نشون میده .
اگه هر کدوم از این پنجره ها رو نداریدش ، کافیه برای باز کردن کافیه از منوی Window داخل Code Browser انتخابشون کنید تا باز بشن .
داخل پنجره Functions لیستی از تمام توابعی که گیدرا داخل فایل setupx.dll شناسایی کرده رو میبینیم :
حالا یک بار روی تابع PIDVALIDATE داخل این پنجره کلیک میکنیم . این باعث میشه تا داخل پنجره Decompiler ، گیدرا بیاد و کد این تابع رو برامون دیکامپایل کنه . بعد از انتخاب تابع اگه بریم به پنجره Decompiler کد دیکامپایل شده تابع توسط گیدرا رو مشاهده میکنیم :
بر خلاف تصور میبینیم که اونقدرا هم کد پیچیده ای نیست (طبیعیه داریم راجع به امنیت بیش از ۲۰ سال پیش صحبت میکنیم) .
کد دیکامپایل شده تابع PIDVALIDATE رو ببینیم :
undefined2 __stdcall16far PIDVALIDATE(undefined4 param_1,undefined4 param_2)
{
undefined2 uVar1;
uVar1 = thunk_FUN_1000_0b36(param_2,(int)&DAT_1170_1170);
switch(uVar1) {
case 1:
case 3:
case 5:
uVar1 = FUN_10a0_04dc(param_1);
break;
case 2:
case 4:
case 6:
uVar1 = FUN_10a0_045e(param_1);
break;
case 7:
case 8:
uVar1 = FUN_10a0_060a(param_1);
break;
case 9:
uVar1 = FUN_10a0_04fa(param_1);
break;
default:
uVar1 = 0;
}
return uVar1;
}
در خط ۳ متغییری به نام uVar1 تعریف شده که نوعش رو گیدرا نتونسته تشخیص بده ولی میدونه که هرچی هست ۲ بایته (اون عدد ۲ آخر undefined همینو نشون میده) . و نهایتا آخر تابع PIDVALIDATE همین uVar1 به عنوان خروجی داده شده . همینطور که از اسم PIDVALIDATE بر میاد احتمالا یه سریال رو تو ورودی میگیره و تو خروجی میگه آیا معتبر بود یا نه . پس این uVar1 به شکلی مشخص میکنه آیا این معتبر هست یا نه .
در خط ۵ میبینیم که یک فراخونی انجام شده به تابع thunk_FUN_1000_0b36 و پارامتر دوم تابع PIDVALIDATE بهش پاس داده شده .
بریم ببینیم داخل تابع thunk_FUN_1000_0b36 چیه و چرا فراخونی شده . برای اینکار داخل همون Decompiler گیدرا روی اسم این تابع دوبار کلیک کنید تا برید به اون تابع .
سورس این تابع (بعد از کمی مرتب سازی و کامنت گذاری برای درک راحت تر) :
int FUN_1000_0b36(char * param_1)
{
char *pbVar1;
char bVar2;
char bVar3;
int result;
char *pbVar5;
undefined2 uVar6;
uVar6 = (undefined2)((ulong)param_1 >> 0x10);
pbVar5 = param_1;
result = 0;
/* copying param_1 characters to pbVar2 */
/* and ignore them if they are '' or '\t' */
do {
do {
pbVar1 = pbVar5;
pbVar5 = pbVar5 + 1;
bVar2 = *pbVar1;
} while (bVar2 == '');
} while (bVar2 == '\t');
/* ignore '-' or '+' if exist */
if ((bVar2 != '-') && (bVar3 = bVar2, bVar2 != '+')) goto LAB_1000_0b57;
while( true ) {
pbVar1 = pbVar5;
pbVar5 = pbVar5 + 1;
bVar3 = *pbVar1;
LAB_1000_0b57:
if(('9' < bVar3) || (bVar3 < '0')) break;
result = result * 10 + (bVar3 - '0');
}
if (bVar2 == 0x2d) {
result = -result;
}
return result;
}
همینطور که مشخص شده این تابع یک رشته (اشاره گر به کاراکتر) در ورودی میگیره و یک عدد صحیح خروجی میده .
آیا تعداد کاراکتراشو میشماره ؟ تبدیلش میکنه به عدد صحیح ؟ بیاین بررسی کنیم .
تو خط ۶ یک متغییر عدد صحیح به نام result تعریف شده که به عنوان خروجی همین برمیگرده بیرون از تابع . همچنین یه متغییر pbVar1 داریم که یک اشاره گری هست به یک کاراکتر . یه متغییر bVar2 هم داریم که از نوع کاراکتره یعنی یک تک کاراکتر داخلش ذخیره میشه . همچنین یک متغییر pbVar5 داریم که یک اشاره گر به کاراکتره و در خط ۱۳ دقیقا برابر ورودی تابع قرار داده شده . یعنی دقیقا مقدار همون رشته ای که داخل param_1 هست داخل اینم قرار میگیره .
از خط ۱۶ تا ۲۴ رو دقت کنید :
/* copying param_1 characters to pbVar2 */
/* and ignore them if they are '' or '\t' */
do {
do {
pbVar1 = pbVar5;
pbVar5 = pbVar5 + 1;
bVar2 = *pbVar1;
} while (bVar2 == '');
} while (bVar2 == '\t');
کار این چند خط اینه که فضاهای خالی اول رشته ای که تابع تو ورودی گرفت رو حذف کنن (اگه وجود داره) .
دوتا do while تو در تو زده شده . درونی ترین do while رو دقت کنید . هر دفعه مقدار pbVar1 (که اشاره گر به کاراکتر بود) برابر قرار داده میشه با pbVar5 (که به رشته ورودی اشاره میکرد) . بنابراین تا اینجا pbVar1 اشاره میکنه به اولین کاراکتر موجود در رشته ورودی .
در خط بعد pbVar5 یکی افزایش پیدا کرده . یعنی اشاره میکنه به خونه بعدی حافظه . این یعنی از این به بعد اشاره میکنه به کاراکتر بعدی رشته ورودی .
در خط بعدی هم bVar2 ( که یک کاراکتر بود ) برابر قرار داده میشه با اون کاراکتری که pbVar1 داشت بهش اشاره میکرد (همون کاراکتر اول رشته ورودی) .
حالا شرط حلقه چک میکنه آیا این bVar2 برابر باشه با ” (فضای خالی) .
گرفتید چی شد . این حلقه تاجایی که کاراکترای اول رشته ورودی فضای خالیه تکرار میشه و اشاره گر ورودی هی یکی یکی میره جلو تا فضاهای خالی رو رد کنه . در نتیجه انگار داریم فضاهای خالی رو از اول رشته ورودی حذف میکنیم . اون do while بیرونی هم دقیقا همینکارو انجام میده ولی برای کاراکتر tab . هرچی کاراکتر تب اول رشته ورودی هست رو ازش رد میشه .
دقیقا بعد از این حلقه ها ، تک کد زیر رو داریم :
if ((bVar2 != '-') && (bVar3 = bVar2, bVar2 != '+')) goto LAB_1000_0b57;
while( true ) {
pbVar1 = pbVar5;
pbVar5 = pbVar5 + 1;
bVar3 = *pbVar1;
LAB_1000_0b57:
if (('9' < bVar3) || (bVar3 < '0')) break;
result = result * 10 + (bVar3 - '0');
}
if (bVar2 == 0x2d) {
result = -result;
}
return result;
}
یه حلقه while true (خط ۲) وجود داره که در هر دور حلقه ، اول کار یکی از کاراکتر های رشته ورودی (pbVar5) قرار میگیره داخل bVar3 و اشاره گر به رشته ورودی (pbVar5) یکی میره جلو تا در دور بعدی حلقه کاراکتر بعدی مورد بررسی قرار بگیره .
در خط هشتم چک شده که کاراکتر bVar3 یک عدد از صفر تا نه باشه .
اگه خط ۹ رو دقت کنید متوجه میشید این تابع چیکار میکنه . در خط ۹ اومده اون کاراکتری که از رشته ورودی برداشت (bVar3) رو از نظر عددی اضافه میکنه به result .
دیدید ؟ کار این تابع این بود که رشته ورودیش رو به عدد صحیح تبدیل کنه . نهایتا متوجه میشیم این یک تابع atoi هستش . تبدیل استرینگ به عدد صحیح .
خب بیاید برگردیم سر همون تابع PIDVALIDATE و حالا که متوجه شدیم تابعی که اول کار فراخونی میشد چیه :
undefined2 __stdcall16far PIDVALIDATE(undefined4 param_1,undefined4 param_2)
{
undefined2 uVar1;
uVar1 = thunk_FUN_1000_0b36(param_2,(int)&DAT_1170_1170);
switch(uVar1) {
case 1:
case 3:
case 5:
uVar1 = FUN_10a0_04dc(param_1);
break;
case 2:
case 4:
case 6:
uVar1 = FUN_10a0_045e(param_1);
break;
case 7:
case 8:
uVar1 = FUN_10a0_060a(param_1);
break;
case 9:
uVar1 = FUN_10a0_04fa(param_1);
break;
default:
uVar1 = 0;
}
return uVar1;
}
تا اینجا متوجه شدیم . اون uVar1 که اول کاره یه متغییر عدد صحیح (int) هستش . چون تو خط ۵ تابع atoi روی پارامتر دوم تابع pidvalidate صدا زده میشه و خروجیش (که عدد صحیحه) داخل uVar1 ذخیره میشه .
نکته اینجاست که گیدرا بعضی چیزا رو درست نمیتونه تشخیص بده . مثلا دیدی که تابع atoi رو نتونسته بود تشخیص بده . ما باید با تشخیص های خودمون کمکش کنیم تا تحلیلش از کد بهتر بشه . حالا که atoi رو شناسایی کردیم باید به گیدرا بگیم که اون تابع thunk_FUN_1000_0B36 که صدا زده میشد atoi هستش . برای اینکار روی اسم تابعی که میخوام تغییرش بدیم کلیک راست میکنیم و گزینه Edit Function Signature رو انتخاب میکنیم . پنجره ای باز میشه که میتونیم Signature یا prototype تابع مورد نظر رو تغییر بدیم :
بعد از اینکه تغییرات رو اعمال کردیم اگه دوباره به کد تابع pidvalidate نگاه کنیم متوجه میشیم یه سری چیزا تغییر کرده !! . مثلا دیگه اون متغییر iVar1 (uVar1 سابق)به عنوان خروجی بر نمیگرده . یه چیز اضافه تر داخلش تعریف شده :
دلیل این موضوع اینه که با توجه به اینکه نوع و مشخصات یکی از توابع رو برای گیدرا مشخص کردیم متوجه شد عه ! داشته اشتباه میکرده و الان تحلیل درست تری به ما از کد ارائه داد . میبینید یه معامله دو طرفس . هرچی ما بیشتر به درک گیدرا کمک کنیم اونم تحلیل بهتری به ما میده .
با توجه به تغییراتی که دادیم ، بار دیگه سورس کد pidvalidate رو داشته باشیم :
undefined2 __stdcall16far PIDVALIDATE(undefined4 param_1,char *param_2)
{
int iVar1;
undefined2 uVar2;
iVar1 = ATOI(param_2);
switch(iVar1) {
case 1:
case 3:
case 5:
uVar2 = FUN_10a0_04dc(param_1);
break;
case 2:
case 4:
case 6:
uVar2 = FUN_10a0_045e(param_1);
break;
case 7:
case 8:
uVar2 = FUN_10a0_060a(param_1);
break;
case 9:
uVar2 = FUN_10a0_04fa(param_1);
break;
default:
uVar2 = 0;
}
return uVar2;
}
در خط ۷ ، ورودی دوم تابع pidvalidate از string به عدد صحیح تبدیل شده و نتیجش ریخته شده توی iVar1 . حالا یه switch روی iVar1 زده شده و با یه سری مقادیر مقایسه شده . اینجور که مشخصه کد ما داخل switch به چهار بخش تقسیم شده . در واقع تو هر کدومشون یه تابع متفاوت صدا زده میشه .
اگر iVar1 برابر ۱ یا ۳ یا ۵ باشه ، فراخونی خط ۱۲ انجام میشه . اگر ۲ یا ۴ یا ۶ باشه فراخونی خط ۱۷ . اگر ۷ یا ۸ باشه فراخونی خط ۲۱ و اگر ۹ باشه فراخونی خط ۲۴ .
مجموعا چهارتا تابع متفاوت میتونه برای مقادیر مختلف iVar1 صدا زده بشه . حین صدا زدن این چهارتا تابع پارامتر اول تابع pidvalidate بهشون پاس داده شده . یه چیزیو میتونیم حدس بزنیم . از اونجایی که ویندوز ۹۵ ورژن های مختلفی داره ، هر ورژنی هم ساختار سریال و فرم سریال خاص خودشو داره . بنابراین برای هر ورژن ویندوز ۹۵ احتمالا تابع بررسی صحت سریال متفاوته . دلیل این switch میتونه همین باشه . احتمالا متغییر iVar1 مشخص میکنه چه ورژنی از ویندوز ۹۵ رو داریم استفاده میکنیم و با توجه به اون ، تابع بررسی صحت سریال صدا زده میشه و پارامتر اول تابع pidvalidate ( که احتمالا همون سریالی هست که کاربر وارد کرده ) بهش پاس داده میشه .
اگه برگردید و عکس صفحه ای که حین نصب ویندوز ۹۵ ازمون سریال میخواست رو ببینید متوجه میشیم فرم سریال اون نسخه ویندوز ۹۵ که ما داریم به این شکله :
aaaaa-OEM-bbbbbbb-ccccc
که اون عبارت OEM وسطش ثابته .
ما ایده ای نداریم که کدوم یکی از این ۴ تا تابع مال نسخه ای هست که ما داریم بنابراین بیاید به ترتیب بررسیشون کنیم . در ابتدا تابعی که در خط ۱۲ فراخونی شده رو بررسی میکنیم . سورس این تابع رو اگر در گیدرا ببینید به این صورت هستش :
همینطور که میبنیید داخلش یک تابع دیگه به اسم FUN_10a0_041a صدا زده شده . سورس این تابع به این صورت هستش :
bool __cdecl16far FUN_10a0_041a(char *param_1,undefined2 param_2)
{
char cVar1;
int iVar2;
int iVar3;
iVar2 = 0;
iVar3 = 0;
while( true ) {
cVar1 = *param_1;
if ((cVar1 < '0') || ('9' < cVar1)) break;
iVar3 = iVar3 + cVar1 + -0x30;
iVar2 = iVar2 + 1;
param_1 = param_1 + 1;
if (6 < iVar2) {
return iVar3 % 7 == 0;
}
}
return false;
}
ورودی اول این تابع از نوع رشته هست به نام param_1 که احتمالا همون سریاله .این تابع خیلی ساده اومده مجموع ۷ عدد اول سریال رو حساب کرده . اگر بر ۷ بخش پذیر بود ، مقدار true و اگر نه مقدار false رو برمیگردونه . پس اسم این تابع رو به روشی که قبل تر دیدیم داخل گیدرا تغییر میدیم و میزارمش mod_7 .
برگردیم به تابع pidvalidate :
undefined2 __stdcall16far PIDVALIDATE(undefined4 param_1,char *param_2)
{
int iVar1;
undefined2 uVar2;
iVar1 = ATOI(param_2);
switch(iVar1) {
case 1:
case 3:
case 5:
uVar2 = FUN_10a0_04dc(param_1); // mod_7 will be called
break;
case 2:
case 4:
case 6:
uVar2 = FUN_10a0_045e(param_1);
break;
case 7:
case 8:
uVar2 = FUN_10a0_060a(param_1);
break;
case 9:
uVar2 = FUN_10a0_04fa(param_1);
break;
default:
uVar2 = 0;
}
return uVar2;
}
فراخونی خط ۱۲ رو چک کردیم و فهمیدیم که تابع mod_7 داخلش صدا زده میشه . اگر سریالی وارد کنیم که مجموع ۷ رقم اولش به عدد ۷ بخش پذیره شرط این نوع سریال رو برقرار کردیم . ولی اگر تست کنیم متوجه میشیم کار نمیده بازم . پس یعنی این تابع بررسی صحت اون نسخه ای که ما استفاده میکنیم نبوده . بریم سراغ فراخونی خط ۱۷ . سورس این تابع به این صورته :
bool __stdcall16far FUN_10a0_045e(int param_1,undefined2 param_2)
{
bool bVar1;
int iVar2;
undefined2 unaff_SS;
char local_6 [3];
undefined local_3;
FUN_10b0_033e(3,param_1 + 6,param_2,local_6,unaff_SS); // copy 3 characters from serial to local_6
local_3 = 0;
iVar2 = ATOI(local_6);
if ((((iVar2 == 333) || (iVar2 == 444)) || (iVar2 == 555)) ||
(((iVar2 == 666 || (iVar2 == 777)) || ((iVar2 == 888 || (iVar2 == 999)))))) {
bVar1 = false;
}
else {
bVar1 = mod_7((char *)(param_1 + 10),param_2);
}
return bVar1;
}
داخل این تابع یک آرایه ۳ تایی از نوع کاراکتر داریم به اسم local_6 (خط ۷) . همچنین در خط ۱۰ یک تابعی صدا زده شده و این آرایه سه تایی local_6 یکی از پارامتراشه . همچنین دوتا از پارامترای دیگش ، پارامتر های خود تابع فعلی (param_1 , param_2) هستند . اگر فرض کنیم مثلا param_2 همون سریاله ، میشه حدس زد کار تابعی که در خط ۱۰ فراخونی شده اینه که سه کاراکتر از سریال رو کپی کنه داخل آرایه local_6 از اونجایی که اندازه این آرایه سه تا از نوع کاراکتره .اگر سورس این تابع رو بررسی کنیم متوجه میشیم همینکارو میکنه . تابعی که در خط ۱۰ صدا زده شده سه کاراکتر اول سریال رو کپی میکنه توی local_6
نهایتا اومده چک کرده سه کاراکتر اول سریال برابر ۲۲۲ ، ۳۳۳ ، ۴۴۴ ، ۵۵۵ ، ۶۶۶ ، ۷۷۷ ، ۸۸۸ یا ۹۹۹ نباشه . اگر بود false برمیگردونه . اگر نبود تابع mod_7 رو فراخونی میکنه . یعنی چک میکنه مجموع ۷ رقم اول سریال بخش پذیر به هفت هست یا نه . اگر این هم بخش پذیر بود true برمیگردونه .
بنابراین سریال مورد علاقه این تابع جوریه که اولا سه کاراکتر اولش ۴۴۴ ۵۵۵ ۶۶۶ و اینا نباشه . دوما مجموع هفت رقم اولش به ۷ بخش پذیر باشه .
متاسفانه همچین سریالی رو هم چک کردیم و متوجه شدیم اینم کار نمیده روی ویندوز ۹۵ ما . پس این تابع هم مال ورژن ما نیست .
برگردیم به pidvalidate که دیگه آخرای کاریم .
undefined2 __stdcall16far PIDVALIDATE(undefined4 param_1,char *param_2)
{
int iVar1;
undefined2 uVar2;
iVar1 = ATOI(param_2);
switch(iVar1) {
case 1:
case 3:
case 5:
uVar2 = FUN_10a0_04dc(param_1); // mod_7 will be called
break;
case 2:
case 4:
case 6:
uVar2 = FUN_10a0_045e(param_1);
break;
case 7:
case 8:
uVar2 = FUN_10a0_060a(param_1);
break;
case 9:
uVar2 = FUN_10a0_04fa(param_1);
break;
default:
uVar2 = 0;
}
return uVar2;
}
مرحله بعدی بررسی فراخونی خط ۲۱ . اگر داخل تابع صدا زده شده در خط ۲۱ رو ببینیم با موضوع جالبی رو به رو میشیم :
داخلش تابعی به اسم FUN_10a0_04fa صدا زده شده . دقت کنید در همون تابع pidvalidate هم فراخونی آخری به همین تابع بود . پس توی تابع pidvalidate فراخونی های خط ۲۱ و ۲۴ به یه جا میرسن نهایتا .
هورااا ما یه تابع دیگه بیشتر نداریم و این همون تابعیه که صحت سریال مارو بررسی میکنه . بریم سراغش :
bool __stdcall16far FUN_10a0_04fa(int param_1,undefined2 param_2)
{
bool bVar1;
int iVar2;
undefined2 unaff_CS;
undefined2 unaff_SS;
char local_a [3];
undefined local_7;
char local_6 [2];
undefined local_4;
char local_3;
iVar2 = LSTRLEN(unaff_CS,param_1);
if ((((iVar2 == 0x17) && (*(char *)(param_1 + 5) == '-')) && (*(char *)(param_1 + 9) == '-')) &&
((*(char *)(param_1 + 0x11) == '-' &&
(iVar2 = FUN_10b0_01c4(3,0x4f9d,(int)&DAT_1170_1170,param_1 + 6,param_2), iVar2 == 0)))) {
iVar2 = 0;
do {
local_3 = *(char *)(param_1 + iVar2);
if (local_3 < '0') {
return false;
}
if ('9' < local_3) {
return false;
}
iVar2 = iVar2 + 1;
} while (iVar2 < 5);
FUN_10b0_033e(3,param_1,param_2,local_a,unaff_SS);
local_7 = 0;
iVar2 = ATOI(local_a);
if ((iVar2 != 0) && (iVar2 < 0x16f)) {
FUN_10b0_033e(2,param_1 + 3,param_2,local_6,unaff_SS);
local_4 = 0;
iVar2 = ATOI(local_6);
if (((iVar2 < 3) || (0x5e < iVar2)) &&
(((*(char *)(param_1 + 10) == '0' && ('0' < *(char *)(param_1 + 0x10))) &&
(*(char *)(param_1 + 0x10) < '8')))) {
iVar2 = 0x12;
while ((local_3 = *(char *)(param_1 + iVar2), '/' < local_3 && (local_3 < ':'))) {
iVar2 = iVar2 + 1;
if (0x16 < iVar2) {
bVar1 = mod_7((char *)(param_1 + 10),param_2);
return bVar1;
}
}
}
}
}
return false;
}
ظاهرا که تابع بزرگیه ولی اصلا نگران نباشید . چند تا شرط و حلقه مهم توی این تابع برای سریال هست که باید رعایت بشه و ما بررسیشون میکنیم .
یه شرطی توی خط ۱۵ زده شده که به این صورته :
iVar2 = LSTRLEN(unaff_CS,param_1);
if ((((iVar2 == 0x17) && (*(char *)(param_1 + 5) == '-')) && (*(char *)(param_1 + 9) == '-')) &&
((*(char *)(param_1 + 0x11) == '-' &&
(iVar2 = FUN_10b0_01c4(3,0x4f9d,(int)&DAT_1170_1170,param_1 + 6,param_2), iVar2 == 0))))
ابتدا طول پارامتر ۱ که همون سریال هست بدست اومده . داخل شرط ، موارد زیر بررسی شده :
۱ – طول سریال (iVar2) برابر ۲۳ (در مبنای ۱۰) باشه . یک بار دیگه فرم سریال رو ببینیم :
aaaaa-OEM-bbbbbbb-ccccc
متوجه میشیم که واقعا ۲۳ کاراکتره (با احتساب کاراکتر “-” و عبارت OEM) . پس شرط اول به درستی برقرار میشه همیشه .
۲ – کاراکتر ششم سریال برابر “-” باشه که اینم برقراره اگه به ساختار سریال دقت کنید
۳ – کاراکتر دهم سریال برابر “-” باشه . برقراره
۴ – کاراکتر ۱۸ ام سریال برابر “-” باشه . برقراره
۵ – یه شرط دیگه هم چک شده که ربطی خیلی به کار ما نداره .
خب شروط این if تقریبا همیشه برقراره چون برمیگرده به ساختار سریال که ثابته اگه تمام کاراکترای سریال رو پر کنیم .
از شرط توی خط ۱۵ که بگذریم ، میرسیم به do … while که در خط ۱۹ قرار داره :
iVar2 = 0;
do {
local_3 = *(char *)(param_1 + iVar2);
if (local_3 < '0') {
return false;
}
if ('9' < local_3) {
return false;
}
iVar2 = iVar2 + 1;
} while (iVar2 < 5);
iVar2 که نقش متغییر حلقه رو داره در ابتدا در خط یک برابر صفر قرار داده شده و در هر دور حلقه یکی بهش اضافه میشه . داخل حلقه در هردور ، یکی از ۵ کاراکتر اول سریال برداشته میشه و در شروط داخل خط ۴ و ۷ چک میشه . شرط داخل خط ۴ چک میکنه آیا کد ascii کاراکتر کمتر از ۰ هست یا نه . اگر بود false برمیگرده و شرط خط ۷ چک میکنه آیا بزرگتر از 9 هست یا نه اگر بود false برمیگرده . به عبارتی چک میکنه که حتما ۵ کاراکتر اول سریال رقم عددی باشن . پس سریال ما باید طوری باشه که پنج کاراکتر اولش عدد باشه حتما .
بعد از حلقه do while میرسیم به شرطی که داخل خط ۳۲ چک شده :
FUN_10b0_033e(3,param_1,param_2,local_a,unaff_SS);
local_7 = 0;
iVar2 = ATOI(local_a);
if ((iVar2 != 0) && (iVar2 < 0x16f))
اگه دقت کنید در ابتدا یه تابعی صدا زده شده . این تابع دقیقا همونیه که در مورد قبلی برای کپی کردن سه کاراکتر اول سریال به کار میرفت . کار این تابع دقیقا مثل memcpy میمونه . یه آرایه مبدا میگیره و یک آرایه مقصد همچنین طول انتقال . سپس به اندازه طول انتقال (مثلا سه کاراکتر) از آرایه مبدا کپی میکنه به مقصد .
در اینجا این تابع صدا زده شده و طول انتقال ۳ هستش . یعنی سه کاراکتر میخواد کپی کنه از کجا به کجا ؟ از آرایه ای که رشته سریال داخلشه به آرایه local_a که یک آرایه سه تایی از نوع char هست . پس این فقط میاد سه کاراکتر اول سریال رو کپی میکنه توی local_a . بعد از اون local_a توسط تابع atoi به عدد صحیح تبدیل شده و نهایتا یه شرطی زده شده . تو این شرط چک کرده که سه کاراکتر اول سریال اولا صفر نباشه دوما کوچیکتر از ۳۶۷ در مبنای ۱۰ باشه .
بعد از این if در خط ۳۶ یک شرط دیگه چک شده که به صورت زیر هستش :
FUN_10b0_033e(2,param_1 + 3,param_2,local_6,unaff_SS);
local_4 = 0;
iVar2 = ATOI(local_6);
if (((iVar2 < 3) || (0x5e < iVar2)) &&
(((*(char *)(param_1 + 10) == '0' && ('0' < *(char *)(param_1 + 0x10))) &&
(*(char *)(param_1 + 0x10) < '8')))) {
همچنان در خط اول دوباره تابع کپی حافظه فراخونی شده . ایندفعه طول انتقال ۲ هستش . آرایه مقصد local_6 هستش که به عنوان یک آرایه ۲ تایی از کاراکتر تعریف شده . به عنوان مبدا هم آرایه ای که سریال داخلشه ، از کاراکتر چهارم سریال به بعده . یعنی به عبارتی کاراکتر های چهارم و پنجم سریال کپی میشن توی آرایه مقصد (local_6).
بعد این دو کاراکتر به عدد صحیح تبدیل شدن و در iVar2 ذخیره شدن . سپس چک شده که دو رقم چهارم و پنجم سریال (به عنوان یک عدد دورقمی) اولا کوچیکتر از ۳ باشن یا اینکه بزرگتر از 94 باشن ، دوما کاراکتر یازدهم سریال برابر ۰ باشه ، سوما کاراکتر هفدهم سریال بزرگتر از ۰ باشه و کوچیک تر از ۸ باشه .
به عنوان بررسی آخر میرسیم به حلقه while داخل خط ۴۰ :
iVar2 = 0x12;
while ((local_3 = *(char *)(param_1 + iVar2), '/' < local_3 && (local_3 < ':'))) {
iVar2 = iVar2 + 1;
if (0x16 < iVar2) {
bVar1 = mod_7((char *)(param_1 + 10),param_2);
return bVar1;
}
ابتدا متغییر iVar2 برابر 18 قرار داده شده . سپس یه حلقه while زده شده که در هر دور حلقه یکی از ۵ کاراکتر آخر سریال (۱۹ ام تا ۲۳ ) در متغییر local_3 ذخیره میشه و شرط زیر روشون چک میشه :
۱ – کد کاراکتری اون بزرگتر از کد کاراکتری “/” باشه
۲ – کد کاراکتری اون کوچیکتر از کد کاراکتری “:” باشه .
نهایتا وقتی هر ۵ تا کاراکتر آخر سریال سالم از این آزمون بیرون اومدن ، تابع mod_7 روی کاراکتر یازدهم به بعد سریال فراخونی شده که این به این معنیه که باید مجموع رقم های یازدهم تا هفدهم سریال بر ۷ بخش پذیر باشه . اگر اینطور بود نهایتا بالاخره 🙂 مقدار true از این تابع برمیگرده بیرون و سریال درسته .
این بود از تمام شروطی که چک میشد . بیاید جمع بندی کنیم .
ما چنین سریالی داشتیم :
AAABB-OEM-CDDDDDE-FFFFF
اگر تمام اون موضوعاتی که گفتیمو جمع بندی کنیم به این نتیجه میرسیم :
۱ – ۳ کاراکتر اول سریال (بخش A) باید کوچیک تر از ۳۶۷ باشه و صفر نباشه
۲ – کاراکتر های چهارم و پنجم سریال به عنوان یک عدد دورقمی (بخش B) باید کوچیکتر از ۳ باشن یا اینکه بزرگتر از ۹۴ باشن .
۳ – کاراکتر یازدهم سریال(C) باید برابر صفر باشه .
۴ – کاراکتر هفدهم سریال (E) باید بزرگتر از صفر و کوچیک تر از ۸ باشه .
۵ – کد کاراکتری هر کدوم از پنج کاراکتر آخر سریال (بخش F) باید بزرگتر از / و کوچیکتر از : باشه . دقت کنید از اونجایی که کد کاراکتری رقم های عدد بین / و : هست پس اگه پنج کاراکتر آخر سریال عددی باشه حله
۶ – مجموع ۷ کاراکتر یازدهم تا هفدهم سریال (C و بخش D و E) باید بر ۷ بخش پذیر باشه .
حالا با توجه به این شروط من مثلا یه سریال دستی درست کردم که باید معتبر باشه :
next رو میزنیم تا ببینیم درست حدس زدیم یا نه :
وووو میبینیم که بلههههه !:)
تونستیم سریالشو دور بزنیم.
ساخت keygen به زبان پایتون
به عنوان شیرین کاری آخر یک اسکریپت keygen به زبان پایتون میتونیم بنویسیم که برامون سریال معتبر تولید کنه :
from random import randint
# AAABB-OEM-CDDDDDE-FFFFF
output = ""
# constructing part A
output += str(randint(100,366))
# constructing part B
output += str(randint(95,99))
# constructing part C
output += "-OEM-0"
#constructing part E
part_e = randint (1 , 7)
# constructing part D
part_d = randint(10000 , 99999)
while ((part_d + part_e) % 7 != 0):
part_d = randint(10000 , 99999)
output += str(part_d) + str(part_e)
# constructing part F
output += "-" + str(randint(10000 , 99999))
print (output)
بعد از هر بار اجرای اسکریپت بالا یک سریال معتبر به صورت شانسی برای ما تولید میشه :
علاقه مند به دنیای کامپیوتر ها ...