برنامه‌نویس‌ها همواره تعاریف متفاوتی از خیلی از جنبه‌های زندگی ارائه می‌کنند، یکی از این تعاریف جدید، معنای متفاوت آبگوشت (Abgoosht) است! آن‌ها به کد‌هایی که سهوا یا عمدا مبهم شده باشند، آبگوشت می‌گویند. با این تعریف، آبگوشتینگ به عملیات مبهم‌سازی (Obfuscation) و دی‌آبگوشتینگ به عملیات رفع ابهام (DeObfuscation) گفته می‌شود.

دیواین (Diwin) که همچنان هنوز هم مثل قبل بر پیاده‌سازی ایده‌های عجیب خود استوار است، این‌بار در سری جدید مسابقات #المپیک‌فناوری پردیس، تصمیم گرفته تا سطح امنیت مسابقات امسال را به شدت و به طرز ابلفضلی‌ای افزایش دهد تا جلوی تمام تقلب‌ها و نشت کد‌ها را در میانه‌ی مسابقات بگیرد. او اما از روشی بسیار عجیبی برای این کار استفاده خواهد کرد، درست کردن آب‌گوشت از کد‌های ارسال شده توسط شرکت‌کنندگان!

دیواین برای آبگوشتینگ (Obfuscation) کد‌های شرکت‌کنندگان از پارسر (Parser) قدرتمندی به نام abgoosht_parser که از محصولات تولید داخل، توسط المپیک‌فناوریون (Olampici Fanavarion) برتر پارسال است، استفاده خواهد کرد اما از آن‌جایی که دیواین در این سری از مسابقات المپیک فناوری، برای حفاظت بیشتر مسابقات باید بیشتر از همیشه حواسش به همه باشد، تنها کد‌های پارسر abgoosht_parser را در اختیار شما قرار داده است و شما وظیفه دارید تا تکنیک‌های آبگوشتینگ مد نظر او را در این سوال پیاده‌سازی کنید.

تصویر سوال چهارم

پروژه اولیه

برای دانلود پروژه‌ی اولیه روی این لینک کلیک کنید.

ساختار فایل‌ها

abgoosht-fanavari
├── Dockerfile
├── abgoosht_parser
├── benchmarks
├── gradio_app.py
├── main.py
├── obfuscator
│    ├── parser.py
│    ├── generator.py
│    ├── techniques
│    │   ├── alias_generator.py
│    │   ├── dead_code.py
│    │   ├── expr_complexifier.py
│    │   ├── opaque_predicate.py
│    │   ├── function_splitter.py
│    │   ├── misleading_comments.py
│    │   ├── flow_flattener.py
│    │   └── rename_vars.py
│    └── transformer.py
└── requirements.txt

راه‌اندازی پروژه

برای اجرای پروژه، باید داکر، پایتون و ابزار pip را از قبل نصب کرده باشید.

  • ابتدا پروژه‌ی اولیه را دانلود و از حالت فشرده خارج کنید.

  • در پوشه‌ی اصلی پروژه، یک محیط مجازی پایتون (venv) ایجاد و فعال کنید:

python -m venv venv
source venv/bin/activate  # در ویندوز: venv\Scripts\activate
  • دستور زیر را برای نصب نیازمندی‌ها در پوشه‌ی اصلی پروژه اجرا کنید:
pip install -r requirements.txt
  • برای اجرای رابط کاربری پروژه که از قبل با استفاده از Gradio طراحی شده است، دستور زیر را در مسیر پوشه‌ی اصلی پروژه اجرا کنید:
python gradio_app.py

در صورت اجرای موفق، یک لینک در خروجی نمایش داده می‌شود که می‌توانید آن را در مرورگر باز کنید.

  • برای اجرای تست‌های نمونه‌ی پروژه، می‌توانید از دستور زیر استفاده کنید:
python -m unittest discover tests

اجرای پروژه با داکر (Docker)

در صورتی که ابزار داکر روی سیستم شما نصب است، می‌توانید پروژه را به کمک داکرفایل آماده‌ی Dockerfile اجرا کنید:

  • ابتدا پروژه اولیه را دانلود و از حالت فشرده خارج کنید.

  • در پوشه‌ی اصلی پروژه، دستور زیر را برای ساخت ایمیج از روی داکرفایل اجرا کنید:

docker build -t abgoosht-app .
  • سپس برای اجرای پروژه، دستور زیر را وارد کنید:
docker run -p 7860:80 \
  -v $(pwd)/examples:/app/examples \
  -v $(pwd)/benchmarks:/app/benchmarks \
  abgoosht-app

در صورت اجرای موفق، رابط کاربری پروژه از طریق آدرس http://localhost:7860 قابل دسترسی خواهد بود.

  • برای اجرای تست‌ها در محیط داکر، دستور زیر را اجرا کنید:
docker exec -it abgoosht-app python -m unittest discover tests

جزئیات پروژه

