Updated: 2021-01-BambooFox/better-than-asm

This commit is contained in:
Hadi Mottale 2025-07-22 11:12:35 +03:30
parent a7620e3144
commit c748f98b21
2 changed files with 633 additions and 34 deletions

Binary file not shown.

View File

@ -1,18 +1,16 @@
# درباره‌ی چالش‌های CTF
**تعریف کلی:** CTF (Capture The Flag) (به فارسی: **پرچم را تصاحب کن**) یک نوع مسابقه امنیت سایبری است که در آن شرکت‌کنندگان (تیم‌ها یا افراد) مهارت‌های خود را در یافتن و بهره‌برداری از آسیب‌پذیری‌ها، تحلیل سیستم‌ها، رمزگشایی و حل مسائل پیچیده امنیت اطلاعات به چالش می‌کشند. 💻🕵️‍♀️
## هدف اصلی:
هدف اصلی در CTF پیدا کردن "**فلگ**" (Flag) است. فلگ معمولاً یک رشته متنی خاص (مانند `flag{this_is_your_flag}`) است که در یک مکان پنهان (مثلاً در یک فایل، دیتابیس، یا خروجی یک برنامه آسیب‌پذیر) قرار دارد. با یافتن و وارد کردن این فلگ در سیستم مسابقه، تیم امتیاز کسب می‌کند.
## دو نوع رایج CTF:
* **رایج‌ترین نوع: Jeopardy (ژئوپاردی)**
* چالش‌ها به صورت مستقل و در دسته‌بندی‌های مختلف (مانند مهندسی معکوس، رمزنگاری، وب، فارنزیک) با امتیازات متفاوت ارائه می‌شوند.
* شرکت‌کنندگان هر چالشی را که می‌خواهند انتخاب کرده و فلگ آن را پیدا می‌کنند.
چالش‌ها به صورت مستقل و در دسته‌بندی‌های مختلف (مانند مهندسی معکوس، رمزنگاری، وب، فارنزیک) با امتیازات متفاوت ارائه می‌شوند.
شرکت‌کنندگان هر چالشی را که می‌خواهند انتخاب کرده و فلگ آن را پیدا می‌کنند.
* **پیچیده‌تر و دینامیک‌تر: Attack-Defense (حمله-دفاع)**
* هر تیم یک سرور (یا مجموعه‌ای از سرویس‌ها) را در اختیار دارد که باید از آن در برابر حملات تیم‌های دیگر دفاع کند و همزمان به سرورهای حریف حمله کرده و فلگ‌های آن‌ها را به دست آورد.
* این نوع CTF مهارت‌های دفاعی و تهاجمی را همزمان می‌سنجد.
هر تیم یک سرور (یا مجموعه‌ای از سرویس‌ها) را در اختیار دارد که باید از آن در برابر حملات تیم‌های دیگر دفاع کند و همزمان به سرورهای حریف حمله کرده و فلگ‌های آن‌ها را به دست آورد.
این نوع CTF مهارت‌های دفاعی و تهاجمی را همزمان می‌سنجد.
## چرا CTF؟
* یادگیری و تمرین مهارت‌های **امنیت سایبری** در محیطی عملی و کنترل‌شده.
@ -24,35 +22,33 @@
در مسابقات CTF با فرمت Jeopardy، چالش‌ها معمولاً به دسته‌بندی‌های مختلفی تقسیم می‌شوند تا تیم‌ها بتوانند بر اساس تخصص خود، چالش‌ها را انتخاب کنند. این دسته‌بندی‌ها می‌توانند کمی متفاوت باشند، اما رایج‌ترین آن‌ها عبارتند از:
* **مهندسی معکوس (Reversing):**
* بررسی و تحلیل کدهای کامپایل‌شده (مانند فایل‌های اجرایی EXE، ELF) برای درک عملکرد آن‌ها، کشف آسیب‌پذیری‌ها یا استخراج اطلاعات پنهان (مثل رمزها یا فلگ‌ها).
* ابزارهای مورد استفاده: دی‌اسامبلرها (مثل IDA Pro، Ghidra، Radare2)، دی‌باگرها (مثل GDB, x64dbg).
بررسی و تحلیل کدهای کامپایل‌شده (مانند فایل‌های اجرایی EXE، ELF) برای درک عملکرد آن‌ها، کشف آسیب‌پذیری‌ها یا استخراج اطلاعات پنهان (مثل رمزها یا فلگ‌ها).
ابزارهای مورد استفاده: دی‌اسامبلرها (مثل IDA Pro، Ghidra، Radare2)، دی‌باگرها (مثل GDB, x64dbg).
* **رمزنگاری (Cryptography):**
* شکستن الگوریتم‌های رمزنگاری، تحلیل پروتکل‌های رمزنگاری، یا پیدا کردن ضعف‌ها در پیاده‌سازی آن‌ها برای رمزگشایی داده‌ها و یافتن فلگ.
* شامل تحلیل الگوهای رمز، کلیدهای ضعیف، یا اشکالات منطقی در سیستم‌های رمزنگاری.
شکستن الگوریتم‌های رمزنگاری، تحلیل پروتکل‌های رمزنگاری، یا پیدا کردن ضعف‌ها در پیاده‌سازی آن‌ها برای رمزگشایی داده‌ها و یافتن فلگ.
شامل تحلیل الگوهای رمز، کلیدهای ضعیف، یا اشکالات منطقی در سیستم‌های رمزنگاری.
* **وب (Web Exploitation):**
* پیدا کردن و بهره‌برداری از آسیب‌پذیری‌های موجود در برنامه‌های تحت وب (مانند SQL Injection, Cross-Site Scripting (XSS), File Inclusion, Authentication Bypass) برای دسترسی به اطلاعات یا اجرای کد.
پیدا کردن و بهره‌برداری از آسیب‌پذیری‌های موجود در برنامه‌های تحت وب (مانند 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 یا اجرای کد دلخواه.
پیدا کردن و بهره‌برداری از آسیب‌پذیری‌ها در برنامه‌های بومی (native binaries) مانند سرریز بافر (Buffer Overflow)، فرمت استرینگ (Format String) یا use-after-free برای کنترل جریان اجرای برنامه و دستیابی به Shell یا اجرای کد دلخواه.
* **استگانوگرافی (Steganography):**
* پیدا کردن اطلاعات پنهان‌شده در فایل‌های رسانه‌ای (تصاویر، صدا، ویدئو) یا سایر فرمت‌های فایل، جایی که داده‌ها به گونه‌ای مخفی شده‌اند که حضور آن‌ها آشکار نباشد.
پیدا کردن اطلاعات پنهان‌شده در فایل‌های رسانه‌ای (تصاویر، صدا، ویدئو) یا سایر فرمت‌های فایل، جایی که داده‌ها به گونه‌ای مخفی شده‌اند که حضور آن‌ها آشکار نباشد.
* **عمومی یا متفرقه (General Skills / Misc):**
* شامل طیف گسترده‌ای از چالش‌هاست که در دسته‌بندی‌های دیگر قرار نمی‌گیرند، مانند چالش‌های مربوط به لینوکس، اسکریپت‌نویسی، اوزینت (OSINT)، یا حل معماهای منطقی.
شامل طیف گسترده‌ای از چالش‌هاست که در دسته‌بندی‌های دیگر قرار نمی‌گیرند، مانند چالش‌های مربوط به لینوکس، اسکریپت‌نویسی، اوزینت (OSINT)، یا حل معماهای منطقی.
* **شبکه (Networking):**
* تحلیل ترافیک شبکه (بسته‌های کپچر شده با Wireshark)، پروتکل‌های شبکه، و پیدا کردن آسیب‌پذیری‌ها یا اطلاعات پنهان در ارتباطات شبکه.
تحلیل ترافیک شبکه (بسته‌های کپچر شده با Wireshark)، پروتکل‌های شبکه، و پیدا کردن آسیب‌پذیری‌ها یا اطلاعات پنهان در ارتباطات شبکه.
* **سیستم عامل‌ها/کانفینگ (OS/Config):**
* چالش‌های مربوط به پیکربندی‌های سیستم عامل، دسترسی به فایل‌ها، مجوزها، یا بهره‌برداری از تنظیمات نادرست در سیستم‌ها.
چالش‌های مربوط به پیکربندی‌های سیستم عامل، دسترسی به فایل‌ها، مجوزها، یا بهره‌برداری از تنظیمات نادرست در سیستم‌ها.
## توضیحات چالش: Bamboo Fox: Better Than Assembly
این چالش 500 امتیاز داشت و در دسته‌بندی **مهندسی معکوس (Reversing)** در سبک CTF از نوع Jeopardy قرار می‌گرفت.
@ -60,18 +56,19 @@
# مسیر حل چالش
## قدم اول: اسکن کردن فایل
## گام اول: اسکن کردن فایل
ابزار ClamAV (Clam AntiVirus) یک موتور آنتی‌ویروس متن‌باز و رایگان است که به طور گسترده برای شناسایی تروجان‌ها، ویروس‌ها، بدافزارها و سایر تهدیدات مخرب استفاده می‌شود. این نرم‌افزار به ویژه در سرورهای ایمیل برای اسکن فایل‌های ضمیمه و جلوگیری از ورود بدافزارها از طریق ایمیل محبوبیت دارد، اما می‌توان از آن برای اسکن فایل‌ها و دایرکتوری‌ها در سیستم‌های لینوکس، یونیکس و ویندوز نیز بهره برد. ابزار خط فرمان اصلی برای اسکن فایل‌ها با ClamAV، دستور `clamscan` است که به کاربران اجازه می‌دهد مسیرهای مشخصی را برای یافتن امضاهای بدافزار (که از پایگاه داده ویروس ClamAV به‌روزرسانی می‌شوند) اسکن کنند. این ابزار به دلیل ماهیت متن‌باز بودن و قابلیت سفارشی‌سازی بالا، گزینه‌ای قدرتمند و انعطاف‌پذیر برای افزودن قابلیت‌های اسکن آنتی‌ویروس به اسکریپت‌ها و سیستم‌های خودکار است.
URL: https://docs.clamav.net/manual/Usage/Scanning.html
URL: https://x.com/pcaversaccio/status/1941114624197231092
```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
@ -85,11 +82,12 @@ sudo apt install libimage-exiftool-perl
file task.ll
sudo apt install file
```
### وب‌سایت FileInfo.com
وب‌سایت FileInfo.com یک دایرکتوری آنلاین بزرگ از پسوندهای فایل است که به شما امکان می‌دهد با وارد کردن پسوند یک فایل، اطلاعات جامعی درباره آن کسب کنید. این وب‌سایت توضیحات مربوط به نوع فایل، دسته‌بندی آن (مانند فایل ویدئویی یا سند)، برنامه‌های نرم‌افزاری مرتبط که می‌توانند آن را باز کنند و اطلاعاتی درباره توسعه‌دهنده فرمت را ارائه می‌دهد، که آن را به ابزاری مفید برای شناسایی فایل‌های ناشناخته تبدیل می‌کند.
## قدم سوم: کامپایل و اجرای فایل
## گام سوم: کامپایل و اجرای فایل
نکته:
پسوند فایل .ll عمدتاً به دو منظور استفاده می‌شود: رایج‌ترین آن، فایل‌های پیش‌نمایش تولید شده توسط نرم‌افزار Combit List & Label است که برای گزارش‌گیری کاربرد دارد. اما در حوزه کامپایلرها، به‌ویژه در پروژه LLVM، این پسوند به فایل‌های سورس نمایش میانی (Intermediate Representation - IR) اشاره دارد که یک فرمت کد سطح پایین و قابل خواندن توسط انسان است و نقش واسطه‌ای بین کد منبع و کد ماشین را ایفا می‌کند تا بهینه‌سازی و تولید کد برای معماری‌های مختلف را تسهیل کند. به‌ندرت نیز ممکن است به فایل‌های کد منبع Lex اشاره داشته باشد. LLVM Bitcode مانند یک زبان مشترک جهانی برای کامپایلرها است
@ -97,9 +95,11 @@ sudo apt install file
کامپایل:
(کلنگ) Clang یک کامپایلر فرانت‌اند (frontend) برای زبان‌های برنامه‌نویسی C، C++، Objective-C و Objective-C++ است. این کامپایلر بخشی از پروژه بزرگتر LLVM (Low Level Virtual Machine) است و به دلیل سرعت بالا، پیام‌های خطای خوانا، و پشتیبانی قوی از استانداردهای جدید زبان‌ها، بسیار محبوب شده است.
URL: https://github.com/llvm/llvm-project
```sh
sudo apt install clang
URL: https://github.com/llvm/llvm-project
```
```sh
@ -112,15 +112,614 @@ 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)
{
// متغیرهای محلی (Local Variables)
// اینها معمولاً توسط دی‌کامپایلر به صورت خودکار نامگذاری می‌شوند (v4, v5, etc.)
// و سپس تحلیلگر می‌تواند نام‌های بامعنی‌تری به آنها بدهد.
char v4; // [rsp+14h] [rbp-94h]
char v5; // [rsp+34h] [rbp-74h]
size_t v6; // [rsp+40h] [rbp-68h] // احتمالاً برای ذخیره طول رشته s استفاده می‌شود.
int j; // [rsp+58h] [rbp-50h] // شمارنده حلقه for در بلاک 'else'
int i; // [rsp+5Ch] [rbp-4Ch] // شمارنده حلقه for در بلاک 'if'
char s[68]; // [rsp+60h] [rbp-48h] BYREF // آرایه‌ای برای ذخیره ورودی کاربر (68 بایت)
int v10; // [rsp+A4h] [rbp-4h] // متغیری برای ذخیره مقدار بازگشتی تابع (معمولاً 0 برای موفقیت، 1 برای خطا)
v10 = 0; // مقدار بازگشتی اولیه را 0 (موفقیت) تنظیم می‌کند.
// چاپ پیام‌های خوش‌آمدگویی و درخواست فلگ از کاربر
printf("Only the chosen one will know what the flag is!\n");
printf("Are you the chosen one?\n");
printf("flag: ");
// دریافت ورودی کاربر
// %64s به این معنی است که حداکثر 64 کاراکتر را در بافر 's' می‌خواند (برای جلوگیری از Buffer Overflow)
__isoc99_scanf("%64s", s);
// محاسبه طول رشته ورودی کاربر
v6 = strlen(s);
// شرط اصلی برنامه: مقایسه طول ورودی کاربر با طول یک رشته پنهان (what)
if ( v6 == strlen(&what) ) // اگر طول ورودی کاربر با طول "what" برابر باشد
{
// اگر تابع 'check(s)' (که احتمالاً یک تابع اعتبارسنجی است) مقدار غیرصفر (true) برگرداند
if ( (unsigned int)check(s) )
{
// حلقه اول: در صورت موفقیت 'check(s)'
// این حلقه یک عملیات XOR روی هر کاراکتر ورودی 's' با کاراکتر متناظر از 'secret' انجام می‌دهد.
// نتیجه به صورت کاراکتر به کاراکتر در خود 's' ذخیره می‌شود.
for ( i = 0; i < strlen(s); ++i )
{
v5 = s[i]; // کاراکتر فعلی از ورودی کاربر
s[i] = secret[i % strlen(secret)] ^ v5; // XOR با 'secret' (به صورت دوره‌ای)
}
}
else // اگر تابع 'check(s)' مقدار صفر (false) برگرداند
{
// حلقه دوم: در صورت عدم موفقیت 'check(s)'
// این حلقه یک عملیات XOR روی هر کاراکتر از 'flag' (که به نظر می‌رسد فلگ واقعی است)
// با کاراکتر متناظر از 'secret' انجام می‌دهد.
// نتیجه به صورت کاراکتر به کاراکتر در 's' ذخیره می‌شود.
for ( j = 0; j < strlen(s); ++j ) // s[j] اینجا همان s ورودی کاربر است
{
v4 = flag[j]; // کاراکتر فعلی از رشته 'flag'
s[j] = secret[j % strlen(secret)] ^ v4; // XOR با 'secret' (به صورت دوره‌ای)
}
}
// چاپ نتیجه عملیات XOR (که در 's' ذخیره شده) با استفاده از فرمت مشخص شده
printf(format, s);
v10 = 0; // تنظیم مقدار بازگشتی به 0 (موفقیت)
}
else // اگر طول ورودی کاربر با طول "what" برابر نباشد
{
// چاپ پیام خطا (asc_40205A احتمالا آدرس یک رشته خطاست)
printf(asc_40205A);
v10 = 1; // تنظیم مقدار بازگشتی به 1 (خطا)
}
return v10; // بازگرداندن وضعیت نهایی برنامه
}
```
```sh
Cutter:
/* jsdec pseudo code output */
/* /home/task.out @ 0x401230 */ // مسیر و آدرس شروع تابع main
#include <stdint.h> // اضافه کردن هدر برای انواع داده استاندارد
int32_t main (int64_t arg_60h) { // تعریف تابع main، arg_60h احتمالاً نشان‌دهنده آرگومان‌های ورودی تابع است
// متغیرهای محلی
// jsdec (دی‌کامپایلر Cutter) نام‌های متغیرها را بر اساس آفست آنها از RSP (Stack Pointer) تعیین می‌کند.
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: شمارنده حلقه دوم
int64_t var_4ch; // معادل i در IDA: شمارنده حلقه اول
const char * s; // معادل char s[68] در IDA: بافر ورودی کاربر. Cutter آن را به صورت اشاره‌گر به char نشان می‌دهد.
int64_t var_4h; // معادل v10 در IDA: متغیر بازگشتی تابع
var_4h = 0; // مقدار بازگشتی اولیه را 0 (موفقیت) تنظیم می‌کند.
// چاپ پیام‌ها با استفاده از رجیستر RDI برای نگهداری آدرس رشته
rdi = "Only the chosen one will know what the flag is!\n"; // RDI رجیستر اولین آرگومان در تابع فراخوانی است.
al = 0; // AL رجیستر برای تعداد آرگومان‌های floating-point در x64 (اینجا 0 است)
eax = printf (rdi); // فراخوانی printf و ذخیره خروجی در EAX
rdi = "Are you the chosen one?\n";
var_54h = eax; // ذخیره موقت مقدار EAX
al = 0;
eax = printf (rdi);
rdi = "flag: ";
var_58h = eax;
al = 0;
eax = printf (rdi);
// دریافت ورودی کاربر
// RSI برای آرگومان دوم (%64s) و RDI برای آرگومان اول (s) در scanf
rsi = &s; // آدرس بافر 's'
rdi = "%64s"; // فرمت ورودی
var_5ch = eax;
al = 0;
eax = isoc99_scanf (); // فراخوانی scanf
rdi = &s;
var_60h = eax;
// محاسبه طول رشته ورودی کاربر
rax = strlen (); // فراخوانی strlen روی 's' (آرگومان RDI)
edi = what; // آرگومان RDI برای strlen دوم (رشته 'what')
var_68h = rax; // ذخیره طول 's' در var_68h (معادل v6 در IDA)
rax = strlen (); // فراخوانی strlen روی 'what' (آرگومان EDI)
rcx = var_68h; // rcx حاوی طول 's' است
if (rcx != rax) { // مقایسه طول 's' با طول 'what'
rdi = data.0040205a; // آدرس رشته خطا (معادل asc_40205A در IDA)
al = 0;
printf (rdi); // چاپ پیام خطا
var_4h = 1; // تنظیم مقدار بازگشتی به 1
goto label_0; // پرش به انتهای برنامه
}
// اگر طول‌ها برابر باشند، تابع check(s) فراخوانی می‌شود
rdi = &s; // آرگومان تابع check
eax = check (); // فراخوانی check()
if (eax == 0) { // اگر check() مقدار 0 (false) برگرداند (معادل else در IDA)
goto label_1; // پرش به بلوک مربوط به حالت 'check' ناموفق
}
// بلوک کد در صورت موفقیت check() (معادل if در IDA)
var_4ch = 0; // شمارنده حلقه اول (معادل i در IDA)
do { // شروع حلقه for
rdi = &s;
rax = (int64_t) var_4ch;
var_70h = rax;
rax = strlen (); // محاسبه strlen(s) در هر تکرار
rcx = var_70h;
if (rcx >= rax) { // شرط خروج از حلقه (i < strlen(s))
goto label_2; // پرش به بعد از حلقه
}
rax = (int64_t) var_4ch;
ecx = *((rsp + rax + 0x60)); // دسترسی به s[i] (rsp+0x60 آدرس شروع بافر s است)
var_74h = ecx; // ذخیره s[i]
rax = (int64_t) var_4ch;
edi = "B\n|_"; // این رشته یک artifact از دی‌کامپایلر است و مربوط به کد واقعی نیست.
var_80h = rax;
rax = strlen (); // محاسبه strlen(secret)
rdx = var_80h;
var_88h = rax;
rax = rdx;
ecx = 0;
edx = ecx;
rsi = var_88h;
rax = rdx:rax / rsi; // محاسبه i / strlen(secret) (در واقع همان i % strlen(secret) است)
rdx = rdx:rax % rsi; // محاسبه i % strlen(secret)
ecx = *((rdx + secret)); // دسترسی به secret[i % strlen(secret)]
r8d = var_74h; // r8d حاوی s[i] است
r8d ^= ecx; // عملیات XOR: s[i] ^ secret[i % strlen(secret)]
rdx = (int64_t) var_4ch;
*((rsp + rdx + 0x60)) = r8b; // ذخیره نتیجه در s[i]
eax = var_4ch;
eax++; // افزایش i
var_4ch = eax;
} while (1); // حلقه بی‌پایان تا زمانی که به 'goto label_2' برسد.
label_2:
rsi = &s;
rdi = format; // آدرس رشته فرمت برای printf
al = 0;
printf (rdi); // چاپ نتیجه
goto label_3; // پرش به انتهای موفقیت‌آمیز برنامه
label_1: // بلوک کد در صورت عدم موفقیت check() (معادل else در IDA)
var_50h = 0; // شمارنده حلقه دوم (معادل j در IDA)
do { // شروع حلقه for
rdi = &s;
rax = (int64_t) var_50h;
var_90h = rax;
rax = strlen (); // محاسبه strlen(s) در هر تکرار
rcx = var_90h;
if (rcx >= rax) { // شرط خروج از حلقه (j < strlen(s))
goto label_4; // پرش به بعد از حلقه
}
rax = (int64_t) arg_60h; // arg_60h در اینجا احتمالاً base address برنامه یا یک متغیر است.
ecx = *((rax + flag)); // دسترسی به flag[j]. 'flag' باید آدرس رشته فلگ باشد.
rax = (int64_t) var_50h;
edi = "B\n|_"; // artifact دی‌کامپایلر
var_94h = ecx; // ذخیره flag[j]
var_a0h = rax;
rax = strlen (); // محاسبه strlen(secret)
rdx = var_a0h;
*(rsp) = rax;
rax = rdx;
ecx = 0;
edx = ecx;
rsi = *(rsp);
rax = rdx:rax / rsi; // محاسبه j / strlen(secret)
rdx = rdx:rax % rsi; // محاسبه j % strlen(secret)
ecx = *((rdx + secret)); // دسترسی به secret[j % strlen(secret)]
r8d = var_94h; // r8d حاوی flag[j] است
r8d ^= ecx; // عملیات XOR: flag[j] ^ secret[j % strlen(secret)]
rdx = (int64_t) var_50h;
*((rsp + rdx + 0x60)) = r8b; // ذخیره نتیجه در s[j]
eax = var_50h;
eax++; // افزایش j
var_50h = eax;
} while (1); // حلقه بی‌پایان
label_4:
rsi = &s;
rdi = format; // آدرس رشته فرمت برای printf
al = 0;
printf (rdi); // چاپ نتیجه
label_3:
var_4h = 0; // تنظیم مقدار بازگشتی به 0 (موفقیت)
label_0:
eax = 0; // این خط نیز مقدار بازگشتی را 0 تنظیم می‌کند
return rax; // بازگرداندن وضعیت نهایی
}
```
## گام پنجم: منطق برنامه
### منطق کلی برنامه (از هر دو کد دیکامپایل شده):
- برنامه سه پیام خوش‌آمدگویی و سپس 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 بسیار رایج است زیرا به تحلیلگر اجازه می‌دهد با معکوس کردن عملیات، به داده‌های پنهان (مثل فلگ) دست پیدا کند.
## گام ششم: اسکریپت نویسی برای تحلیل
اسکریپت اول:
```sh
secret = b'B\x0A|_\x22\x06\x1Bg7#\x5CF\x0A)\x090Q8_{Y\x13\x18\x0DP\x00...' # متغیر 'secret' را تعریف می‌کند که حاوی بایت‌های کلید رمزنگاری است. این مقدار از تحلیل فایل باینری اصلی (مثلاً با Cutter) به دست آمده است.
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(&what)').
newStr += chr(secret[i % len(secret)] ^ flag[i]) # این خط هسته عملیات رمزگشایی است:
# `flag[i]`: بایت `i`ام از رشته رمزنگاری شده 'flag' را انتخاب می‌کند.
# `secret[i % len(secret)]`: بایت `i`ام از کلید 'secret' را انتخاب می‌کند. عملگر `% len(secret)` تضمین می‌کند که اگر طول 'secret' از 56 کمتر باشد، کلید به صورت چرخشی (دوره‌ای) تکرار شود و هر بایت از 'flag' با یک بایت از '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" در کد اصلی می‌شود و یک فلگ متفاوت را تولید می‌کند.
اسکریپت دوم:
```python
__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 را که از کد دی‌کامپایل شده برنامه task.out به دست آمده است، به دقت بررسی کنیم. این تابع کلید اصلی اعتبارسنجی ورودی کاربر است.
تحلیل تابع check 🕵️‍♀️
C
__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"
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'
# این دو خط، رشته‌های بایت 'what' و 'secret' را با مقادیر استخراج شده از فایل باینری تعریف می‌کنند.
# 'what' در تابع check() استفاده می‌شود و 'secret' کلید XORینگ است.
what = what.rstrip(b'\0')
secret = secret.rstrip(b'\0')
# این خطوط بایت‌های NUL (صفر) را از انتهای رشته‌های 'what' و 'secret' حذف می‌کنند.
# در C، رشته‌ها با بایت NUL (\0) خاتمه می‌یابند. در پایتون، برای کار با طول دقیق داده‌ها، اغلب این بایت‌های اضافه حذف می‌شوند.
for i in range(30, 127):
# این حلقه یک بروت فورس (حدس زدن) روی اولین کاراکتر فلگ (که ورودی 's' تابع main و 'a1' تابع check است) انجام می‌دهد.
# range(30, 127) شامل کدهای ASCII برای کاراکترهای قابل چاپ (اعداد، حروف، نمادها) است.
# فرض می‌شود که کاراکتر اول فلگ در این محدوده قرار دارد.
flag = [] # در هر تکرار حلقه 'i' (برای هر حدس کاراکتر اول)، یک لیست خالی 'flag' ایجاد می‌شود.
flag.append(i) # اولین حدس (کاراکتر 'i') به عنوان اولین عنصر به لیست 'flag' اضافه می‌شود.
# این لیست 'flag' در اینجا در واقع کاندیدای رشته ورودی صحیح برای تابع check() است (یعنی همان 's' که باید پیدا شود).
for j in range(len(what)):
# این حلقه بقیه کاراکترهای کاندیدای فلگ را بر اساس منطق تابع check() بازسازی می‌کند.
# تابع check() از شرط `(*(char *)(a1 + (i + 1) % strlen(what)) ^ v2) == what[i]` استفاده می‌کرد.
# این به معنی `char_next ^ char_current == what[j]` است.
# با استفاده از خاصیت XOR (`A ^ B = C` پس `A = C ^ B`)، می‌توانیم کاراکتر بعدی را بازسازی کنیم:
# `char_next = what[j] ^ char_current`
flag.append(flag[j]^what[j])
# `flag[j]` در اینجا همان `char_current` است.
# `what[j]` همان مقدار 'what[i]' از تابع check است.
# نتیجه XOR به عنوان `char_next` (یعنی `flag[j+1]`) به لیست اضافه می‌شود.
# این خط در واقع الگوریتم check را برای پیدا کردن ورودی صحیح، معکوس می‌کند.
newFlag = "" # یک رشته خالی 'newFlag' برای ذخیره نتیجه نهایی رمزگشایی ایجاد می‌شود.
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 (به صورت دوره‌ای)
# `chr(...)`: تبدیل نتیجه XOR به کاراکتر و اضافه کردن به 'newFlag'.
print(newFlag) # هر کاندیدای 'newFlag' تولید شده (که بر اساس هر حدس اولیه 'i' ایجاد شده است) چاپ می‌شود.
```
این قطعه کد پایتون یک رویکرد بروت فورس (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"
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'
# این دو خط، رشته‌های بایت `what` و `secret` را با مقادیر استخراج شده از فایل باینری تعریف می‌کنند.
# `what` در تابع `check()` برای اعتبارسنجی ورودی استفاده می‌شود و `secret` کلید XORینگ برای رمزنگاری/رمزگشایی است.
what = what.rstrip(b'\0')
secret = secret.rstrip(b'\0')
# این خطوط بایت‌های `NUL` (`\0`) را از انتهای رشته‌های بایت `what` و `secret` حذف می‌کنند.
# این کار برای اطمینان از اینکه طول دقیق داده‌ها در عملیات بعدی استفاده شود، انجام می‌شود.
for i in range(30, 127):
# این حلقه یک **بروت‌فورس هوشمند** را آغاز می‌کند.
# `range(30, 127)` شامل کدهای ASCII برای کاراکترهای قابل چاپ رایج است (مانند اعداد، حروف، نمادها).
# در هر تکرار، `i` به عنوان **اولین کاراکتر احتمالی فلگ صحیح** (ورودی‌ای که `check()` را True می‌کند) در نظر گرفته می‌شود.
flag = [] # برای هر حدس `i`، یک لیست خالی `flag` جدید ایجاد می‌شود.
flag.append(i) # حدس فعلی `i` به عنوان اولین بایت به این لیست `flag` اضافه می‌شود.
# این لیست `flag` در اینجا در واقع دنباله بایت‌های **ورودی صحیح برای تابع `check()`** را نشان می‌دهد.
for j in range(len(what)):
# این حلقه بقیه بایت‌های دنباله ورودی صحیح (`flag` در اینجا) را بازسازی می‌کند.
# این فرآیند **معکوس کردن منطق تابع `check()`** است:
# از آنجایی که می‌دانیم `(next_char_of_input ^ current_char_of_input) == what[j]` باید درست باشد (از تابع `check()`),
# می‌توانیم `next_char_of_input` را با `current_char_of_input ^ what[j]` به دست آوریم.
flag.append(flag[j]^what[j])
# `flag[j]` (که `current_char_of_input` است) با `what[j]` XOR می‌شود.
# نتیجه به عنوان `next_char_of_input` به لیست `flag` اضافه می‌شود.
newFlag = "" # یک رشته خالی `newFlag` برای ذخیره نتیجه نهایی رمزگشایی ایجاد می‌شود.
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` (به صورت دوره‌ای).
# `chr(...)`: تبدیل نتیجه XOR به کاراکتر.
if ord(char) not in range(30, 127):
# این خط **فیلتر اصلی** این قطعه کد است و آن را از نسخه قبلی متمایز می‌کند.
# `ord(char)`: کد ASCII کاراکتر `char` را برمی‌گرداند.
# `range(30, 127)`: محدوده کدهای ASCII برای کاراکترهای قابل چاپ است.
# `if ... not in ...`: اگر کاراکتر تولید شده **قابل چاپ نباشد** (مثلاً یک کاراکتر کنترلی یا غیرعادی باشد)...
continue
# ...کل حلقه داخلی (برای این کاندیدای `newFlag`) ادامه پیدا نمی‌کند و به تکرار بعدی (برای `j`) می‌رود.
# این باعث می‌شود رشته `newFlag` ناقص بماند یا اصلاً تشکیل نشود.
else:
# اگر کاراکتر قابل چاپ باشد...
newFlag += char
# ...آن را به رشته `newFlag` اضافه می‌کند.
print(newFlag) # هر `newFlag` تولید شده (که بر اساس هر حدس اولیه `i` و فیلتر کاراکترهای قابل چاپ تشکیل شده) چاپ می‌شود.
```
دید کلی درباره این قطعه کد:
این قطعه کد پایتون، که مشابه کد قبلی است اما با یک تفاوت مهم، برای یافتن فلگ واقعی با بروت‌فورس هوشمند و فیلتر کردن نتایج غیرقابل چاپ طراحی شده است. هدف آن معکوس کردن منطق تابع 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