# درباره‌ی چالش‌های 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 به‌روزرسانی می‌شوند) اسکن کنند. این ابزار به دلیل ماهیت متن‌باز بودن و قابلیت سفارشی‌سازی بالا، گزینه‌ای قدرتمند و انعطاف‌پذیر برای افزودن قابلیت‌های اسکن آنتی‌ویروس به اسکریپت‌ها و سیستم‌های خودکار است. ```sh 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 ```sh sudo apt install libimage-exiftool-perl ``` ### دستور File دستور `file` یک ابزار استاندارد و قدرتمند در سیستم‌عامل‌های شبه‌یونیکس (مانند لینوکس و macOS) است که برای شناسایی نوع محتوای یک فایل به کار می‌رود. برخلاف بسیاری از دستورات که نوع فایل را صرفاً بر اساس پسوند آن حدس می‌زنند، `file` با بررسی جادویی (magic numbers) موجود در ابتدای فایل‌ها، ساختار داخلی و محتوای واقعی آن‌ها را تحلیل می‌کند. این قابلیت به آن اجازه می‌دهد تا حتی فایل‌هایی را که پسوند اشتباه دارند یا اصلاً پسوندی ندارند، به درستی تشخیص دهد؛ مثلاً می‌تواند یک فایل متنی ساده، یک فایل اجرایی باینری (مانند ELF، Mach-O یا PE)، یک تصویر (JPEG، PNG)، یک آرشیو فشرده (ZIP، GZ)، یا حتی یک سند Word را شناسایی کند. این دستور برای مهندسی معکوس، بررسی امنیتی فایل‌ها، یا صرفاً برای درک اینکه یک فایل ناشناخته واقعاً چیست، بسیار مفید است. ```sh 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) است و به دلیل سرعت بالا، پیام‌های خطای خوانا، و پشتیبانی قوی از استانداردهای جدید زبان‌ها، بسیار محبوب شده است. ```sh sudo apt install clang URL: https://github.com/llvm/llvm-project ``` ```sh clang task.ll -mllvm -W -g -W1,-pie -o task.out ``` اجرا: ```sh $ ./task.out Only the chosen one will know what the flag is! Are you the chosen one? flag: ``` خروجی: ```sh flag: Hi 😠😡😠😡😠😡 You are not the chosen one! 😡😠😡😠😡😠 ``` ## گام چهارم: تحلیل باینری ### ابزار Cutter یک پلتفرم مهندسی معکوس (Reverse Engineering) رایگان و متن‌باز است که بر پایه موتور Rizin ساخته شده است. هدف اصلی Cutter ارائه یک محیط پیشرفته و قابل شخصی‌سازی برای مهندسی معکوس است، ضمن اینکه تجربه کاربری را نیز در نظر می‌گیرد. این ابزار توسط مهندسان معکوس برای مهندسان معکوس توسعه یافته است. Cutter ابزاری چندپلتفرمی است و برای سیستم‌عامل‌های اصلی مانند لینوکس، macOS و ویندوز در دسترس می‌باشد. از ویژگی‌های برجسته آن می‌توان به رابط کاربری گرافیکی (GUI) مبتنی بر Qt/C++، ادغام بومی با دی‌کامپایلر Ghidra، نماهای مختلف (مانند Graph View و Hex Editor)، قابلیت اسکریپت‌نویسی با پایتون، پلاگین‌ها و پشتیبانی از دیباگر (Debugger) اشاره کرد. نحوه اجرا برنامه: ```sh chmod +x Cutter*.AppImage; ./Cutter*.AppImage URL: https://cutter.re/ URL: https://github.com/rizinorg/cutter ``` خروجی دیکامپایل فایل task.out ```sh 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 برای خطا) را به عنوان کد خروجی برنامه برمی‌گرداند. } ``` ```sh Cutter: /* jsdec pseudo code output */ /* /home/task.out @ 0x401230 */ // اینها کامنت‌هایی هستند که توسط ابزار دی‌کامپایلر (jsdec) اضافه شده‌اند. // نشان می‌دهند که این کد از فایل `task.out` و از آدرس `0x401230` شروع شده است. #include // یک هدر استاندارد 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#'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() برای محدود کردن فضای جستجو و بازسازی هوشمندانه فلگ استفاده می‌کند. ### اسکریپت چهارم: ```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()` انجام می‌شود. 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() و ماهیت کاراکترهای فلگ استفاده می‌کند تا تنها کاندیداهای معقول را تولید و آزمایش کند. احتمالاً، در بین خروجی‌های این کد، فلگ نهایی و صحیح چالش یافت خواهد شد، زیرا این همان مسیری است که برنامه برای تولید فلگ صحیح طی می‌کند. --- better_than_asm_huh Bamboofox{b3tt3r_th4n_4_d3c0mp1l3r_huh} 7h15_v3ry_l0ng_4nd_1_h0p3_th3r3_4r3_n0_7yp0