در این پروژه، شما قرار است مجموعه‌ای از تکنیک‌های مبهم‌سازی کدهای MiniC را روی ساختار نحوی انتزاعی (AST) اعمال کنید. هدف اصلی، پیاده‌سازی ابزاری است که بتوانند کدهای MiniC را به صورت ایستا تحلیل کرده و تغییراتی مانند افزودن کامنت‌های بی‌معنا، بازنویسی عبارات منطقی، افزودن انتساب‌های زائد و بازنویسی جریان کنترل را روی آن اعمال کنند. برای این کار، از AST تولیدشده توسط پارسر abgoosht_parser استفاده می‌کنید و با بهره‌گیری از دیزاین پترن ویزیتور (Visitor)، گره‌های مختلف درخت را پیمایش و تکنیک‌های خواسته شده را روی کد‌های ورودی اعمال می‌کنید.

معرفی زبان برنامه‌نویسی MiniC - همان C ولی کوچولوش!

زبان برنامه نویسی MiniC در واقع همان زبان برنامه نویسی C است، اما نسخه‌ای ساده‌تر و سبک‌شده از آن محسوب می‌شود. این زبان ساختار و قواعد اصلی C را حفظ کرده اما برخی امکانات پیچیده‌تر مانند هدر فایل‌ها (#includeساختارها (struct) و اشاره‌گرها (Pointers) را ندارد. هدف از طراحی MiniC ساده‌سازی زبان و تمرکز بر مفاهیم پایه‌ای برنامه‌نویسی است تا یادگیری و همچنین پیاده‌سازی مفسر یا کامپایلر برای آن راحت‌تر باشد.

در MiniC تنها عناصر اصلی یک زبان برنامه‌نویسی مانند انواع دادهٔ ابتدایی (int، float، char)، عملگرهای ریاضی و منطقی، دستورات شرطی (if، else) و حلقه‌ها (while، for) پشتیبانی می‌شوند. برای ورودی و خروجی نیز به جای توابع کتابخانه‌ای، معمولاً از دستورهای ساده‌تری مانند print یا read استفاده می‌شود. همین موضوع باعث می‌شود برنامه‌ها کوتاه‌تر و شفاف‌تر نوشته شوند و پیچیدگی‌های غیرضروری حذف گردند.

کاربرد اصلی MiniC بیشتر در حوزه‌های آموزشی و تحقیقاتی مانند درس‌های طراحی زبان‌ها و کامپایلر است. با حذف ویژگی‌های پیشرفته، این زبان بستری فراهم می‌کند تا دانشجو یا برنامه‌نویس بتواند بر اصول بنیادی مانند تعریف متغیر، کنترل جریان برنامه و ساختار توابع تمرکز کند. در نتیجه می‌توان گفت MiniC نسخه‌ای آموزشی و ساده‌شده از C است که تنها بخش‌های ضروری و بنیادین آن را در بر می‌گیرد. در این سوال نیز تضمین خواهد شد که کد‌هایی که به عنوان ورودی به مبهم‌ساز شما داده می‌شود از نوع MiniC هستند و فاقد پیچیدگی‌های معمول زبان برنامه نویسی C می‌باشند. به مثال‌های زیر از این زبان کوچولو، توجه کنید:

int sum(int n) {
    int s;
    s = 0;
    int i;
    i = 1;
    while (i <= n) {
        s = s + i;
        i = i + 1;
    }
    return s;
}

void main() {
    int result;
    result = sum(5);
    print(result);
}
int fib(int n) {
    if (n <= 1) {
        return n;
    } else {
        return fib(n - 1) + fib(n - 2);
    }
}

void main() {
    int i;
    for (i = 0; i < 6; i = i + 1) {
        print(fib(i));
    }
}

void main() {
    int n;
    read(n);
    if (n % 2 == 0) {
        print(0);
    } else {
        print(1);
    }
}

معرفی آبگوشت پارسر abgoosh_parser

ساختار کلی پروژه abgoosht_parser

پروژه‌ی abgoosht_parser در واقع یک چارچوب ساده و قابل توسعه برای مبهم‌سازی و بازنویسی برنامه‌های MiniC است. این چارچوب، شامل اجزایی برای تجزیه کد (Parsing)، تبدیل (AST Transforming) و تولید مجدد کد (Code Generation) می‌باشد. ساختار پوشه‌بندی پروژه به‌گونه‌ای است که شفافیت ماژول‌ها حفظ شده و شما به راحتی می‌تواند اجزای مختلف را گسترش دهید.

در هسته‌ی این سیستم، ابزارهایی مانند c_parser.py و c_ast.py قرار دارند که مسئول ایجاد AST از کد MiniC و تعریف ساختار آن هستند. درخت نحوی انتزاعی (AST) در این پروژه به‌شکل مجموعه‌ای از کلاس‌های پایتونی تعریف شده که بازتاب دقیق ساختارهای نحوی زبان MiniC است. فایل c_parser.py، تجزیه‌گر اصلی است که با استفاده از ابزارهای lex و yacc است. این فایل‌ها با کمک c_lexer.py (تحلیل‌گر واژگانی) به ورودی زبان MiniC معنا می‌بخشند و با استفاده از c_parser.py آن را به درختی نحوی انتزاعی‌اش تبدیل می‌کنند. ساختار کلی abgoosht_parser به شکل زیر است:

abgoosht_parser
├── __init__.py
├── _ast_gen.py
├── _build_tables.py
├── _c_ast.cfg
├── ast_transforms.py
├── c_ast.py
├── c_generator.py
├── c_lexer.py
├── c_parser.py
├── ply
└── plyparser.py

فایل‌های c_lexer.py و c_parser.py هسته‌ی اصلی تحلیل زبانی را تشکیل می‌دهند؛ اولی وظیفه‌ی شناسایی واژه‌ها (کلیدواژه‌ها، عملگرها، شناسه‌ها و...) را دارد و دومی با تکیه بر گرامر زبان MiniC ساختار نحوی برنامه را ایجاد می‌کند. درخت‌های نحوی (AST) در قالب کلاس‌هایی که در c_ast.py تعریف شده‌اند نمایش داده می‌شوند. این کلاس‌ها خودکار از روی فایل پیکربندی _c_ast.cfg توسط اسکریپت _ast_gen.py تولید می‌شوند. اگر لازم باشد روی این درخت تغییر یا مبهم‌سازی صورت گیرد، فایل ast_transforms.py ابزارهای لازم را فراهم می‌کند.

در طرف دیگر، فایل c_generator.py وجود دارد که مسیر معکوس را طی می‌کند؛ یعنی AST کد را گرفته و دوباره به کد MiniC خوانا بازتولید می‌کند. ماژول‌هایی مثل _build_tables.py و plyparser.py نقش کمکی دارند و برای مدیریت جداول گرامری و تعامل ساده‌تر استفاده می‌شوند. به این ترتیب، مجموعه‌ی این فایل‌ها زنجیره‌ای کامل می‌سازند: از دریافت کد خام MiniC، تولید AST، اعمال تغییرات احتمالی روی آن و در نهایت بازگردانی یا تولید خروجی دلخواه.

فایل generators.py و تبدیل AST به کد C

یکی از اجزای مهم پروژه، بخش تولید کد یا Generator است. ماژول c_generator.py شامل کلاسی به نام CGenerator است که یک ویزیتور AST محسوب می‌شود. این کلاس تمام رئوس AST را پیمایش می‌کند و برای هر راس، کدی معادل در زبان MiniC تولید می‌کند. کدی که شما در پروژه اولیه دریافت می‌کنید شامل یک کلاس ساده‌تر به نام CustomGenerator است که از CGenerator ارث‌بری می‌کند و در این سوال، این فایل قابل تغییر نخواهد بود.

این بخش از پروژه از دیزاین پترن ویزیتور (Visitor) استفاده می‌کند. این الگو به ما اجازه می‌دهد عملیات مختلف (مثلاً تولید کد، بررسی، تبدیل) را روی کد آن‌ها اعمال کنیم. هر راس در AST با متدی از جنس visit_* هندل می‌شود و این شیوه‌ی به شما این امکان را خواهد داد که با پیاده‌سازی ویزیتور‌های شخصی‌سازی شده، عملیات ابهام‌سازی کد را پیاده‌سازی کند.

from abgoosht_parser.c_generator import CGenerator

class CustomGenerator(CGenerator):
    def __init__(self):
        super().__init__()
        self.indent_level = 0

def generate_code(ast):
    generator = CustomGenerator()
    return generator.visit(ast)

فایل parser.py و تبدیل کد C به AST

در ابتدای هر پردازش، باید کد MiniC تجزیه شود. این مسئولیت بر عهده‌ی c_parser.py است که با استفاده از توابع parse_code و parse_file که ساختار اولیه abgoosht_parser قرار داده شده‌اند، در واقع رابطی ساده برای فراخوانی این تجزیه‌گر فراهم می‌کنند. ورودی آن‌ها کدی متنی به زبان MiniC است و خروجی، AST مربوط به کد به شکل یک درخت AST خواهد بود. تبدیل کد به AST، گام اول هر نوع تحلیل و ابهام‌سازی روی کد ورودی است. این AST تولید شده سپس توسط تکنیک‌های مختلف که جلوتر توسط شما پیاده‌سازی می‌شوند، قابل اصلاح یا بازنویسی است.

from abgoosht_parser import c_parser, c_ast

def parse_code(code):
    parser = c_parser.CParser()
    ast = parser.parse(code)
    return ast

def parse_file(filename):
    with open(filename, 'r') as f:
        code = f.read()
    return parse_code(code)

فایل transformer.py تبدیل ساختار AST

برای دستکاری AST، ابزار Transformer در پروژه اولیه تعریف شده است. این کلاس با دریافت لیستی از تکنیک‌های تبدیل (Transformation Techniques)، آن‌ها را به ترتیب روی AST اعمال می‌کند. هر تکنیک، یک کلاس ویزیتور جدید است که راس‌های خاصی از AST را شناسایی و بازنویسی می‌کند. این طراحی مبتنی بر الگوی طراحی استراتژی (Strategy Pattern) است؛ چراکه هر تکنیک به عنوان یک استراتژی متغییر عمل می‌کند و می‌توان به دلخواه تکنیک‌ها را اضافه، حذف یا جابجا کرد.

کلاس Transformer باعث شده ترکیب و اجرای چندین تکنیک به‌شکل ساده و پایپلاین ممکن شود. به عنوان مثال، می‌توان تکنیکی برای حذف کد مرده، تکنیکی برای تغییر نام متغیرها و تکنیکی برای بازآرایی بلوک‌های شرطی را به ترتیب اجرا کرد و در نهایت AST نهایی را تولید نمود.

class Transformer:
    def __init__(self, techniques=None):
        self.techniques = techniques or []

    def transform(self, ast):
        current_ast = ast
        for technique in self.techniques:
            current_ast = technique.visit(current_ast)
            if current_ast is None:
                raise ValueError(f"Technique {technique.__class__.__name__} returned None")
        return current_ast

def apply_transformations(ast, techniques):
    transformer = Transformer(techniques)
    return transformer.transform(ast)

پیاده‌سازی آبگوشت obfuscator

پیاده‌سازی تکنیک AliasGenerator و فایل alias_generator.py

این تکنیک روی بخش‌هایی از کد که متغیرها در آن تعریف شده‌اند، عمل می‌کند. هر متغیری که تعریف می‌شود، دقیقا بعد از آن یک متغیر جدید با نام اصلی به علاوه پسوند _alias ساخته می‌شود. مثلاً اگر متغیری به نام count باشد، بلافاصله متغیری به نام count_alias تعریف می‌شود. نوع داده این متغیر جدید دقیقاً همان نوع متغیر اصلی است و مقدار اولیه‌اش به صورت مستقیم برابر با همان متغیر اصلی قرار می‌گیرد (مثل یک اشاره‌گر به مقدار همان متغیر).

from abgoosht_parser.c_ast import NodeVisitor

class AliasGenerator(NodeVisitor):
	pass
  • توجه کنید که برای پیاده‌سازی این تکنیک شما صرفا مجاز به تغییر کلاس AliasGenerator از فایل alias_generator.py هستید.

این عملیات باید برای تمام متغیرهای تعریف‌شده در بدنه توابع انجام می‌شود. پس از اجرای این تکنیک، کدی تولید می‌شود که در آن هر متغیر، یک نسخه اضافی با نامی مشابه ولی پسوند _alias دارد که دقیقا همان مقدار و نوع را نگه می‌دارد. این باعث می‌شود تحلیل و درک نحوه استفاده از متغیرها سخت‌تر شود، زیرا چندین نام مختلف برای همان داده وجود دارد، اما عملکرد برنامه کاملاً بدون تغییر باقی می‌ماند. به مثال زیر از این تکنیک ابهام‌سازی توجه کنید:

  • کد MIniC اولیه:
int main(){
    int a = 10;
    int b = 20;
    int c = a + b;
    printf("Hello world\n");
    return 0;
}
  • کد مبهم شده با تکنیک AliasGenerator:
int main(){
    int a = 10;
    int a_alias = a;
    int b = 20;
    int b_alias = b;
    int c = a + b;
    int c_alias = c
    printf("Hello world\n");
    return 0;
}
  • پس از ابهام سازی سه متغیر جدید با نام‌های a_alias,b_alias و c_alias به کد ساخته شده اضافه شده‌اند که مقادیری برابر با متغیر اصلی خود دارند و از یک نوع هستند.

پیاده‌سازی تکنیک ExprComplexifier و فایل expr_complexifier.py

این تکنیک روی عبارات محاسباتی در کد تمرکز دارد و تلاش می‌کند که عبارات ساده را به شکل‌های پیچیده‌تر ولی معادل تبدیل کند. هدف این است که نتیجه نهایی همان باشد ولی ظاهر کد پیچیده‌تر شود و خواندن آن سخت‌تر شود.

در این بخش تنها دو مورد از اعمال ریاضی مد نظر می‌باشند. در مواردی که عملگر جمع (+) در یک عبارت باینری دیده شود، آن عبارت به فرم معادلی تبدیل می‌شود که از عملیات بیت به بیت استفاده می‌کند: مقدار اصلی به صورت (left ^ right) + ((left & right) << 1) جایگزین می‌شود. در واقع عبارت a + b به (a XOR b) + ((a AND b) shifted left by 1) تغییر می‌کند که از نظر محاسباتی معادل جمع معمولی است اما نوشتار آن بسیار پیچیده‌تر است. در مواردی که عمل ضرب (*) بین یک عدد صحیح و مقدار 2 باشد، این عبارت با یک شیفت چپ (<< 1) معادل جایگزین می‌شود. برای مثال، 2 * x یا x * 2 تبدیل به x << 1 خواهد شد.

from abgoosht_parser.c_ast import NodeVisitor

class ExprComplexifier(NodeVisitor):
	pass
  • توجه کنید که برای پیاده‌سازی این تکنیک شما صرفا مجاز به تغییر کلاس ExprComplexifier از فایل expr_complexifier.py هستید.

در نهایت توجه داشته باشید که از این تکنیک درهم‌سازی در این سوال فقط این دو مورد تبدیل مورد نیاز است و اگر عملگرهای دیگری دیده شوند یا شرایط فوق برقرار نباشد، عبارت بدون تغییر باقی می‌ماند. به مثال‌های زیر توجه کنید:

  • کد MIniC اولیه:
int main(){
    int ans = 2 + 5;
    int output = ans * 2;
    return 0;
}
  • کد مبهم شده با تکنیک ExprComplexifier:
int main(){
    int ans = (2 ^ 5) + ((2 & 5) << 1);
    int output = ans << 1;
    return 0;
}
  • همانطور که مشاهده می‌کنید، پس از اعمال این روش ابهام‌سازی، عبارات ریاضی نسبت به کد اولیه پیچیده‌تر شده اما دقیقا از لحاظ مقدار اولیه یکسان می‌باشند.

پیاده‌سازی تکنیک DeadCodeInserter و فایل dead_code.py

این تکنیک درون بلوک‌های کد، بخصوص بخش‌هایی که چند دستور پشت سر هم قرار دارند، کدهایی اضافه می‌کند که هیچ‌گاه اجرا نمی‌شوند. این کدهای اضافه در این سوال، به شکل یک شرط if با شرط همیشه نادرست (if (0)) و یک حلقه for با شرط آغاز و پایانی که هیچ‌گاه وارد حلقه نمی‌شود (for (;0;)) هستند.

from abgoosht_parser.c_ast import NodeVisitor

class DeadCodeInserter(NodeVisitor):
	pass
  • توجه کنید که برای پیاده‌سازی این تکنیک شما صرفا مجاز به تغییر کلاس DeadCodeInserter از فایل dead_code.py هستید.

این دو کد مرده (Dead Code) به شکل زیر خواهند بود:

  • شرط if با مقدار و بدنه 0 که هیچ‌گاه اجرا نخواهد شد و تغییری در ساختار اصلی کد به وجود نخواهد آورد:
if (0)
{
    0;
}
  • حلقه for با بدنه‌ای خالی که هیچ‌گاه اجرا نخواهد شد و این کد مرده نیز تغییری در ساختار اصلی کد به وجود نیاورده اما باعث پیچیده‌تر شدن و ناخواناتر شدن کد می‌شود:
for (; 0;)
{
}

توجه داشته باشید که در تکنیکی که در این سوال پیاده‌سازی می‌کنید، باید دقیقا در ابتدای هر بلاکی که دارای بدنه است (مانند for بالا بدنه‌اش خالی نیست) یک شرط مرده و دقیقا در انتهای بلاک یک شرط حلقه مرده را اضافه کند. به مثال زیر از ابهام‌سازی کد توجه کنید:

  • کد MIniC اولیه:
int main(){
    int ans = 2 + 5;
    int output = ans * 2;
    if(ans > 10){
        ans = 10;
    }
    print(output);
    return 0;
}
  • کد مبهم شده با تکنیک DeadCodeInserter:
int main(){
    if (0)
    {
        0;
    }
    int ans = 2 + 5;
    int output = ans * 2;
    if(ans > 10){
        if (0)
        {
            0;
        }
        ans = 10;
        for (; 0;)
        {
        }
    }
    print(output);
    return 0;
    for (; 0;)
    {
    }
}
  • همانطور که مشاهده می‌گنید، در کد مبهم شده در ابتدای هر بلاک کد یک شرط if مرده و در انتهای هر بلاک کد یک حلقه for مرده درج شده است که خوانایی کد را کاهش اما عملکرد کد را نسبت به کد اولیه بدون تغییر نگه می‌دارد.

پیاده‌سازی تکنیک FunctionSplitter و فایل function_splitter.py

در این تکنیک مبهم‌سازی باید ابتدا، تابع‌های کمی تا حدودی بزرگ (یعنی توابعی که بیش از دو دستور دارند)، به دو بخش تقریباً مساوی تقسیم می‌شوند. نیمه‌ی اول همان بدنه‌ی اصلی تابع باقی می‌ماند و نیمه‌ی دوم در یک تابع جدید با نامی مشتق‌شده از نام تابع اصلی (دقیقا به شکل <نام_تابع>_split_<شمارنده>) قرار می‌گیرد. این تابع کمکی جدید باید تمامی پارامترهای تابع اصلی را به همراه متغیرهایی که در نیمه دوم استفاده شده‌اند اما در نیمه اول تعریف شده‌اند، به عنوان پارامتر دریافت کند.

from abgoosht_parser.c_ast import NodeVisitor

class FunctionSplitter(NodeVisitor):
	pass
  • توجه کنید که برای پیاده‌سازی این تکنیک شما صرفا مجاز به تغییر کلاس FunctionSplitter از فایل function_splitter.py هستید.

توجه داشته باشید که نام تابع کمکی و پارامترهای اضافه باید دقیقاً مطابق این الگو ساخته شوند تا در سیستم داوری قابل تشخیص باشند. پارامترهای اضافه شده باید از نوع و مشخصات همان متغیرهای اصلی کپی می‌شوند، بدون ایجاد تغییرات اضافه‌ای که در سیستم داوری مورد پذیرش قرار نخواهند گرفت. تابع اصلی باید پس از اجرای نیمه اول، با ارسال پارامترهای لازم، تابع کمکی را فراخوانی کند. اگر نوع بازگشتی تابع اصلی غیر void باشد، فراخوانی تابع کمکی باید در یک دستور بازگشت (return) قرار گیرد، در غیر این صورت فقط فراخوانی به تنهایی در بدنه اضافه می‌شود.

نتیجه نهایی، کدی است که ساختار توابع بزرگ را به مجموعه‌ای از توابع کوچک‌تر تقسیم می‌کند که با یکدیگر در تعامل‌اند. این کار باعث می‌شود درک جریان کنترل و ردیابی مقادیر در کد دشوارتر شود، بدون آنکه منطق برنامه تغییر کند. به مثال زیر از این روش ابهام‌سازی توجه کنید:

  • کد MIniC اولیه:
int add_and_print(int a, int b) {
    int sum = a + b;
    int square = sum * sum;
    printf("sum: %d\n", sum);
    printf("square: %d\n", square);
    return sum;
}
  • کد مبهم شده با تکنیک FunctionSplitter:
int add_and_print_split_0(int a, int b, int sum, int square)
{
    printf("sum: %d\n", sum);
    printf("square: %d\n", square);
    return sum;
}

int add_and_print(int a, int b)
{
    int sum = a + b;
    int square = sum * sum;
    return add_and_print_split_0(a, b, sum, square);
}
  • در مثال بالا، تابع اولیه add_and_print به دو تابع تقسیم شده است. تابع اولیه add_and_print که پارامتر‌ها و خروجی‌اش تغییری نکرده است و تابع add_and_print_split_0 که نیمه‌ی دوم کد‌های بدنه‌ی تابع اولیه را داراست. به این صورت، کد اولیه به دو تابع تقسیم شده که تحلیل و خواناییش را کاهش می‌دهد.

پیاده‌سازی تکنیک MisleadingComments و فایل misleading_comments.py

در این تکنیک، در هر بلوک کد که مجموعه‌ای از دستورات را در خود دارد (و بلوکی با بدنه خالی نیست)، دقیقاً یک کامنت گمراه‌کننده از بین کامنت‌های زیر اضافه خواهد شد. متن کامنت از بین قالب‌های ثابت و مشخص انتخاب می‌شود و به شکل زیر خواهند بود:

  • // optimization level={}

  • // todo: refactor this loop

  • // warning: potential overflow at line {}

  • // debug: value of x is unknown

  • // temporary hack, remove later

همانطور که مشاهده می‌کنید، کامنت‌های اول و سوم دارای یک Placeholder می‌باشند، که باید با یک مقدار عددی جایگزین شوند. مقدار عددی این شمارنده در ابتدا از مقدار 0 شروع شده و با مشاهده هر بلاک کد که خالی نباشد، مبهم‌سازی شما باید به ترتیب و به صورت چرخشی هر کدام از کامنت‌ها را در ابتدای آن بلاک کد درج کرده و مقدار شمارنده را یکی افزایش دهد.

from abgoosht_parser.c_ast import NodeVisitor

class MisleadingComments(NodeVisitor):
	pass
  • توجه کنید که برای پیاده‌سازی این تکنیک شما صرفا مجاز به تغییر کلاس MisleadingComments از فایل misleading_comments.py هستید.

این کامنت‌ها به صورت رشته متنی در ابتدای هر بلاک کد به گونه‌ای درج می‌شود که ترتیب اجرای دستورات تغییر نکند. هدف این تکنیک افزودن کامنت‌هایی است که هیچ ارتباط واقعی یا فنی با کد ندارند و صرفاً باعث گمراه کردن خواننده یا تحلیل‌گر کد می‌شوند، بدون اینکه تاثیری روی عملکرد برنامه داشته باشند. به مثال زیر توجه کنید:

  • کد MIniC اولیه:
void compute(int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += i;
    }

    if (sum > 50) {
        printf("Large sum: %d\n", sum);
    } else {
        printf("Small sum: %d\n", sum);
    }

    while (sum > 0) {
        sum--;
    }

    {
        int temp = sum * 2;
        printf("Temp: %d\n", temp);
    }
}

