90 KiB
دربارهی چالشهای CTF
تعریف کلی: CTF (Capture The Flag) (به فارسی: پرچم را تصاحب کن) یک نوع مسابقه امنیت سایبری است که در آن شرکتکنندگان (تیمها یا افراد) مهارتهای خود را در یافتن و بهرهبرداری از آسیبپذیریها، تحلیل سیستمها، رمزگشایی و حل مسائل پیچیده امنیت اطلاعات به چالش میکشند. 💻🕵️♀️
هدف اصلی:
هدف اصلی در CTF پیدا کردن "فلگ" (Flag) است. فلگ معمولاً یک رشته متنی خاص (مانند flag{this_is_your_flag}) است که در یک مکان پنهان (مثلاً در یک فایل، دیتابیس، یا خروجی یک برنامه آسیبپذیر) قرار دارد. با یافتن و وارد کردن این فلگ در سیستم مسابقه، تیم امتیاز کسب میکند.
دو نوع رایج CTF:
- رایجترین نوع: Jeopardy (ژئوپاردی) چالشها به صورت مستقل و در دستهبندیهای مختلف (مانند مهندسی معکوس، رمزنگاری، وب، فارنزیک) با امتیازات متفاوت ارائه میشوند. شرکتکنندگان هر چالشی را که میخواهند انتخاب کرده و فلگ آن را پیدا میکنند.
- پیچیدهتر و دینامیکتر: Attack-Defense (حمله-دفاع) هر تیم یک سرور (یا مجموعهای از سرویسها) را در اختیار دارد که باید از آن در برابر حملات تیمهای دیگر دفاع کند و همزمان به سرورهای حریف حمله کرده و فلگهای آنها را به دست آورد. این نوع CTF مهارتهای دفاعی و تهاجمی را همزمان میسنجد.
چرا CTF؟
- یادگیری و تمرین مهارتهای امنیت سایبری در محیطی عملی و کنترلشده.
- شناسایی و جذب استعدادها در حوزه امنیت.
- افزایش آگاهی نسبت به آسیبپذیریهای رایج و تکنیکهای نفوذ. به طور خلاصه، CTF یک ورزش ذهنی هیجانانگیز در دنیای سایبری است که به شما کمک میکند مهارتهای عملی خود را در امنیت اطلاعات بهبود ببخشید. 🧠🛡️
دستهبندیهای رایج چالشها در CTF (سبک Jeopardy):
در مسابقات CTF با فرمت Jeopardy، چالشها معمولاً به دستهبندیهای مختلفی تقسیم میشوند تا تیمها بتوانند بر اساس تخصص خود، چالشها را انتخاب کنند. این دستهبندیها میتوانند کمی متفاوت باشند، اما رایجترین آنها عبارتند از:
-
مهندسی معکوس (Reversing): بررسی و تحلیل کدهای کامپایلشده (مانند فایلهای اجرایی EXE، ELF) برای درک عملکرد آنها، کشف آسیبپذیریها یا استخراج اطلاعات پنهان (مثل رمزها یا فلگها). ابزارهای مورد استفاده: دیاسامبلرها (مثل IDA Pro، Ghidra، Radare2)، دیباگرها (مثل GDB, x64dbg).
-
رمزنگاری (Cryptography): شکستن الگوریتمهای رمزنگاری، تحلیل پروتکلهای رمزنگاری، یا پیدا کردن ضعفها در پیادهسازی آنها برای رمزگشایی دادهها و یافتن فلگ. شامل تحلیل الگوهای رمز، کلیدهای ضعیف، یا اشکالات منطقی در سیستمهای رمزنگاری.
-
وب (Web Exploitation): پیدا کردن و بهرهبرداری از آسیبپذیریهای موجود در برنامههای تحت وب (مانند SQL Injection, Cross-Site Scripting (XSS), File Inclusion, Authentication Bypass) برای دسترسی به اطلاعات یا اجرای کد.
-
فارنزیک (Forensics): تحلیل شواهد دیجیتال (مثل تصاویر دیسک، فایلهای حافظه، ترافیک شبکه، فایلهای سیستمی) برای بازسازی وقایع، شناسایی فعالیتهای مخرب یا استخراج اطلاعات پنهان.
-
باینری اکسپلویت یا PWN (Binary Exploitation / Pwning): پیدا کردن و بهرهبرداری از آسیبپذیریها در برنامههای بومی (native binaries) مانند سرریز بافر (Buffer Overflow)، فرمت استرینگ (Format String) یا use-after-free برای کنترل جریان اجرای برنامه و دستیابی به Shell یا اجرای کد دلخواه.
-
استگانوگرافی (Steganography): پیدا کردن اطلاعات پنهانشده در فایلهای رسانهای (تصاویر، صدا، ویدئو) یا سایر فرمتهای فایل، جایی که دادهها به گونهای مخفی شدهاند که حضور آنها آشکار نباشد.
-
عمومی یا متفرقه (General Skills / Misc): شامل طیف گستردهای از چالشهاست که در دستهبندیهای دیگر قرار نمیگیرند، مانند چالشهای مربوط به لینوکس، اسکریپتنویسی، اوزینت (OSINT)، یا حل معماهای منطقی.
-
شبکه (Networking): تحلیل ترافیک شبکه (بستههای کپچر شده با Wireshark)، پروتکلهای شبکه، و پیدا کردن آسیبپذیریها یا اطلاعات پنهان در ارتباطات شبکه.
-
سیستم عاملها/کانفینگ (OS/Config): چالشهای مربوط به پیکربندیهای سیستم عامل، دسترسی به فایلها، مجوزها، یا بهرهبرداری از تنظیمات نادرست در سیستمها.
توضیحات چالش: Bamboo Fox: Better Than Assembly
این چالش 500 امتیاز داشت و در دستهبندی مهندسی معکوس (Reversing) در سبک CTF از نوع Jeopardy قرار میگرفت. نکته: منظور از Jeopardy در مسابقات CTF (Capture The Flag)، یک قالب (format) خاص برای ارائه و حل چالشهاست. یا به طور خلاصه، وقتی میگوییم یک چالش در سبک CTF از نوع Jeopardy است، یعنی شما با یک سوال یا مسئله مستقل روبرو هستید که باید آن را حل کنید تا فلگ مربوطه را پیدا کرده و امتیاز کسب کنید.
مسیر حل چالش
گام اول: اسکن کردن فایل
ابزار ClamAV (Clam AntiVirus) یک موتور آنتیویروس متنباز و رایگان است که به طور گسترده برای شناسایی تروجانها، ویروسها، بدافزارها و سایر تهدیدات مخرب استفاده میشود. این نرمافزار به ویژه در سرورهای ایمیل برای اسکن فایلهای ضمیمه و جلوگیری از ورود بدافزارها از طریق ایمیل محبوبیت دارد، اما میتوان از آن برای اسکن فایلها و دایرکتوریها در سیستمهای لینوکس، یونیکس و ویندوز نیز بهره برد. ابزار خط فرمان اصلی برای اسکن فایلها با ClamAV، دستور clamscan است که به کاربران اجازه میدهد مسیرهای مشخصی را برای یافتن امضاهای بدافزار (که از پایگاه داده ویروس ClamAV بهروزرسانی میشوند) اسکن کنند. این ابزار به دلیل ماهیت متنباز بودن و قابلیت سفارشیسازی بالا، گزینهای قدرتمند و انعطافپذیر برای افزودن قابلیتهای اسکن آنتیویروس به اسکریپتها و سیستمهای خودکار است.
sudo clamscan --infected --recursive
sudo apt install clamav clamav-daemon clamav-freshclam
sudo freshclam
URL: https://docs.clamav.net/manual/Usage/Scanning.html
URL: https://x.com/pcaversaccio/status/1941114624197231092
گام دوم: شناسایی نوع و ماهیت فایلها
دستور ExifTool
دستور ExifTool یک ابزار خط فرمان رایگان و متنباز و یک کتابخانه پِرل (Perl library) قدرتمند است که برای خواندن، نوشتن و ویرایش فراداده (metadata) در طیف وسیعی از فرمتهای فایل، از جمله تصاویر (EXIF, IPTC, XMP)، ویدئوها، فایلهای صوتی و اسناد PDF، استفاده میشود. این ابزار قادر است تقریباً تمام تگهای فراداده استاندارد و سفارشی را استخراج و دستکاری کند، که آن را برای عکاسان، محققان پزشکی قانونی دیجیتال، توسعهدهندگان و هر کسی که نیاز به مدیریت دقیق اطلاعات جاسازی شده در فایلها دارد، بیاندازه ارزشمند میسازد. ExifTool به دلیل پشتیبانی گستردهاش از انواع تگها و فرمتها، قابلیتهای ویرایش دستهای، و توانایی حفظ یکپارچگی دادهها حتی پس از تغییر فراداده، به عنوان یک استاندارد صنعتی شناخته میشود. URL: https://exiftool.org
sudo apt install libimage-exiftool-perl
دستور File
دستور file یک ابزار استاندارد و قدرتمند در سیستمعاملهای شبهیونیکس (مانند لینوکس و macOS) است که برای شناسایی نوع محتوای یک فایل به کار میرود. برخلاف بسیاری از دستورات که نوع فایل را صرفاً بر اساس پسوند آن حدس میزنند، file با بررسی جادویی (magic numbers) موجود در ابتدای فایلها، ساختار داخلی و محتوای واقعی آنها را تحلیل میکند. این قابلیت به آن اجازه میدهد تا حتی فایلهایی را که پسوند اشتباه دارند یا اصلاً پسوندی ندارند، به درستی تشخیص دهد؛ مثلاً میتواند یک فایل متنی ساده، یک فایل اجرایی باینری (مانند ELF، Mach-O یا PE)، یک تصویر (JPEG، PNG)، یک آرشیو فشرده (ZIP، GZ)، یا حتی یک سند Word را شناسایی کند. این دستور برای مهندسی معکوس، بررسی امنیتی فایلها، یا صرفاً برای درک اینکه یک فایل ناشناخته واقعاً چیست، بسیار مفید است.
file task.ll
sudo apt install file
وبسایت FileInfo.com
وبسایت FileInfo.com یک دایرکتوری آنلاین بزرگ از پسوندهای فایل است که به شما امکان میدهد با وارد کردن پسوند یک فایل، اطلاعات جامعی درباره آن کسب کنید. این وبسایت توضیحات مربوط به نوع فایل، دستهبندی آن (مانند فایل ویدئویی یا سند)، برنامههای نرمافزاری مرتبط که میتوانند آن را باز کنند و اطلاعاتی درباره توسعهدهنده فرمت را ارائه میدهد، که آن را به ابزاری مفید برای شناسایی فایلهای ناشناخته تبدیل میکند.
گام سوم: کامپایل و اجرای فایل
نکته: پسوند فایل .ll عمدتاً به دو منظور استفاده میشود: رایجترین آن، فایلهای پیشنمایش تولید شده توسط نرمافزار Combit List & Label است که برای گزارشگیری کاربرد دارد. اما در حوزه کامپایلرها، بهویژه در پروژه LLVM، این پسوند به فایلهای سورس نمایش میانی (Intermediate Representation - IR) اشاره دارد که یک فرمت کد سطح پایین و قابل خواندن توسط انسان است و نقش واسطهای بین کد منبع و کد ماشین را ایفا میکند تا بهینهسازی و تولید کد برای معماریهای مختلف را تسهیل کند. بهندرت نیز ممکن است به فایلهای کد منبع Lex اشاره داشته باشد. LLVM Bitcode مانند یک زبان مشترک جهانی برای کامپایلرها است
کامپایل: (کلنگ) Clang یک کامپایلر فرانتاند (frontend) برای زبانهای برنامهنویسی C، C++، Objective-C و Objective-C++ است. این کامپایلر بخشی از پروژه بزرگتر LLVM (Low Level Virtual Machine) است و به دلیل سرعت بالا، پیامهای خطای خوانا، و پشتیبانی قوی از استانداردهای جدید زبانها، بسیار محبوب شده است.
sudo apt install clang
URL: https://github.com/llvm/llvm-project
clang task.ll -mllvm -W -g -W1,-pie -o task.out
اجرا:
$ ./task.out
Only the chosen one will know what the flag is!
Are you the chosen one?
flag:
خروجی:
flag: Hi
😠😡😠😡😠😡 You are not the chosen one! 😡😠😡😠😡😠
نکته: پسوند .out معمولاً به معنای یک فایل خروجی (output file) است و به طور خاص در سیستمهای یونیکسمانند (مانند لینوکس) برای نامگذاری فایلهای اجرایی (executables) که توسط کامپایلرها (مانند GCC) تولید میشوند، کاربرد دارد. به عنوان مثال، وقتی یک فایل سورس کد C را بدون تعیین نام خروجی کامپایل میکنید، کامپایلر به صورت پیشفرض یک فایل اجرایی با نام a.out ایجاد میکند. این پسوند همچنین میتواند برای فایلهای خروجی عمومی دیگر (مانند لاگها یا نتایج پردازش) استفاده شود، اما رایجترین کاربرد آن در CTFها و محیطهای توسعه، اشاره به یک باینری کامپایل شده است که اغلب برای تحلیل و مهندسی معکوس مورد استفاده قرار میگیرد.
گام چهارم: تحلیل باینری
ابزار Cutter
یک پلتفرم مهندسی معکوس (Reverse Engineering) رایگان و متنباز است که بر پایه موتور Rizin ساخته شده است. هدف اصلی Cutter ارائه یک محیط پیشرفته و قابل شخصیسازی برای مهندسی معکوس است، ضمن اینکه تجربه کاربری را نیز در نظر میگیرد. این ابزار توسط مهندسان معکوس برای مهندسان معکوس توسعه یافته است. Cutter ابزاری چندپلتفرمی است و برای سیستمعاملهای اصلی مانند لینوکس، macOS و ویندوز در دسترس میباشد. از ویژگیهای برجسته آن میتوان به رابط کاربری گرافیکی (GUI) مبتنی بر Qt/C++، ادغام بومی با دیکامپایلر Ghidra، نماهای مختلف (مانند Graph View و Hex Editor)، قابلیت اسکریپتنویسی با پایتون، پلاگینها و پشتیبانی از دیباگر (Debugger) اشاره کرد.
نحوه اجرا برنامه:
chmod +x Cutter*.AppImage; ./Cutter*.AppImage
URL: https://cutter.re/
URL: https://github.com/rizinorg/cutter
خروجی دیکامپایل فایل task.out
IDA Pro:
int __cdecl main(int argc, const char **argv, const char **envp)
// تعریف تابع `main` که نقطه شروع اجرای هر برنامه C/C++ است.
// `argc`: تعداد آرگومانهای خط فرمان.
// `argv`: آرایهای از رشتهها که آرگومانهای خط فرمان را نگه میدارد.
// `envp`: آرایهای از رشتهها که متغیرهای محیطی را نگه میدارد.
// `__cdecl`: یک قرارداد فراخوانی (calling convention) استاندارد برای توابع C.
{
char v4; // [rsp+14h] [rbp-94h]
// تعریف یک متغیر محلی `char` به نام `v4`.
// این متغیر برای ذخیره موقت کاراکتر در حلقه رمزگشایی مسیر "غلط" استفاده میشود.
char v5; // [rsp+34h] [rbp-74h]
// تعریف یک متغیر محلی `char` به نام `v5`.
// این متغیر برای ذخیره موقت کاراکتر در حلقه رمزگشایی مسیر "صحیح" استفاده میشود.
size_t v6; // [rsp+40h] [rbp-68h]
// تعریف یک متغیر محلی از نوع `size_t` به نام `v6`.
// این متغیر برای ذخیره طول رشته ورودی کاربر (`s`) استفاده میشود.
int j; // [rsp+58h] [rbp-50h]
// تعریف یک متغیر محلی از نوع `int` به نام `j`.
// این متغیر به عنوان شمارنده حلقه برای مسیر "غلط" استفاده میشود.
int i; // [rsp+5Ch] [rbp-4Ch]
// تعریف یک متغیر محلی از نوع `int` به نام `i`.
// این متغیر به عنوان شمارنده حلقه برای مسیر "صحیح" استفاده میشود.
char s[68]; // [rsp+60h] [rbp-48h] BYREF
// تعریف یک بافر (آرایه کاراکتری) به نام `s` با اندازه 68 بایت.
// این بافر برای ذخیره رشته ورودی کاربر که با `scanf` خوانده میشود، استفاده میشود.
// `BYREF` (اشاره شده توسط IDA/Cutter) نشان میدهد که این متغیر در پشته (stack) قرار دارد.
int v10; // [rsp+A4h] [rbp-4h]
// تعریف یک متغیر محلی `int` به نام `v10`.
// این متغیر برای نگهداری کد خروجی (بازگشتی) برنامه استفاده میشود (0 برای موفقیت، 1 برای خطا).
v10 = 0;
// مقداردهی اولیه `v10` به 0 (موفقیت).
printf("Only the chosen one will know what the flag is!\n");
// یک رشته متنی را در خروجی استاندارد (کنسول) چاپ میکند.
printf("Are you the chosen one?\n");
// یک رشته متنی دیگر را در خروجی استاندارد چاپ میکند.
printf("flag: ");
// یک پیغام "flag: " را برای درخواست ورودی از کاربر چاپ میکند.
__isoc99_scanf("%64s", s);
// ورودی کاربر را از ورودی استاندارد میخواند و آن را در بافر `s` ذخیره میکند.
// `"%64s"` به `scanf` میگوید که حداکثر 64 کاراکتر (به همراه یک کاراکتر null terminator) را بخواند تا از سرریز بافر (buffer overflow) جلوگیری کند.
// `__isoc99_scanf` نسخه استاندارد C99 از تابع `scanf` است.
v6 = strlen(s);
// طول رشته ورودی کاربر (`s`) را محاسبه کرده و در `v6` ذخیره میکند.
if ( v6 == strlen(&what) )
// شرط اول: بررسی طول ورودی.
// `strlen(&what)` طول رشته ثابت `what` (که از باینری استخراج شده) را برمیگرداند.
// اگر طول ورودی کاربر (`s`) **برابر** با طول رشته `what` باشد، برنامه وارد بلوک `if` (مسیر اصلی اعتبارسنجی) میشود.
// در غیر این صورت، به بلوک `else` میرود.
{
if ( (unsigned int)check(s) )
// شرط دوم: فراخوانی تابع `check()`.
// `check(s)` تابع `check` را با رشته ورودی کاربر `s` فراخوانی میکند.
// اگر `check(s)` مقدار `1` (True) برگرداند (یعنی ورودی صحیح باشد)، برنامه وارد این بلوک `if` (مسیر "صحیح") میشود.
{
for ( i = 0; i < strlen(s); ++i )
// حلقه رمزگشایی برای مسیر "صحیح":
// اگر `check(s)` موفق باشد، این حلقه اجرا میشود.
{
v5 = s[i];
// بایت فعلی از رشته ورودی `s` را در `v5` ذخیره میکند.
s[i] = secret[i % strlen(secret)] ^ v5;
// این خط عملیات **رمزگشایی فلگ واقعی** را انجام میدهد.
// بایت `i`ام از رشته `secret` (با استفاده از `%` برای تکرار کلید) با بایت `v5` (همان `s[i]`) XOR میشود.
// نتیجه دوباره در `s[i]` ذخیره میشود.
// این یعنی رشته `s` پس از این حلقه، تبدیل به فلگ واقعی رمزگشایی شده میشود.
}
}
else
// بلوک `else` برای `if (check(s))`:
// اگر `check(s)` مقدار `0` (False) برگرداند (یعنی ورودی غلط باشد)، برنامه وارد این بلوک `else` (مسیر "غلط") میشود.
{
for ( j = 0; j < strlen(s); ++j )
// حلقه رمزگشایی برای مسیر "غلط":
// اگر `check(s)` ناموفق باشد، این حلقه اجرا میشود.
{
v4 = flag[j];
// بایت `j`ام از رشته ثابت `flag` (که در واقع حاوی فلگ جعلی رمزنگاری شده است) را در `v4` ذخیره میکند.
s[j] = secret[j % strlen(secret)] ^ v4;
// این خط عملیات **رمزگشایی فلگ جعلی** را انجام میدهد.
// بایت `j`ام از رشته `secret` (با تکرار کلید) با بایت `v4` (همان `flag[j]`) XOR میشود.
// نتیجه در `s[j]` ذخیره میشود.
// این یعنی رشته `s` پس از این حلقه، تبدیل به فلگ جعلی رمزگشایی شده میشود.
}
}
printf(format, s);
// این خط، محتوای فعلی بافر `s` را چاپ میکند.
// بسته به اینکه کدام مسیر (صحیح یا غلط) فعال شده باشد، `s` یا حاوی فلگ واقعی رمزگشایی شده است یا فلگ جعلی رمزگشایی شده.
// `format` احتمالاً یک رشته فرمت (مانند `"%s"`) یا حتی خود فلگ جعلی (برای چاپ یک پیام خاص) است که در حافظه باینری قرار دارد.
v10 = 0;
// `v10` دوباره به 0 تنظیم میشود (نشانگر خروجی موفق).
}
else
// بلوک `else` برای `if (v6 == strlen(&what))`:
// اگر طول ورودی کاربر با طول `what` برابر نباشد، این بلوک اجرا میشود.
{
printf(asc_40205A);
// یک پیام خطای عمومی (مثلاً "You are not the chosen one!" که قبلاً دیدیم) را چاپ میکند.
// `asc_40205A` اشاره به آدرس حافظهای است که این رشته خطا در آن ذخیره شده.
v10 = 1;
// `v10` به 1 تنظیم میشود (نشانگر خروجی خطا).
}
return v10;
// مقدار نهایی `v10` (0 برای موفقیت یا 1 برای خطا) را به عنوان کد خروجی برنامه برمیگرداند.
}
Cutter:
/* jsdec pseudo code output */
/* /home/task.out @ 0x401230 */
// اینها کامنتهایی هستند که توسط ابزار دیکامپایلر (jsdec) اضافه شدهاند.
// نشان میدهند که این کد از فایل `task.out` و از آدرس `0x401230` شروع شده است.
#include <stdint.h>
// یک هدر استاندارد C را شامل میکند که تعاریف انواع دادهای با اندازه مشخص (مانند `int32_t`, `int64_t`) را فراهم میکند.
int32_t main (int64_t arg_60h) {
// تعریف تابع `main`، نقطه شروع اجرای برنامه.
// `int32_t` نوع بازگشتی تابع است.
// `arg_60h` احتمالاً نمایشی از آرگومانهای خط فرمان است که به تابع `main` پاس داده میشوند.
// متغیرهای محلی
// jsdec (دیکامپایلر Cutter) نامهای متغیرها را بر اساس آفست آنها از RSP (Stack Pointer) تعیین میکند.
// این نامها داخلی دیکامپایلر هستند و برای فهمیدن بهتر باید آنها را با نامهای معادل در IDA (مثل v4, v5, s و ...) مقایسه کنیم.
int64_t var_a8h;
size_t * var_a0h;
int64_t var_94h;
int64_t var_90h;
size_t var_88h;
size_t * var_80h;
int64_t var_74h;
int64_t var_70h;
size_t var_68h; // معادل v6 در IDA: این متغیر برای ذخیره طول رشته ورودی کاربر استفاده میشود.
int32_t var_60h; // معادل متغیرهای موقت برای ذخیره خروجی توابع (مثل scanf, printf)
int32_t var_5ch;
int32_t var_58h;
int32_t var_54h;
int64_t var_50h; // معادل j در IDA: این متغیر به عنوان شمارنده حلقه برای مسیر "غلط" (else branch) استفاده میشود.
int64_t var_4ch; // معادل i در IDA: این متغیر به عنوان شمارنده حلقه برای مسیر "صحیح" (if branch) استفاده میشود.
const char * s; // معادل char s[68] در IDA: این متغیر بافر (آرایه کاراکتری) است که ورودی کاربر در آن ذخیره میشود. jsdec آن را به صورت اشارهگر به char نشان میدهد.
int64_t var_4h; // معادل v10 در IDA: این متغیر برای ذخیره مقدار بازگشتی نهایی تابع `main` (کد خروج برنامه) استفاده میشود.
var_4h = 0;
// مقدار اولیه متغیر `var_4h` (کد خروج برنامه) را به 0 (نشاندهنده موفقیت) تنظیم میکند.
// چاپ پیامها با استفاده از رجیستر RDI برای نگهداری آدرس رشته
rdi = "Only the chosen one will know what the flag is!\n";
// آدرس رشته "Only the chosen one will know what the flag is!\n" را در رجیستر `RDI` قرار میدهد.
// در معماری x64، `RDI` رجیستر استاندارد برای اولین آرگومان توابع است.
al = 0;
// رجیستر `AL` (بخشی از `RAX`) را به 0 تنظیم میکند.
// این معمولاً نشاندهنده این است که هیچ آرگومان floating-point به تابع پاس داده نمیشود.
eax = printf (rdi);
// تابع `printf` را با آدرسی که در `RDI` قرار دارد فراخوانی میکند.
// مقدار بازگشتی `printf` (تعداد کاراکترهای چاپ شده) در `EAX` (بخشی از `RAX`) ذخیره میشود.
rdi = "Are you the chosen one?\n";
// آدرس رشته "Are you the chosen one?\n" را در `RDI` قرار میدهد.
var_54h = eax;
// مقدار فعلی `EAX` (نتیجه `printf` قبلی) را به صورت موقت در `var_54h` ذخیره میکند.
al = 0;
// `AL` را دوباره به 0 تنظیم میکند.
eax = printf (rdi);
// تابع `printf` را برای چاپ رشته دوم فراخوانی میکند.
rdi = "flag: ";
// آدرس رشته "flag: " را در `RDI` قرار میدهد.
var_58h = eax;
// مقدار `EAX` (نتیجه `printf` قبلی) را در `var_58h` ذخیره میکند.
al = 0;
// `AL` را به 0 تنظیم میکند.
eax = printf (rdi);
// تابع `printf` را برای چاپ "flag: " فراخوانی میکند.
// دریافت ورودی کاربر
// RSI برای آرگومان دوم (%64s) و RDI برای آرگومان اول (s) در scanf
rsi = &s;
// آدرس بافر `s` (جایی که ورودی کاربر باید ذخیره شود) را در رجیستر `RSI` قرار میدهد.
// `RSI` رجیستر استاندارد برای آرگومان دوم توابع است.
rdi = "%64s";
// آدرس رشته فرمت `"%64s"` را در `RDI` قرار میدهد.
var_5ch = eax;
// مقدار `EAX` (نتیجه `printf` قبلی) را در `var_5ch` ذخیره میکند.
al = 0;
// `AL` را به 0 تنظیم میکند.
eax = isoc99_scanf ();
// تابع `isoc99_scanf` (نسخه استاندارد C99 از `scanf`) را فراخوانی میکند تا ورودی کاربر را بخواند.
// ورودی بر اساس فرمت `"%64s"` (حداکثر 64 کاراکتر رشتهای) خوانده شده و در `s` ذخیره میشود.
rdi = &s;
// آدرس `s` را دوباره در `RDI` قرار میدهد (آمادهسازی برای فراخوانی `strlen` بعدی).
var_60h = eax;
// مقدار بازگشتی `scanf` (تعداد آیتمهای خوانده شده) را در `var_60h` ذخیره میکند.
// محاسبه طول رشته ورودی کاربر
rax = strlen ();
// تابع `strlen` را روی رشتهای که آدرس آن در `RDI` (یعنی `s`) قرار دارد، فراخوانی میکند.
// طول رشته `s` در `RAX` ذخیره میشود.
edi = what;
// آدرس رشته ثابت `what` را در `EDI` (که بخشی از `RDI` است) قرار میدهد.
// آمادهسازی برای فراخوانی `strlen` بعدی روی `what`.
var_68h = rax;
// طول رشته `s` (که در `RAX` بود) را در `var_68h` (معادل `v6` در IDA) ذخیره میکند.
rax = strlen ();
// تابع `strlen` را روی رشتهای که آدرس آن در `EDI` (یعنی `what`) قرار دارد، فراخوانی میکند.
// طول رشته `what` در `RAX` ذخیره میشود.
rcx = var_68h;
// مقدار `var_68h` (طول رشته `s`) را در رجیستر `RCX` قرار میدهد.
if (rcx != rax) {
// شرط: اگر طول `s` (`RCX`) با طول `what` (`RAX`) برابر نباشد (یعنی `!=` به جای `==` در سورس اصلی).
// این همان شرط `if (v6 == strlen(&what))` است، اما با منطق معکوس برای `goto`.
rdi = data.0040205a;
// آدرس رشته خطای "You are not the chosen one!" (که در حافظه `0x40205a` قرار دارد) را در `RDI` قرار میدهد.
al = 0;
// `AL` را به 0 تنظیم میکند.
printf (rdi);
// پیام خطا را چاپ میکند.
var_4h = 1;
// مقدار بازگشتی نهایی برنامه (`var_4h`) را به 1 (نشانگر خطا) تنظیم میکند.
goto label_0;
// برنامه به `label_0` پرش میکند که انتهای تابع `main` است.
}
// اگر طولها برابر باشند، تابع check(s) فراخوانی میشود
rdi = &s;
// آدرس بافر `s` را در `RDI` قرار میدهد (به عنوان آرگومان برای تابع `check`).
eax = check ();
// تابع `check()` را فراخوانی میکند و مقدار بازگشتی آن را در `EAX` ذخیره میکند.
if (eax == 0) {
// شرط: اگر `check()` مقدار 0 (false) برگرداند.
// این همان `else` در کدهای دیکامپایل شده قبلی است.
goto label_1;
// برنامه به `label_1` پرش میکند که بلوک کد مربوط به حالت `check` ناموفق است.
}
// بلوک کد در صورت موفقیت check() (معادل if در IDA)
var_4ch = 0;
// شمارنده حلقه `var_4ch` (معادل `i` در IDA) را به 0 تنظیم میکند.
do {
// شروع یک حلقه `do-while` (معادل حلقه `for` در سورس اصلی).
rdi = &s;
// آدرس `s` را در `RDI` قرار میدهد.
rax = (int64_t) var_4ch;
// مقدار شمارنده `var_4ch` را به `RAX` منتقل میکند.
var_70h = rax;
// مقدار `RAX` را در `var_70h` ذخیره میکند.
rax = strlen ();
// `strlen` را روی `s` فراخوانی میکند (این `strlen` هر بار در حلقه تکرار میشود که در C کامپایل شده رایج است).
rcx = var_70h;
// مقدار `var_70h` را در `RCX` قرار میدهد.
if (rcx >= rax) {
// شرط خروج از حلقه: اگر `var_4ch` (شمارنده) بزرگتر یا مساوی طول `s` شود.
// این معادل `i < strlen(s)` در `for` لوپ است، اما برای پرش.
goto label_2;
// پرش به `label_2` که بعد از حلقه رمزگشایی موفقیتآمیز است.
}
rax = (int64_t) var_4ch;
// مقدار شمارنده `var_4ch` را به `RAX` منتقل میکند.
ecx = *((rsp + rax + 0x60));
// به بایت `var_4ch`ام از بافر `s` دسترسی پیدا میکند (که در `rsp + 0x60` شروع میشود).
// این همان `s[i]` است.
var_74h = ecx;
// مقدار `s[i]` را در `var_74h` ذخیره میکند (معادل `v5` در IDA).
rax = (int64_t) var_4ch;
// مقدار شمارنده `var_4ch` را به `RAX` منتقل میکند.
edi = "B\n|_";
// **نکته:** این خط `edi = "B\n|_"` یک artifact (محتوای باقیمانده یا تحلیل نادرست) از دیکامپایلر است.
// `secret` واقعی از آدرس `secret` در بخش `data` باینری خوانده میشود، نه از این رشته ثابت.
// این بخش نباید در ترجمه منطق واقعی برنامه در نظر گرفته شود.
var_80h = rax;
// مقدار `RAX` را در `var_80h` ذخیره میکند.
rax = strlen ();
// `strlen` را روی `secret` فراخوانی میکند (که در `EDI` بود، اما اینجا اشاره به `secret` واقعی در حافظه دارد).
rdx = var_80h;
// مقدار `var_80h` را در `RDX` قرار میدهد.
var_88h = rax;
// طول `secret` را در `var_88h` ذخیره میکند.
rax = rdx;
// `RDX` (که حاوی `var_4ch` بود) را به `RAX` منتقل میکند.
ecx = 0;
// `ECX` را به 0 تنظیم میکند.
edx = ecx;
// `EDX` را به `ECX` (0) تنظیم میکند.
rsi = var_88h;
// طول `secret` (`var_88h`) را به `RSI` منتقل میکند.
rax = rdx:rax / rsi;
// این بخش محاسبات مربوط به `i / strlen(secret)` (تقسیم) را انجام میدهد.
rdx = rdx:rax % rsi;
// این بخش محاسبات مربوط به `i % strlen(secret)` (باقیمانده) را انجام میدهد.
// `RDX` در نهایت حاوی نتیجه `i % strlen(secret)` است.
ecx = *((rdx + secret));
// به بایت مورد نظر از رشته `secret` (یعنی `secret[i % strlen(secret)]`) دسترسی پیدا میکند.
r8d = var_74h;
// مقدار `var_74h` (که `s[i]` بود) را به `R8D` منتقل میکند.
r8d ^= ecx;
// عملیات XOR را انجام میدهد: `s[i] ^ secret[i % strlen(secret)]`.
// نتیجه در `R8D` ذخیره میشود.
rdx = (int64_t) var_4ch;
// مقدار شمارنده `var_4ch` را به `RDX` منتقل میکند.
*((rsp + rdx + 0x60)) = r8b;
// نتیجه XOR را (که در `R8D` و به صورت `r8b` - بایت پایینتر از `R8D` - است) در `s[i]` ذخیره میکند.
// این `s[i]` اکنون حاوی یک کاراکتر از فلگ واقعی رمزگشایی شده است.
eax++;
// شمارنده `eax` را یک واحد افزایش میدهد (معادل `i++`).
var_4ch = eax;
// مقدار افزایش یافته را در `var_4ch` ذخیره میکند.
} while (1);
// حلقه بیپایان `do-while` که با دستورات `goto` کنترل میشود.
label_2:
rsi = &s;
// آدرس بافر `s` (که اکنون حاوی فلگ رمزگشایی شده است) را در `RSI` قرار میدهد.
rdi = format;
// آدرس رشته فرمت برای `printf` (احتمالاً `"%s"` یا مشابه آن) را در `RDI` قرار میدهد.
al = 0;
// `AL` را به 0 تنظیم میکند.
printf (rdi);
// محتوای `s` (که فلگ رمزگشایی شده است) را چاپ میکند.
goto label_3;
// پرش به `label_3` که بلوک مربوط به خروج موفقیتآمیز است.
label_1:
// بلوک کد در صورت عدم موفقیت check() (معادل else در IDA)
var_50h = 0;
// شمارنده حلقه `var_50h` (معادل `j` در IDA) را به 0 تنظیم میکند.
do {
// شروع یک حلقه `do-while` (معادل حلقه `for` در سورس اصلی).
rdi = &s;
// آدرس `s` را در `RDI` قرار میدهد.
rax = (int64_t) var_50h;
// مقدار شمارنده `var_50h` را به `RAX` منتقل میکند.
var_90h = rax;
// مقدار `RAX` را در `var_90h` ذخیره میکند.
rax = strlen ();
// `strlen` را روی `s` فراخوانی میکند.
rcx = var_90h;
// مقدار `var_90h` را در `RCX` قرار میدهد.
if (rcx >= rax) {
// شرط خروج از حلقه: اگر `var_50h` (شمارنده) بزرگتر یا مساوی طول `s` شود.
goto label_4;
// پرش به `label_4` که بعد از حلقه رمزگشایی فلگ جعلی است.
}
rax = (int64_t) arg_60h;
// **نکته:** `arg_60h` در اینجا به اشتباه استفاده شده است.
// این خط باید به جای آن به آدرس رشته ثابت `flag` (که حاوی فلگ جعلی رمزنگاری شده است) دسترسی پیدا کند.
// دیکامپایلر احتمالاً نتوانسته آدرس دقیق `flag` را ردیابی کند و به یک آرگومان اولیه اشاره کرده است.
ecx = *((rax + flag));
// به بایت `var_50h`ام از رشته `flag` (جعلی و رمزنگاری شده) دسترسی پیدا میکند.
// این همان `flag[j]` است.
rax = (int64_t) var_50h;
// مقدار شمارنده `var_50h` را به `RAX` منتقل میکند.
edi = "B\n|_";
// **نکته:** این هم یک artifact دیکامپایلر است. مربوط به `secret` واقعی نیست.
var_94h = ecx;
// مقدار `flag[j]` (جعلی و رمزنگاری شده) را در `var_94h` ذخیره میکند (معادل `v4` در IDA).
var_a0h = rax;
// مقدار `RAX` را در `var_a0h` ذخیره میکند.
rax = strlen ();
// `strlen` را روی `secret` فراخوانی میکند.
rdx = var_a0h;
// مقدار `var_a0h` را در `RDX` قرار میدهد.
*(rsp) = rax;
// طول `secret` را به صورت موقت در پشته ذخیره میکند.
rax = rdx;
// `RDX` (که حاوی `var_50h` بود) را به `RAX` منتقل میکند.
ecx = 0;
// `ECX` را به 0 تنظیم میکند.
edx = ecx;
// `EDX` را به `ECX` (0) تنظیم میکند.
rsi = *(rsp);
// طول `secret` را از پشته بازیابی کرده و در `RSI` قرار میدهد.
rax = rdx:rax / rsi;
// محاسبه `j / strlen(secret)`.
rdx = rdx:rax % rsi;
// محاسبه `j % strlen(secret)`. `RDX` حاوی نتیجه نهایی است.
ecx = *((rdx + secret));
// به بایت مورد نظر از رشته `secret` (یعنی `secret[j % strlen(secret)]`) دسترسی پیدا میکند.
r8d = var_94h;
// مقدار `var_94h` (که `flag[j]` بود) را به `R8D` منتقل میکند.
r8d ^= ecx;
// عملیات XOR را انجام میدهد: `flag[j] ^ secret[j % strlen(secret)]`.
// نتیجه در `R8D` ذخیره میشود.
rdx = (int64_t) var_50h;
// مقدار شمارنده `var_50h` را به `RDX` منتقل میکند.
*((rsp + rdx + 0x60)) = r8b;
// نتیجه XOR را در `s[j]` ذخیره میکند.
// این `s[j]` اکنون حاوی یک کاراکتر از فلگ جعلی رمزگشایی شده است.
eax++;
// شمارنده `eax` را یک واحد افزایش میدهد (معادل `j++`).
var_50h = eax;
// مقدار افزایش یافته را در `var_50h` ذخیره میکند.
} while (1);
// حلقه بیپایان `do-while`.
label_4:
rsi = &s;
// آدرس بافر `s` (که اکنون حاوی فلگ جعلی رمزگشایی شده است) را در `RSI` قرار میدهد.
rdi = format;
// آدرس رشته فرمت برای `printf` (احتمالاً `"%s"` برای چاپ فلگ) را در `RDI` قرار میدهد.
al = 0;
// `AL` را به 0 تنظیم میکند.
printf (rdi);
// محتوای `s` (فلگ جعلی) را چاپ میکند.
label_3:
var_4h = 0;
// `var_4h` (کد خروج) را به 0 تنظیم میکند.
label_0:
eax = 0;
// `EAX` را به 0 تنظیم میکند. این خط نهایی است که مقدار بازگشتی تابع `main` را تعیین میکند.
return rax;
// مقدار نهایی `RAX` (0) را به عنوان کد خروج برنامه بازمیگرداند.
}
گام پنجم: منطق برنامه
منطق کلی برنامه (از هر دو کد دیکامپایل شده):
- برنامه سه پیام خوشآمدگویی و سپس flag: را چاپ میکند.
- از کاربر یک رشته (احتمالاً فلگ) را با scanf میخواند و در بافر s ذخیره میکند (حداکثر ۶۴ کاراکتر).
شرط اول: طول رشته ورودی کاربر (s) را با طول یک رشته دیگر به نام what مقایسه میکند. اگر طولها برابر نباشند، پیام خطا (asc_40205A یا data.0040205a) را چاپ کرده و برنامه با کد خطا 1 خارج میشود.
شرط دوم (در صورت برابری طولها): تابعی به نام check(s) را با ورودی کاربر (s) فراخوانی میکند. حالت A (اگر check(s) موفق باشد): یک حلقه for اجرا میشود. در این حلقه، هر کاراکتر از ورودی کاربر (s[i]) با کاراکتر متناظر از رشته secret (به صورت دورهای i % strlen(secret)) عملگر XOR میشود. نتیجه در همان بافر s ذخیره میشود. حالت B (اگر check(s) ناموفق باشد): یک حلقه for دیگر اجرا میشود. در این حلقه، هر کاراکتر از رشته flag (که احتمالاً فلگ صحیح است) با کاراکتر متناظر از رشته secret (به صورت دورهای j % strlen(secret)) عملگر XOR میشود. نتیجه نیز در بافر s ذخیره میشود. در نهایت، برنامه محتوای تغییر یافته s را با استفاده از یک رشته فرمت (format) چاپ میکند.
هدف این چالش (CTF): پیدا کردن ورودی صحیح (s) است که باعث شود تابع check(s) موفق عمل کند (حالت A). اگر بتوانیم این ورودی را پیدا کنیم، برنامه فلگ XOR شده را چاپ میکند که سپس باید آن را با همان secret دوباره XOR کنیم تا فلگ اصلی را به دست آوریم.اگر نتوانیم check(s) را دور بزنیم، میتوانیم ورودی غلطی بدهیم تا برنامه به حالت B برود و فلگ واقعی (XOR شده) را برایمان چاپ کند که سپس باید آن را دیکد کنیم.
خلاصه این بخش: (برنامه یک رشته ورودی (حداکثر 64 کاراکتر) دریافت میکنه. اگر طول و محتوای رشته درست باشه، اون رو با یک secret (مقدار سری) XOR میکنه. در غیر این صورت، یک رشته دیگه (که حاوی فلگ هست) رو با همون secret XOR میکنه و خروجی میده.)
منطق XOR و درباره رمز نگاری XOR
همانطور که میبینید، این (کد) با خروجی برنامه هنگام اجرا مطابقت دارد. برنامه یک رشته (حداکثر 64 کاراکتر) را میخواند؛ اگر طول آن با طول رشتهای که سپس برنامه بررسی میکند یکسان باشد و رشته مورد نظر از بررسی عبور کند، آن را با secret (مقدار سری) XOR میکند. در غیر این صورت، یک رشته دیگر (فلگ) را با secret XOR میکند. سپس من به سراغ پیدا کردن مقادیر برای متغیرهای مختلف رفتم:
what = b"\x17/'\x17\x1DJy\x03,\x11\x1E&\x0AexjONacA-&\x01LANH'.&\x12>#'Z\x0FO\x0B%:(&HI\x0CJylL'\x1EmtdC\x00\x00\x00\x00\x00\x00\x00\x00"
secret = b'B\x0A|\x22\x06\x1Bg7#\x5CF\x0A)\x090Q8{Y\x13\x18\x0DP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
flag = b'\x1DU#hJ7.8\x06\x16\x03rUO=[bg9JmtGt`7U\x0BnNjD\x01\x03\x120\x19;OVIaM\x00\x08,qu<g\x1D;K\x00}Y\x00\x00\x00\x00\x00\x00\x00\x00'
format = b'\x0A\xF0\x9F\x98\x82\xF0\x9F\x91\x8C\xF0\x9F\x98\x82\xF0\x9F\x91\x8C\xF0\x9F\x98\x82\xF0\x9F\x91\x8C flag{%s} \xF0\x9F\x91\x8C\xF0\x9F\x98\x82\xF0\x9F\x91\x8C\xF0\x9F\x98\x82\xF0\x9F\x91\x8C\xF0\x9F\x98\x82\x0A\x0A\x00\x00\x00'
asc_40205A = b'\x0A\xF0\x9F\x98\xA0\xF0\x9F\x98\xA1\xF0\x9F\x98\xA0\xF0\x9F\x98\xA1\xF0\x9F\x98\xA0\xF0\x9F\x98\xA1 You are not the chosen one! \xF0\x9F\x98\xA1\xF0\x9F\x98\xA0\xF0\x9F\x98\xA1\xF0\x9F\x98\xA0\xF0\x9F\x98\xA1\xF0\x9F\x98\xA0\x0A\x0A\x00'
درباره رمز نگاری XOR: مخفف "Exclusive OR" (یا انحصاری) است و یک عملگر منطقی و بیتی مهم در علوم کامپیوتر و رمزنگاری است.
عملکرد پایه XOR عملگر XOR دو ورودی (بیت) را دریافت کرده و اگر این دو بیت متفاوت باشند، خروجی 1 (True) و اگر یکسان باشند، خروجی 0 (False) میدهد. ورودی A ورودی B خروجی A XOR B 0 0 0 0 1 1 1 0 1 1 1 0
کاربرد XOR در رمزنگاری 🔒 به دلیل ویژگیهای خاصش، به طور گستردهای در رمزنگاری و امنیت اطلاعات استفاده میشود: خاصیت برگشتپذیری (Invertibility): این مهمترین ویژگی برای رمزنگاری است. اگر A XOR B = C باشد، آنگاه C XOR B = A نیز خواهد بود. این یعنی اگر پیامی (A) را با یک کلید (B) رمزنگاری (XOR) کنید و به متن رمز شده (C) برسید، میتوانید با دوباره XOR کردن متن رمز شده (C) با همان کلید (B)، به پیام اصلی (A) بازگردید. این خاصیت برای رمزگشایی ضروری است. در مثالی که دیدیم (چالش CTF)، رشته flag با secret XOR میشد. برای پیدا کردن flag اصلی، باید (flag XOR secret) را دوباره با secret XOR کنیم. سادگی و سرعت: عملیات XOR در سطح بیت بسیار ساده و سریع است، به همین دلیل در سیستمهایی که نیاز به پردازش سریع دادهها دارند، کارآمد است. تغییر هر بیت به صورت مستقل: هر بیت از دادهها به صورت مستقل با بیت متناظر از کلید XOR میشود. این بدان معناست که تغییر در یک بیت از کلید یا متن اصلی، فقط بر بیت متناظر در خروجی تأثیر میگذارد و نه بر کل خروجی. پوشاندن الگوها: XOR میتواند الگوهای قابل تشخیص در دادههای اصلی را پنهان کند و آنها را تصادفیتر به نظر برساند، که برای پنهان کردن اطلاعات مفید است.
مثال ساده با کاراکترها: فرض کنید میخواهیم کاراکتر 'A' را با کاراکتر 'K' (به عنوان کلید) رمزنگاری کنیم. مقدار ASCII کاراکتر 'A' برابر 65 است (باینری: 01000001). مقدار ASCII کاراکتر 'K' برابر 75 است (باینری: 01001011).
01000001 (A) XOR 01001011 (K)
00001010 (نتیجه XOR) نتیجه 00001010 (باینری) برابر 10 در مبنای 10 است که معادل کاراکتر LF (Line Feed) یا یک کاراکتر غیرقابل چاپ دیگر است.
حالا برای رمزگشایی: 00001010 (نتیجه XOR) XOR 01001011 (K)
01000001 (برمیگردیم به A)
به همین دلیل، XOR یک عملگر اساسی در رمزنگاریهای ساده (مانند رمزنگاری جریانی یا One-Time Pad) و همچنین بخشی از الگوریتمهای رمزنگاری پیچیدهتر است. در چالشهای CTF، XOR بسیار رایج است زیرا به تحلیلگر اجازه میدهد با معکوس کردن عملیات، به دادههای پنهان (مثل فلگ) دست پیدا کند.
گام ششم: اسکریپت نویسی برای تحلیل
اسکریپت اول:
secret = b'B\x0A|_\x22\x06\x1Bg7#\x5CF\x0A)\x090Q8_{Y\x13\x18\x0DP\x00...'
# متغیر `secret` را تعریف میکند که حاوی بایتهای کلید رمزنگاری است.
# این مقدار از تحلیل فایل باینری اصلی (مثلاً با Cutter) به دست آمده و برای عملیات XOR استفاده میشود.
flag = b'\x1DU#hJ7.8\x06\x16\x03rUO=[bg9JmtGt`7U\x0BnNjD\x01\x03\x120...'
# متغیر `flag` را تعریف میکند که حاوی بایتهای پیام رمزنگاری شده است (این فلگ، فلگ جعلی است).
# این مقدار نیز از تحلیل فایل باینری به دست آمده است.
newStr = ""
# یک رشته خالی به نام `newStr` ایجاد میکند.
# این رشته برای ذخیره کاراکترهای رمزگشایی شده (خروجی نهایی) به کار میرود.
for i in range(0, 56):
# یک حلقه `for` را آغاز میکند که از عدد 0 شروع شده و تا 55 ادامه مییابد (در مجموع 56 تکرار).
# عدد 56 احتمالاً طول رشته رمزگشایی شده مورد انتظار است که از تحلیل برنامه (مثلاً از `strlen()`) به دست آمده است.
newStr += chr(secret[i % len(secret)] ^ flag[i])
# این خط هسته عملیات رمزگشایی با XOR است:
# `flag[i]`: بایت `i`ام از رشته رمزنگاری شده `flag` را انتخاب میکند.
# `len(secret)`: طول رشته `secret` را برمیگرداند.
# `i % len(secret)`: این عملگر باقیمانده تقسیم `i` بر طول `secret` را محاسبه میکند. این کار تضمین میکند که اگر طول `secret` از 56 کمتر باشد، کلید به صورت چرخشی (دورهای) تکرار شود و هر بایت از `flag` با یک بایت از `secret` جفت شود.
# `secret[...]`: بایت متناظر از کلید `secret` را انتخاب میکند.
# `^`: این عملگر `XOR` (یا انحصاری) است که عملیات بیتی را روی دو بایت انجام میدهد.
# `chr()`: نتیجه عددی عملیات XOR را به کاراکتر ASCII مربوطه تبدیل میکند.
# `newStr += ...`: کاراکتر حاصل را به انتهای رشته `newStr` اضافه میکند.
print(newStr)
# پس از اتمام حلقه، رشته `newStr` که حاوی پیام رمزگشایی شده است، در خروجی چاپ میشود.
خروجی: 7h15_15_4_f4k3_f14g_y0u_w1ll_f41l_1f_y0u_subm17_17
نتیجه: چه اتفاقی افتاده است: این کد بایتهای رمزنگاری شده flag را میگیرد و هر بایت آن را با بایت متناظر از secret (که به صورت دورهای تکرار میشود) دوباره XOR میکند. نتیجه این عملیات، کاراکترهای اصلی پیام است که به تدریج در newStr جمعآوری و در نهایت چاپ میشوند. خروجی 7h15_15_4_f4k3_f14g_y0u_w1ll_f41l_1f_y0u_subm17_17 نشان میدهد که این عملیات رمزگشایی با موفقیت انجام شده است. با این حال، همانطور که از متن خروجی پیداست، این یک فلگ "جعلی" (fake flag) است که برای گمراه کردن شرکتکنندگان چالش طراحی شده است. این بدان معنی است که شاید راه حل واقعی چالش، یافتن ورودی صحیح برای تابع check() باشد که منجر به شاخه "True" در کد اصلی میشود و یک فلگ متفاوت را تولید میکند.
نکته: اگر A ^ B = C باشد، آنگاه A ^ C = B و B ^ C = A. این یک خاصیت بنیادی است که برای معکوس کردن عملیات XOR در هر دو مسیر (فلگ واقعی و جعلی) استفاده میشود.
اسکریپت دوم:
__int64 __fastcall check(__int64 a1)
// تعریف تابع `check`:
// این تابع یک آرگومان از نوع `__int64` به نام `a1` میگیرد. `a1` اشارهگری به رشته ورودی کاربر (مانند "s" در تابع main) است.
// این تابع یک مقدار از نوع `__int64` برمیگرداند که در اینجا عملاً یک مقدار بولین (0 یا 1 برای False/True) است.
// `__fastcall` یک قرارداد فراخوانی (calling convention) است که به نحوه پاس دادن آرگومانها به تابع اشاره دارد.
{
int v2; // [rsp+1Ch] [rbp-1Ch]
// تعریف یک متغیر محلی از نوع `int` به نام `v2`.
// این متغیر برای ذخیره موقت بایت فعلی از ورودی کاربر در هر تکرار حلقه استفاده میشود.
int i; // [rsp+28h] [rbp-10h]
// تعریف یک متغیر محلی از نوع `int` به نام `i`.
// این متغیر به عنوان شمارنده اصلی حلقه `for` استفاده میشود.
unsigned int v4; // [rsp+2Ch] [rbp-Ch]
// تعریف یک متغیر محلی از نوع `unsigned int` به نام `v4`.
// این متغیر برای نگهداری نتیجه نهایی اعتبارسنجی استفاده میشود. مقدار 1 به معنای "ورودی صحیح" و 0 به معنای "ورودی غلط" است.
v4 = 1;
// مقداردهی اولیه `v4` به 1.
// این خط فرض میکند که ورودی در ابتدا صحیح است و اگر هر یک از مقایسهها در حلقه غلط باشد، این مقدار به 0 تغییر خواهد کرد.
for ( i = 0; i < strlen(what); ++i )
// شروع حلقه `for`:
// حلقه از `i = 0` (اولین بایت) شروع میشود.
// تا زمانی که `i` کمتر از طول رشته ثابت `what` باشد، ادامه مییابد. `strlen(what)` طول رشته `what` را برمیگرداند.
// در هر تکرار، `i` یک واحد افزایش مییابد (`++i`).
{
v2 = *(char *)(a1 + i);
// دسترسی به بایت فعلی از ورودی کاربر:
// `a1 + i`: به آدرس بایت `i`ام از رشته ورودی کاربر اشاره میکند.
// `*(char *)`: محتوای آدرس را به عنوان یک کاراکتر (بایت) میخواند.
// مقدار این بایت خوانده شده در `v2` ذخیره میشود.
v4 = (unsigned __int8)v4 & ((*(char *)(a1 + (i + 1) % strlen(what)) ^ v2) == what[i]);
// این خط هسته الگوریتم اعتبارسنجی است و `v4` را در هر تکرار بروزرسانی میکند:
// 1. `(i + 1) % strlen(what)`: شاخص بایت **بعدی** از ورودی کاربر را محاسبه میکند. عملگر `%` (باقیمانده) تضمین میکند که اگر به انتهای رشته `what` رسید، به ابتدای آن برگردد (عملیات چرخشی).
// 2. `*(char *)(a1 + ...)`: بایت **بعدی** از رشته ورودی کاربر (`a1`) را میخواند.
// 3. `^ v2`: بایت **بعدی** ورودی کاربر را با بایت **فعلی** ورودی کاربر (`v2`) به صورت **XOR** میکند.
// 4. `(...) == what[i]`: نتیجه عملیات XOR را با بایت `i`ام از رشته **ثابت `what`** مقایسه میکند. این مقایسه یک مقدار بولین (True یا False) برمیگرداند.
// 5. `(unsigned __int8)v4 & (...)`: مقدار فعلی `v4` (که 0 یا 1 است) را با نتیجه بولین مقایسه قبل، به صورت **AND بیتی** میکند.
// * اگر `v4` قبلاً 0 (False) شده باشد، حاصل `AND` همیشه 0 باقی میماند.
// * اگر `v4` هنوز 1 (True) باشد، و نتیجه مقایسه `True` باشد، `v4` همچنان 1 میماند.
// * اگر `v4` هنوز 1 (True) باشد، اما نتیجه مقایسه `False` باشد، `v4` به 0 تغییر میکند.
// این یعنی **همه مقایسهها در طول حلقه باید True باشند** تا `v4` در پایان 1 (صحیح) باقی بماند.
}
return v4;
// پس از اتمام حلقه، مقدار نهایی `v4` (1 برای موفقیت، 0 برای عدم موفقیت) را برمیگرداند.
}
بیایید تابع check را که از کد دیکامپایل شده برنامه task.out به دست آمده است، به دقت بررسی کنیم. این تابع کلید اصلی اعتبارسنجی ورودی کاربر است.
تحلیل تابع check 🕵️♀️
__int64 __fastcall check(__int64 a1) // 1. تعریف تابع check: یک آرگومان (a1) از نوع __int64 میگیرد که به ورودی کاربر (رشته 's' در تابع main) اشاره دارد. مقدار برگشتی آن نیز __int64 است (که در واقع یک Boolean (true/false) را نشان میدهد). { int v2; // [rsp+1Ch] [rbp-1Ch] // 2. تعریف متغیر محلی: برای ذخیره موقت یک کاراکتر (بایت) از ورودی کاربر. int i; // [rsp+28h] [rbp-10h] // 3. تعریف متغیر محلی: به عنوان شمارنده حلقه 'for'. unsigned int v4; // [rsp+2Ch] [rbp-Ch] // 4. تعریف متغیر محلی: برای نگهداری نتیجه نهایی اعتبارسنجی (true/false).
v4 = 1; // 5. مقداردهی اولیه: 'v4' را با 1 (معادل True یا موفقیت) مقداردهی میکند. این فرض اولیه است که ورودی صحیح است، مگر اینکه در حلقه نقض شود.
for ( i = 0; i < strlen(what); ++i ) // 6. شروع حلقه 'for': // حلقه از i = 0 شروع میشود. // تا زمانی که 'i' کوچکتر از طول رشته 'what' باشد، ادامه مییابد (مثلاً 56 تکرار). // در هر تکرار، 'i' یک واحد افزایش مییابد. { v2 = *(char *)(a1 + i); // 7. دسترسی به کاراکتر فعلی: // *(char *)(a1 + i) به بایت 'i'ام از رشته ورودی کاربر (a1) دسترسی پیدا میکند و آن را در 'v2' ذخیره میکند.
// 8. هسته الگوریتم اعتبارسنجی:
v4 = (unsigned __int8)v4 & ((*(char *)(a1 + (i + 1) % strlen(what)) ^ v2) == what[i]);
// این خط یک عملیات پیچیده است که در هر تکرار، 'v4' را بروزرسانی میکند:
// الف) `*(char *)(a1 + (i + 1) % strlen(what))`: به بایت بعدی از ورودی کاربر (a1) دسترسی پیدا میکند.
// `% strlen(what)` در اینجا بسیار مهم است. اگر به انتهای رشته برسد، به ابتدای آن برمیگردد و بایت اول را در نظر میگیرد. این یک عملیات "چرخشی" یا "گرد" است.
// ب) `^ v2`: بایت بعدی (از ورودی کاربر) با بایت فعلی (v2) ورودی کاربر **XOR** میشود.
// ج) `... == what[i]`: نتیجه XOR مرحله قبل با بایت `i`ام از رشته ثابت **'what'** مقایسه میشود.
// د) `(unsigned __int8)v4 & (...)`: نتیجه این مقایسه (که True/False است) با مقدار فعلی `v4` به صورت **AND** بیتی میشود.
// یعنی اگر در هر مرحله یکی از مقایسهها `False` شود، `v4` به `0` (False) تبدیل میشود و برای باقی حلقه همان `0` باقی میماند. این بدان معناست که همه مقایسهها باید `True` باشند تا تابع `True` برگرداند.
} return v4; // 9. بازگرداندن نتیجه: در نهایت، مقدار نهایی 'v4' (1 برای موفقیت، 0 برای عدم موفقیت) را برمیگرداند. }
دید کلی و کاربرد این تابع 🧠
تابع check یک الگوریتم اعتبارسنجی سفارشی را پیادهسازی میکند. این تابع مسئول تعیین این است که آیا ورودی کاربر (a1) "درست" است یا خیر.
دقیقاً چه کاری انجام میدهد:
این تابع هر دو بایت متوالی در ورودی کاربر (a1) را با هم XOR میکند و نتیجه را با بایت متناظر در رشته ثابت what مقایسه میکند. این مقایسه برای تمام طول رشته what انجام میشود و از یک عملگر چرخشی (modulo) برای انتخاب بایت بعدی استفاده میکند.
چرا این کار را میکند؟
این الگو در چالشهای CTF رایج است:
1- شناسایی فلگ صحیح: اگر بتوانیم ورودی a1 را پیدا کنیم که باعث شود check مقدار 1 (True) برگرداند، به این معنی است که ورودی ما همان "فلگ" صحیح است.
2- پنهان کردن فلگ واقعی: این تابع یک لایه پنهانسازی اضافه میکند. اگرچه main دارای یک فلگ هاردکد شده بود که در صورت check ناموفق چاپ میشد (یک فلگ جعلی)، اما هدف واقعی چالش این است که ورودیای پیدا شود که این check را دور بزند.
چالش برای تحلیلگر:
برای حل کامل چالش، تحلیلگر باید این تابع را معکوس (reverse) کند. یعنی با داشتن رشته what، باید بتواند رشته a1 را پیدا کند که تمام شرطهای مقایسهای را برآورده کند. این کار معمولاً شامل استفاده از اسکریپتنویسی (مانند پایتون) و الگوریتمهای XOR معکوس است، مشابه آنچه در قطعات کد پایتون سوم و چهارم دیدیم.
به زبان سادهتر، این تابع یک معما است. برنامه از شما میخواهد یک رشته ورودی بدهید که این معما را حل کند. اگر حل کنید، به شاخه "موفق" میروید، در غیر این صورت، به شاخه "ناموفق" (که یک فلگ جعلی را نشان میدهد).
### اسکریپت سوم:
```python
what = b"\x17/'\x17\x1DJy\x03,\x11\x1E&\x0AexjONacA-&\x01LANH'.&\x12>#'Z\x0FO\x0B%:(&HI\x0CJylL'\x1EmtdC\x00\x00\x00\x00\x00\x00\x00\x00"
# متغیر `what` را تعریف میکند که حاوی بایتهای رشته ثابت `what` است.
# این رشته مستقیماً از فایل باینری برنامه اصلی (که در تابع `check()` استفاده میشود) استخراج شده است.
secret = b'B\x0A|_\x22\x06\x1Bg7#\x5CF\x0A)\x090Q8_{Y\x13\x18\x0DP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
# متغیر `secret` را تعریف میکند که حاوی بایتهای کلید رمزنگاری است.
# این مقدار نیز از تحلیل فایل باینری به دست آمده و برای عملیات XOR نهایی استفاده میشود.
what = what.rstrip(b'\0')
# بایتهای NULL (`\0`) را از انتهای رشته `what` حذف میکند.
# این کار برای اطمینان از اینکه طول دقیق دادهها در محاسبات بعدی استفاده شود، انجام میشود.
secret = secret.rstrip(b'\0')
# بایتهای NULL (`\0`) را از انتهای رشته `secret` حذف میکند.
# این کار نیز برای حفظ دقت طول رشته در عملیات XOR ضروری است.
for i in range(30, 127):
# این حلقه یک **بروتفورس (حدس زدن)** هوشمند را آغاز میکند.
# `range(30, 127)` شامل کدهای ASCII برای کاراکترهای قابل چاپ رایج (مانند اعداد، حروف، نمادها) است.
# در هر تکرار، `i` به عنوان **اولین کاراکتر احتمالی ورودی صحیح** برای تابع `check()` در نظر گرفته میشود.
flag = []
# در هر تکرار حلقه بیرونی (`for i in ...`)، یک لیست خالی به نام `flag` ایجاد میشود.
# این لیست `flag` در اینجا در واقع دنباله بایتهای **ورودی صحیح برای تابع `check()`** (یعنی رشته `s` در تابع `main`) را نشان میدهد که ما در حال تلاش برای بازسازی آن هستیم.
flag.append(i)
# اولین کاراکتر حدس زده شده (`i`) به عنوان اولین بایت به لیست `flag` اضافه میشود.
for j in range(len(what)):
# این حلقه داخلی، بقیه بایتهای دنباله ورودی صحیح (`flag` در اینجا) را بازسازی میکند.
# این فرآیند **معکوس کردن منطق تابع `check()`** است.
# از آنجایی که از تابع `check()` میدانیم: `(بایت_بعدی_ورودی ^ بایت_فعلی_ورودی) == what[j]`
# با استفاده از خاصیت XOR (`A ^ B = C` پس `A = C ^ B`)، میتوانیم بایت بعدی را به صورت زیر محاسبه کنیم:
# `بایت_بعدی_ورودی = بایت_فعلی_ورودی ^ what[j]`
flag.append(flag[j]^what[j])
# `flag[j]` (که در اینجا همان `بایت_فعلی_ورودی` است) با `what[j]` (که از رشته ثابت `what` میآید) XOR میشود.
# نتیجه XOR به عنوان `بایت_بعدی_ورودی` (یعنی `flag[j+1]`) به لیست `flag` اضافه میشود.
# این خط به صورت زنجیرهای بایتهای بعدی ورودی صحیح را بر اساس بایت قبلی و مقادیر `what` بازسازی میکند.
newFlag = ""
# یک رشته خالی به نام `newFlag` ایجاد میشود.
# این رشته برای ذخیره فلگ نهایی (که پس از رمزگشایی کاندیدای ورودی صحیح با `secret` به دست میآید) استفاده میشود.
for j in range(len(what)):
# این حلقه، کاندیدای ورودی صحیح بازسازی شده (که در لیست `flag` است) را با کلید `secret` XOR میکند.
# این عملیات **شبیهسازی فرآیند نهایی تولید فلگ توسط برنامه اصلی** است که در صورت موفقیت `check()` انجام میشود.
newFlag += chr(flag[j]^secret[j%len(secret)])
# `flag[j]`: بایت `j`ام از کاندیدای ورودی صحیح بازسازی شده.
# `secret[j%len(secret)]`: بایت متناظر از کلید `secret` را انتخاب میکند (با استفاده از عملگر `%` برای تکرار چرخشی کلید).
# `^`: عملیات XOR را بین دو بایت انجام میدهد.
# `chr()`: نتیجه عددی XOR را به کاراکتر ASCII مربوطه تبدیل میکند.
# `newFlag += ...`: کاراکتر حاصل را به انتهای رشته `newFlag` اضافه میکند.
print(newFlag)
# هر کاندیدای `newFlag` تولید شده (که بر اساس هر حدس اولیه `i` و فرآیند کامل بازسازی و XOR نهایی ایجاد شده است) در خروجی چاپ میشود.
این قطعه کد پایتون یک رویکرد بروت فورس (Brute-Force) و معکوسسازی الگوریتم تابع check() را برای پیدا کردن فلگ اصلی پیادهسازی میکند. هدف این کد، تولید کاندیداهای فلگ است تا یکی از آنها فلگ صحیح باشد.
نکته: بروتفورس در علوم کامپیوتر و امنیت سایبری به معنای یک روش حل مسئله است که در آن تمام ترکیبهای ممکن برای یافتن راهحل امتحان میشوند تا زمانی که راهحل صحیح پیدا شود. این روش معمولاً شامل امتحان کردن هر گزینه موجود به صورت منظم و خستهکننده است تا زمانی که مورد درست کشف شود.
دید کلی درباره این قطعه کد 🧩
این قطعه کد پایتون یک حمله رمزی معکوس (Cryptographic Reverse Engineering) برای یافتن فلگ اصلی است. به جای اینکه سعی کند تابع check() را با ورودیهای تصادفی دور بزند (که ناکارآمد است)، این کد منطق check() را معکوس میکند.
نکته: مهندسی معکوس رمزنگاری که گاهی اوقات به آن رمزنگاری معکوس نیز گفته میشود، فرایند تحلیل و بازسازی الگوریتمها و مکانیزمهای رمزنگاری پنهان شده در یک برنامه یا سیستم است. هدف از این کار کشف نحوه رمزنگاری و رمزگشایی دادهها بدون دسترسی به سورس کد اصلی یا مستندات طراحی است.
فرایند کار آن به شرح زیر است: استخراج دادهها: فرض بر این است که مقادیر what و secret قبلاً از باینری برنامه استخراج شدهاند.
حدس هوشمندانه (Brute-Force بر روی اولین کاراکتر): میدانیم که فلگها معمولاً از کاراکترهای قابل چاپ تشکیل شدهاند. این کد با حدس زدن تمامی کاراکترهای قابل چاپ ASCII (کدهای 30 تا 126) به عنوان اولین کاراکتر فلگ صحیح (ورودی s) شروع میکند.
بازسازی دنباله فلگ: برای هر حدس اولیه، این کد از منطق معکوس تابع check() استفاده میکند. از آنجایی که میدانیم (char_next ^ char_current) == what[j] باید درست باشد، میتوانیم char_next را با what[j] ^ char_current محاسبه کنیم. این فرآیند به صورت زنجیرهای ادامه مییابد و تمامی کاراکترهای بعدی فلگ صحیح (یا کاندیدای فلگ) را بر اساس کاراکتر قبلی و بایت متناظر از what بازسازی میکند.
شبیهسازی خروجی فلگ صحیح: پس از بازسازی کامل یک کاندیدای فلگ صحیح (flag در این کد)، این کد همان عملیات XORی را انجام میدهد که برنامه اصلی در صورت ورودی صحیح انجام میدهد (یعنی XOR کردن ورودی صحیح با secret).
چاپ کاندیداها: در نهایت، برای هر حدس اولیه، یک newFlag چاپ میشود. تحلیلگر باید خروجیها را بررسی کند و فلگی را که به نظر معنیدار و صحیح میآید، پیدا کند.
این رویکرد بسیار کارآمدتر از حدس زدن کل فلگ به صورت تصادفی است، زیرا از دانش بدستآمده از تحلیل تابع check() برای محدود کردن فضای جستجو و بازسازی هوشمندانه فلگ استفاده میکند.
اسکریپت چهارم:
what = b"\x17/'\x17\x1DJy\x03,\x11\x1E&\x0AexjONacA-&\x01LANH'.&\x12>#'Z\x0FO\x0B%:(&HI\x0CJylL'\x1EmtdC\x00\x00\x00\x00\x00\x00\x00\x00"
# متغیر `what` را تعریف میکند که حاوی بایتهای رشته ثابت `what` است.
# این رشته مستقیماً از فایل باینری برنامه اصلی (که در تابع `check()` استفاده میشود) استخراج شده است.
secret = b'B\x0A|_\x22\x06\x1Bg7#\x5CF\x0A)\x090Q8_{Y\x13\x18\x0DP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
# متغیر `secret` را تعریف میکند که حاوی بایتهای کلید رمزنگاری است.
# این مقدار نیز از تحلیل فایل باینری به دست آمده و برای عملیات XOR نهایی استفاده میشود.
what = what.rstrip(b'\0')
# بایتهای NULL (`\0`) را از انتهای رشته `what` حذف میکند.
# این کار برای اطمینان از اینکه طول دقیق دادهها در محاسبات بعدی استفاده شود، انجام میشود.
secret = secret.rstrip(b'\0')
# بایتهای NULL (`\0`) را از انتهای رشته `secret` حذف میکند.
# این کار نیز برای حفظ دقت طول رشته در عملیات XOR ضروری است.
for i in range(30, 127):
# این حلقه یک **بروتفورس (حدس زدن)** هوشمند را آغاز میکند.
# `range(30, 127)` شامل کدهای ASCII برای کاراکترهای قابل چاپ رایج (مانند اعداد، حروف، نمادها) است.
# در هر تکرار، `i` به عنوان **اولین کاراکتر احتمالی ورودی صحیح** برای تابع `check()` در نظر گرفته میشود.
flag = []
# در هر تکرار حلقه بیرونی (`for i in ...`)، یک لیست خالی به نام `flag` ایجاد میشود.
# این لیست `flag` در اینجا در واقع دنباله بایتهای **ورودی صحیح برای تابع `check()`** (یعنی رشته `s` در تابع `main`) را نشان میدهد که ما در حال تلاش برای بازسازی آن هستیم.
flag.append(i)
# اولین کاراکتر حدس زده شده (`i`) به عنوان اولین بایت به لیست `flag` اضافه میشود.
for j in range(len(what)):
# این حلقه داخلی، بقیه بایتهای دنباله ورودی صحیح (`flag` در اینجا) را بازسازی میکند.
# این فرآیند **معکوس کردن منطق تابع `check()`** است.
# از آنجایی که از تابع `check()` میدانیم: `(بایت_بعدی_ورودی ^ بایت_فعلی_ورودی) == what[j]`
# با استفاده از خاصیت XOR (`A ^ B = C` پس `A = C ^ B`)، میتوانیم بایت بعدی را به صورت زیر محاسبه کنیم:
# `بایت_بعدی_ورودی = بایت_فعلی_ورودی ^ what[j]`
flag.append(flag[j]^what[j])
# `flag[j]` (که در اینجا همان `بایت_فعلی_ورودی` است) با `what[j]` (که از رشته ثابت `what` میآید) XOR میشود.
# نتیجه XOR به عنوان `بایت_بعدی_ورودی` (یعنی `flag[j+1]`) به لیست `flag` اضافه میشود.
# این خط به صورت زنجیرهای بایتهای بعدی ورودی صحیح را بر اساس بایت قبلی و مقادیر `what` بازسازی میکند.
newFlag = ""
# یک رشته خالی به نام `newFlag` ایجاد میشود.
# این رشته برای ذخیره فلگ نهایی (که پس از رمزگشایی کاندیدای ورودی صحیح با `secret` به دست میآید) استفاده میشود.
for j in range(len(what)):
# این حلقه، کاندیدای ورودی صحیح بازسازی شده (که در لیست `flag` است) را با کلید `secret` XOR میکند.
# این عملیات **شبیهسازی فرآیند نهایی تولید فلگ توسط برنامه اصلی** است که در صورت موفقیت `check()` انجام میشود.
char = chr(flag[j]^secret[j%len(secret)])
# `flag[j]`: بایت `j`ام از کاندیدای ورودی صحیح بازسازی شده.
# `secret[j%len(secret)]`: بایت متناظر از کلید `secret` را انتخاب میکند (با استفاده از عملگر `%` برای تکرار چرخشی کلید).
# `^`: عملیات XOR را بین دو بایت انجام میدهد.
# `chr()`: نتیجه عددی XOR را به کاراکتر ASCII مربوطه تبدیل میکند.
if ord(char) not in range(30, 127):
# این خط **فیلتر اصلی** این قطعه کد است و آن را از نسخه قبلی (سوم) متمایز میکند.
# `ord(char)`: کد ASCII کاراکتر `char` را برمیگرداند.
# `range(30, 127)`: محدوده کدهای ASCII برای کاراکترهای قابل چاپ رایج است (شامل اعداد، حروف، نمادها و برخی کاراکترهای کنترلی محدود).
# `if ... not in ...`: اگر کاراکتر تولید شده **قابل چاپ نباشد** (مثلاً یک کاراکتر کنترلی، غیرقابل خواندن یا غیرمعمول باشد)...
continue
# ...این خط باعث میشود که حلقه داخلی (برای `j` فعلی) به تکرار بعدی برود و این کاراکتر به `newFlag` اضافه نشود.
# این فیلتر به شدت نتایج چاپ شده را محدود میکند و فقط آن دسته از خروجیهایی را که شبیه یک فلگ معتبر (شامل کاراکترهای قابل چاپ) هستند، نمایش میدهد.
else:
# اگر کاراکتر قابل چاپ باشد (یعنی کد ASCII آن در محدوده مجاز باشد)...
newFlag += char
# ...آن کاراکتر به رشته `newFlag` اضافه میشود.
print(newFlag)
# هر `newFlag` تولید شده (که بر اساس هر حدس اولیه `i` و فرآیند کامل بازسازی و XOR نهایی، و پس از فیلتر کاراکترهای غیرقابل چاپ تشکیل شده است) در خروجی چاپ میشود.
دید کلی درباره این قطعه کد: این قطعه کد پایتون، که مشابه کد قبلی است اما با یک تفاوت مهم، برای یافتن فلگ واقعی با بروتفورس هوشمند و فیلتر کردن نتایج غیرقابل چاپ طراحی شده است. هدف آن معکوس کردن منطق تابع check() از برنامه اصلی و سپس رمزگشایی خروجی با secret است، اما فقط نتایجی را نشان میدهد که شبیه یک فلگ معتبر باشند. این کد یک استراتژی پیشرفتهتر برای کشف فلگ در مقایسه با روش صرفاً رمزگشایی فلگ جعلی است. در حالی که کد قبلی فلگ جعلی را از بایتهای هاردکد شده رمزگشایی میکرد، این کد تلاش میکند تا فلگ واقعی (که با ورودی صحیح تولید میشود) را بیابد.
هدف اصلی: 1- یافتن ورودی صحیح: هدف اصلی این کد، پیدا کردن رشته ورودی (s در تابع main) است که باعث میشود تابع check() در برنامه اصلی True برگرداند.
2- تولید فلگهای کاندیدا: سپس، برای هر ورودی صحیح کاندیدا، خروجی نهایی برنامه را (همانطور که اگر ورودی صحیح داده میشد چاپ میشد) شبیهسازی میکند.
3- فیلتر کردن خروجیها: مهمترین تفاوت با کد قبلی، وجود فیلتر if ord(char) not in range(30, 127) است. این فیلتر به شدت نتایج چاپ شده را محدود میکند. با توجه به اینکه فلگهای CTF معمولاً از کاراکترهای قابل چاپ تشکیل شدهاند، این فیلتر کمک میکند تا فقط آن دسته از نتایج که منطقاً میتوانند فلگ باشند (یعنی شامل تنها کاراکترهای قابل چاپ هستند) نمایش داده شوند و نویز و خروجیهای بیمعنی حذف گردند.
چرا این روش؟ این روش یک حدس و بازسازی کنترلشده است. تحلیلگر به جای اینکه میلیونها ورودی تصادفی را امتحان کند، از دانش خود درباره الگوریتم check() و ماهیت کاراکترهای فلگ استفاده میکند تا تنها کاندیداهای معقول را تولید و آزمایش کند. احتمالاً، در بین خروجیهای این کد، فلگ نهایی و صحیح چالش یافت خواهد شد، زیرا این همان مسیری است که برنامه برای تولید فلگ صحیح طی میکند.
نکتههای تکمیلی:
دربارهی "Artifacts" (ماده دستساز/باقیمانده) در خروجی دیکامپایلر.
در خروجیهای کد C شبهکدی که از دیکامپایلرهایی مانند IDA Pro یا Cutter به دست میآید، گاهی اوقات میبینید که دیکامپایلر نمیتواند به طور کامل دستورالعملهای اسمبلی را به کد C معنیدار تبدیل کند. این موارد را Artifacts یا باقیماندههای تحلیل نادرست مینامیم.
در خروجی jsdec از تابع main، این خطوط را داشتیم: edi = "B\n|"; // این رشته یک artifact از دیکامپایلر است و مربوط به کد واقعی نیست. // ... edi = "B\n|"; // artifact دیکامپایلر
این خطوط زمانی ظاهر میشوند که دیکامپایلر سعی میکند یک رجیستر (مانند EDI) را که در واقع برای عملیات روی آدرسهای حافظه یا مقادیر عددی استفاده میشود، به صورت یک رشته ثابت تفسیر کند. دلیلش این است که شاید در آن لحظه، یک مقدار بایت خاص در رجیستر وجود داشته که شباهت نزدیکی به ابتدای یک رشته (مثل B\n|_) دارد و دیکامپایلر آن را به اشتباه به عنوان آدرس یک رشته تفسیر کرده است.
چرا این مهم است؟ ⚠️ گمراه کننده هستند: اگر به این Artifactsها توجه نکنید، ممکن است فکر کنید این رشتهها واقعاً در کد استفاده میشوند، در حالی که اینطور نیست. این میتواند شما را در تحلیل و تلاش برای بازسازی منطق برنامه گمراه کند.
نیاز به تفکیک: باید بتوانید بین کد منطقی واقعی برنامه و تفسیرهای اشتباه دیکامپایلر تمایز قائل شوید.
تأکید بر دانش اسمبلی: این Artifactsها نشان میدهند که اگرچه دیکامپایلرها بسیار مفیدند، اما همیشه کامل نیستند و گاهی نیاز است برای درک کامل، به کد اسمبلی خام نیز نگاهی بیندازید تا ببینید رجیسترها واقعاً چه مقداری را نگه میدارند.
چگونه این Artifacts را شناسایی و نادیده بگیریم؟ ناهمخوانی با منطق کلی: اگر یک رشته یا عملیات خاص به نظر میرسد که در منطق کلی برنامه جا نمیگیرد یا بیمعنی است (مثلاً یک رشته عجیب که هرگز استفاده نمیشود)، مشکوک شوید. بررسی Xrefs: اگر روی رشته مشکوک (مثلاً B\n|_) در Cutter کلیک راست کنید و "Xrefs to" را بزنید، احتمالاً میبینید که هیچ ارجاع معناداری به آن در بخشهای مهم کد (مانند حلقههای اصلی یا فراخوانیهای توابع) وجود ندارد.
مشاهده اسمبلی: بهترین راه، بررسی کد اسمبلی مربوطه است. در اسمبلی، میبینید که رجیستر EDI ممکن است یک آدرس حافظه یا یک مقدار عددی را بارگذاری کند، نه یک رشته ثابت. در این چالش، با شناخت این Artifacts میتوانستیم مطمئن شویم که رشتههایی مانند B\n|_ جزو دادههای secret یا what نیستند، بلکه صرفاً محصول جانبی فرآیند دیکامپایل هستند. این کمک میکند تا روی دادههای واقعی و مهم تمرکز کنید.
ابزار strip
یک ابزار خط فرمان است که در سیستمعاملهای یونیکس-مانند (مانند لینوکس و macOS) و همچنین در ابزارهای توسعه ویندوز (مانثل MinGW/MSYS2) موجود است. وظیفه اصلی آن حذف نمادهای اشکالزدایی (debugging symbols) و متادیتای غیرضروری از فایلهای اجرایی (باینریها) و کتابخانهها است. وقتی یک برنامه کامپایل میشود، کامپایلر اغلب اطلاعاتی مانند نام توابع، نام متغیرها، و مسیرهای فایلهای سورس را برای کمک به اشکالزدایی (Debugging) در فایل باینری قرار میدهد. ابزار strip این اطلاعات را حذف میکند. این کار باعث کاهش حجم فایل نهایی و همچنین دشوارتر شدن مهندسی معکوس میشود، زیرا تحلیلگر دیگر به این اطلاعات کمکی دسترسی ندارد و باید بیشتر روی کد اسمبلی خام تمرکز کند. به همین دلیل، strip معمولاً در مرحله نهایی بیلد برای تولید نسخههای Release و نهایی محصولات استفاده میشود.
URL: https://www.man7.org/linux/man-pages/man1/strip.1.html
ابزارهای دیگر؛
objdump
چرکنویس؛
better_than_asm_huh Bamboofox{b3tt3r_th4n_4_d3c0mp1l3r_huh} 7h15_v3ry_l0ng_4nd_1_h0p3_th3r3_4r3_n0_7yp0