int main() {
    compute(10);

    {
        int a = 5;
        int b = 10;
        if (a < b) {
            printf("a < b\n");
        }
    }

    return 0;
}

  • کد مبهم شده با تکنیک MisleadingComments:
void compute(int n)
{
    // optimization level=5;
    int sum = 0;
    for (int i = 0; i < n; i++)
    {
        // optimization level=0;
        sum += i;
    }

    if (sum > 50)
    {
        // todo: refactor this loop;
        printf("Large sum: %d\n", sum);
    }
    else
    {
        // warning: potential overflow at line 2;
        printf("Small sum: %d\n", sum);
    }
    while (sum > 0)
    {
        // debug: value of x is unknown;
        sum--;
    }

    {
        // temporary hack, remove later;
        int temp = sum * 2;
        printf("Temp: %d\n", temp);
    }
}

int main()
{
    // debug: value of x is unknown;
    compute(10);
    {
        // warning: potential overflow at line 7;
        int a = 5;
        int b = 10;
        if (a < b)
        {
            // todo: refactor this loop;
            printf("a < b\n");
        }
    }
    return 0;
}
  • در مثال بالا، ترتیب چرخشی درج کامنت‌ها و کامنت‌هایی که دارای Placeholder هستند مشخص شده است. توجه داشته باشید که این ترتیب، بر اساس ترتیب پیمایش dfs ای بر روی ساختار درخت AST کد می‌باشد. به همین دلیل است که اولین کامنت بیهوده افزوده شده، کامنت // optimization level=0; می‌باشد.

پیاده‌سازی تکنیک VariableRenamer و فایل rename_vars.py

در این تکنیک، تمامی نام‌های متغیرهای محلی و پارامترهای توابع به‌صورت کامل بازنام‌گذاری می‌شوند به طوری که هر نام یکتا به یک نام جدید با طول دقیقا هشت حرف انگلیسی (حروف بزرگ و کوچک) تبدیل می‌شود. این بازنام‌گذاری ثابت است؛ یعنی هر بار که یک نام مشخص در کد دیده شود، به همان نام جدید اختصاص یافته تغییر می‌یابد و تمامی ارجاعات به آن متغیر در جایگاه‌های مختلف کد نیز اصلاح می‌شود. نکته مهم این است که نام توابع تغییر نمی‌کند و فقط متغیرها و پارامترهای توابع تحت این تبدیل قرار می‌گیرند.

from abgoosht_parser.c_ast import NodeVisitor

class VariableRenamer(NodeVisitor):
	pass
  • توجه کنید که برای پیاده‌سازی این تکنیک شما صرفا مجاز به تغییر کلاس VariableRenamer از فایل rename_vars.py هستید.

نام‌های جدید کاملاً تصادفی ساخته می‌شوند، مثلاً به شکل AbcDefGh، اما در کل برنامه ثابت و یکسان باقی می‌مانند تا رفتار کد حفظ شود و فقط ابهام در نامگذاری متغیرها ایجاد شود. این روش باید به‌گونه‌ای پیاده‌سازی شود که هیچ تغییری در ساختار و عملکرد اصلی برنامه رخ ندهد و تنها باعث گمراهی خواننده کد از طریق تغییر اسامی متغیرها شود.

  • کد MIniC اولیه:
int add(int a, int b) {
    int sum = a + b;
    int square = sum * sum;
    printf("sum=%d, square=%d\n", sum, square);
    return sum;
}

int main() {
    int x = 5;
    int y = 10;
    int result = add(x, y);
    printf("result=%d\n", result);
    return 0;
}
  • کد مبهم شده با تکنیک VariableRenamer:
int add(int hQZrHNIb, int aAblsZPU)
{
    int nZqxayZQ = hQZrHNIb + aAblsZPU;
    int KMgwRWmn = nZqxayZQ * nZqxayZQ;
    printf("sum=%d, square=%d\n", nZqxayZQ, KMgwRWmn);
    return nZqxayZQ;
}

int main()
{
    int iNVTbmjf = 5;
    int rcfEdJgf = 10;
    int yhEbwcjU = add(iNVTbmjf, rcfEdJgf);
    printf("result=%d\n", yhEbwcjU);
    return 0;
}
  • همانطور که در مثال بالا مشاهده می‌کنید، اسامی تمامی متغیر‌ها و پارامتر‌های توابع و همچنین تمام استفاده‌های آن‌ها در کد بازنویسی شده‌اند و به رشته‌هایی با مقادیر تصادفی تبدیل شده‌اند تا خوانایی کد را کاهش دهند.

پیاده‌سازی تکنیک ControlFlowFlattener و فایل control_flow_flattener.py

در این تکنیک مبهم‌سازی، جریان کنترل در تابع اصلی (تابع main) به صورت عادی دنبال نمی‌شود، بلکه به کمک یک متغیر وضعیت (State variable) و یک ساختار switch-case درون یک حلقه‌ی بی‌نهایت بازنویسی می‌شود. در ابتدای بدنه تابع، یک متغیر جدید به نام __cf_state تعریف و مقدار اولیه‌ی آن صفر قرار داده می‌شود. هر دستور در بدنه‌ی اصلی تابع به یک case متناظر تبدیل می‌شود که در آن، ابتدا دستور اصلی اجرا شده و سپس مقدار متغیر __cf_state به شماره‌ی دستور بعدی تغییر می‌کند.

در این ساختار، یک switch روی مقدار __cf_state وجود دارد که مشخص می‌کند کدام بخش از کد باید اجرا شود. این switch داخل یک حلقه‌ی while(1) قرار داده می‌شود تا پس از هر تغییر وضعیت، اجرای دستورات بعدی ادامه پیدا کند. در مواردی که یک دستور از نوع return باشد، دیگر نیازی به تغییر وضعیت نیست و همانجا جریان اجرا خاتمه پیدا می‌کند.

from abgoosht_parser.c_ast import NodeVisitor

class ControlFlowFlattener(NodeVisitor):
    pass
  • توجه کنید که برای پیاده‌سازی این تکنیک شما صرفا مجاز به تغییر کلاس ControlFlowFlattener از فایل control_flow_flattener.py هستید.

به این ترتیب، ساختار اصلی کد به جای توالی ساده‌ای از دستورات، به یک ماشین حالت (State Machine) تبدیل می‌شود که دنبال کردن جریان اجرای آن برای انسان و ابزارهای تحلیل ایستا بسیار دشوارتر است. این کار بدون تغییر در منطق برنامه، باعث افزایش سطح ابهام و سخت‌تر شدن درک اجرای واقعی کد می‌شود. به مثال ساده زیر توجه کنید:

  • کد MIniC اولیه:
int main() {
    int a = 5;
    int b = 10;
    return a + b;
}
  • کد مبهم شده با تکنیک ControlFlowFlattener:
int main() {
    int __cf_state = 0;
    while (1) {
        switch(__cf_state) {
            case 0:
                a = 5;
                __cf_state = 1;
                break;
            case 1:
                b = 10;
                __cf_state = 2;
                break;
            case 2:
                return a + b;
        }
    }
}
  • این روش مبهم‌سازی، کد بسیار ساده‌ی اولیه را به ماشین حالتی تبدیل کرده است که دقیقا همان عملکرد را در سطح کد خواهد داشت اما خوانایی و دنبال کردن جریان اجرای کد را بسیار پیچیده کرده است.

پیاده‌سازی تکنیک OpaquePredicate و فایل opaque_predicate.py

در این تکنیک مبهم‌سازی هر دستور ساده داخل بلاک‌های کد (دستوراتی که جزو دستورات if, while, return, break, continue, switch و تعریف متغیر‌ها نیستند) به‌جای اینکه مستقیماً اجرا شود، داخل یک شرطِ همیشه‌صادق 1 == 1 قرار می‌گیرد تا خواندن و تحلیل کد سخت‌تر شود. همهٔ دستورات واجد شرایط (غیر از مواردی که پیش‌تر اشاره شد) همیشه و بدون تغییر اضافه‌تری با یک if احاطه می‌شوند. شرط if مورد استفاده ثابت و ساده است (1 == 1) تا رفتار اجرا هیچ‌گاه تغییر نکند ولی ساختار کد پیچیده‌تر و پر از شاخه‌های بی‌اثر شود.

from abgoosht_parser.c_ast import NodeVisitor

class OpaquePredicate(NodeVisitor):
    pass
  • توجه کنید که برای پیاده‌سازی این تکنیک شما صرفا مجاز به تغییر کلاس OpaquePredicate از فایل opaque_predicate.py هستید.

تبدیل باعث می‌شود بلوک‌های کد با شاخه‌های بی‌اثر و شرط‌های همیشه‌حقیقی احاطه شوند؛ این امر خوانایی و تحلیل ایستا را کاهش می‌دهد بدون اینکه منطق یا جریان دادهٔ برنامه تغییر کند. چنین تغییری برای ابزارهای سادهٔ آنالیز یا بررسی دستی، ردیابی مسیرهای اجرا و یافتن رابطهٔ بین دستورها را دشوارتر می‌کند. به مثال تغییر پیش و پس از اِعمال این تکنیک توجه کنید:

  • کد MIniC اولیه:
int main() {
    int a = 5;
    int b = 10;
    int c = a + b;
    printf("%d\n", c);
    return 0;
}
  • کد مبهم شده با تکنیک OpaquePredicate:
int main() {
    int a = 5;
    int b = 10;
    int c = a + b;
    if (1 == 1) {
        printf("%d\n", c);
    }
    return 0;
}
  • در کد ابهام شده، می‌توان مشاهده کرد که تنها دستور printf توسط یک شرط if همیشه درست ابهام شده و دستورات دیگر به دلیل اینکه تعاریف متغیر‌ها هستند، بی تغییر باقی مانده‌اند.

آن‌چه باید آپلود کنید

  • توجه: سیستم داوری این سوال برای نمره‌دهی، ابتدا با استفاده از مبهم‌ساز شما، کد‌های ورودی در هر تست‌کیس را مبهم کرده و سپس AST کد مبهم‌شده توسط شما را با AST کدی که به درستی از قبل ابهام شده مقایسه کرده و در صورتی که شباهت AST این دو کد، اکیداً بیشتر از ۸۰ درصد باشد، نمره‌ی آن تست‌کیس مشخص به کد ارسالی شما تعلق خواهد گرفت.
  • توجه: سیستم داوری در هر مرحله، کد‌های مبهم‌شده توسط شما را اجرا کرده و از عدم تغییر عملکرد این کد‌ها نسبت به کد‌های ورودی اولیه، اطمینان حاصل می‌کند. در صورتی که هر گونه تغییری در روند اجرای کد‌های مبهم‌شده توسط شما (مثل کامپایل ارور‌ها یا خروجی‌های متفاوت) رخ دهد، حتی در صورت برآورده شدن شرط پیشین و شباهت بیش از ۸۰ درصدی، ارسال شما برای تست‌کیس مشخص شده امتیازی در بر نخواهد داشت و نمره صفر دریافت خواهد کرد.
  • توجه: پس از پیاده‌سازی موارد خواسته شده، کل فایل‌های پروژه را زیپ کرده و ارسال کنید.
  • توجه: شما مجاز به افزودن فایل جدیدی در این ساختار نیستید و تنها باید تغییرات را در فایل‌های موجود اعمال کنید.
  • توجه: که نام فایل Zip اهمیتی ندارد.
ارسال پاسخ برای این سؤال
فایلی انتخاب نشده است.