
يُعتّم كود C/C++ عبر تمريرات LLVM: تشفير السلاسل النصية، وتسطيح تدفق التحكم، وإعادة كتابة MBA، ومضاد التحليل لهزيمة الهندسة العكسية.
██╗ ███████╗███████╗████████╗
██║ ██╔════╝██╔════╝╚══██╔══╝
██║ █████╗ █████╗ ██║
██║ ██╔══╝ ██╔══╝ ██║
███████╗███████╗███████╗ ██║
╚══════╝╚══════╝╚══════╝ ╚═╝
██████╗ ██████╗ ███████╗██╗ ██╗███████╗ ██████╗ █████╗ ████████╗ ██████╗ ██████╗
██╔═══██╗██╔══██╗██╔════╝██║ ██║██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗
██║ ██║██████╔╝█████╗ ██║ ██║███████╗██║ ███████║ ██║ ██║ ██║██████╔╝
██║ ██║██╔══██╗██╔══╝ ██║ ██║╚════██║██║ ██╔══██║ ██║ ██║ ██║██╔══██╗
╚██████╔╝██████╔╝██║ ╚██████╔╝███████║╚██████╗██║ ██║ ██║ ╚██████╔╝██║ ██║
╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
أداة تشويش بسيطة جدًا لشفرة C/C++ الخاصة بمعماريتي x64 وx86
تم إنشاء هذا المشروع كفرع (fork) من LLVM، لذا تحتاج إلى الكود المصدري الفعلي لتشويشه. إنها ليست أداة تشويش تعمل على أي ملف تنفيذي عشوائي، بل نسخة معدّلة من المترجم.
إذا كنت تريد الاطّلاع على النتائج، فقد صنعتُ وشوّشتُ برنامجَي crackme بهذه الأداة. يمكنك الحصول عليهما في الإصدارات إلى جانب ثنائي clang المجمَّع مسبقًا.
تم تجميع كل هذا باستخدام علم -O3 وكل لقطات الشاشة مأخوذة من IDA Pro 9.4. كما اختبرته مع Binary Ninja وGhidra، وكانت النتائج إمّا مماثلة أو أسوأ.
يشفّر السلاسل النصية في وقت الترجمة ويُدرج دالة فك تشفير عند كل استخدام للسلسلة المشفّرة. هذا يعطّل تمامًا القدرة على البحث عن أي سلاسل نصية في الملف الثنائي. ولكل سلسلة نصية مفتاح فريد خاص بها مضمّن في دالة فك التشفير، مما يجعل تفريغها وفك تشفيرها أصعب بكثير.
![]() قبل المرحلة |
![]() بعد |
يستبدل العمليات الحسابية بمكافئاتها من MBA. من المستحيل تقريبًا معرفة ما كانت تفعله العملية الأصلية ما لم تمرّرها أولًا عبر أداة إزالة تشويش MBA. بالطبع هذا التشويش ضعيف نوعًا ما لأن MBA هو من أقدم الحيل المعروفة، لذلك توجد العديد من الأدوات للتعامل مع ذلك، مثلًا CoBRA، فهي ستزيل التشويش بنجاح وتحوّله إلى التعبير الأصلي:
./cobra-cli --mba "((x^y) - (((x^y)&0xFF)&0xA) + 10) * ((x^y) - ((x^y)|0xA) + 10) + ((x^y) - (((x^y)&0xFF)|0xFFFFFFF5) - 11) * (~(x^y) - (~(x^y)|0xA) + 10)" --bitwidth 32
10 * (x ^ y)
لهذا السبب صنعتُ مرحلة AAMBA.
![]() قبل المرحلة |
![]() بعد |
يستبدل معاملات العمليات الثنائية بـ ADC(X, 255) - 255 - CF و SBB(X, 255) + 255 + CF. بالطبع الناتج دائمًا يساوي X، لكنه يجعل التعبير معتمدًا على علم الحمل (carry flag). ما لم تتتبّع أداة فك الترجمة حالة CF (وهو ما يكون مستحيلًا أحيانًا) فإنها ستصاب بالارتباك الشديد ولن تتمكن من طيّ هذه التعبيرات. وهي تتناسق بشكل رائع مع مرحلة MBA السابقة لتشويش العمليات الحسابية أكثر. وكما ترى أدناه، أنشأت أداة فك الترجمة بعض المتغيرات الإضافية وتستخدم الكثير من استدعاءات __PAIR64__ و __CFADD__، لذلك يصبح لصق ذلك في أدوات مثل CoBRA أصعب بكثير (وإن لم يكن مستحيلًا)، كما أن إضافة gooMBA الخاصة بـ IDA لا تساعد في تبسيط هذا. أيضًا، تتتبّع أداة فك الترجمة في IDA علم الحمل إلى حدٍ ما، لكن الجمع بين هذا وتشويش تدفق التحكم يجعل التتبع مستحيلًا تقريبًا بدون تنفيذ، لأنك لا تعرف ما هي العملية السابقة، ربما ضبطت CF إلى 1 وربما لا، لذا فإن المراحل اللاحقة تزيد الطين بلة.
يجمع كل الكتل داخل الدالة ويصنع منها آلة حالات ضخمة واحدة، وينشئ جدول قفزات في بداية الدالة ويضع جميع مؤشرات الكتل داخله. ثم بدلًا من قفزة عادية في نهاية كل كتلة، يُوجَّه كل شيء عبر الموزّع (dispatcher) الذي يستخدم قفزات غير مباشرة، وهذه يكاد يكون من المستحيل حلها ساكنًا (statically) بدون أي تنفيذ. كما أنه يخفض السجلات إلى المكدس، لذا إذا قسّمت كتلة إلى نصفين، فستكون جميع المتغيرات من الكتلة السابقة على المكدس، مما يعني وجود الكثير جدًا من المتغيرات في كل تعبير. إذا مرّرت عمليات كتلة واحدة عبر CoBRA فلن تتمكن من إزالة التشويش عنها كثيرًا لوجود متغيرات غير معروفة أكثر من اللازم.
ينشئ مجموعة من الكتل الزائفة التي تحتوي على تعليمات تجميع غير صالحة. هذا يربك أدوات التفكيك بشكل هائل، لأن أداة التفكيك إذا صادفت بايتًا غير صالح تقنيًا ولا يتم تنفيذه أبدًا، فستظل تحاول فهمه. لذا إذا كان البايت غير مكتمل، فستنشئ تعليمة من أي بايتات تصادفها بعده، مستهلكةً إياها في الأساس. وهذا يخلق فقدان تزامن (desync) يدمر كل تعليمة بعده. في الملفات الثنائية لنظام Windows، ستتمكن IDA من التعافي من هذا إلى حدٍ ما، وفي حالات نادرة ستتمكن من إنشاء رسم بياني وفك ترجمة ما يمكنها (وإن كان مكسورًا وغير مكتمل)، بينما في الملفات الثنائية لنظام Linux فإنها تكسر عرض الرسم البياني تمامًا وتعطّل فك الترجمة. بالإضافة إلى ذلك، سيُدرج فحوصات مؤقّت RDTSC، فإذا استغرق الأمر وقتًا طويلًا (مثلًا عند وجود مصحح أخطاء ملحَق) يحدث الانهيار.
![]() Windows |
![]() Linux |
علاوة على ذلك، إذا رأت المرحلة أي تعليمة تبدأ بـ 0xFF، فإنها تُدرج بايتًا واحدًا 0xEB قبلها. سيؤدي هذا إلى إنشاء JMP RIP+1، لذا يبقى تدفق التحكم دون تغيير (يتقدم RIP ببساطة بايتًا واحدًا داخل التعليمة الأصلية)، لكن أدوات التفكيك تفقد التزامن مجددًا.
التعليمات التي تبدأ بـ 0xFF هي غالبًا INC/DEC وJMP/CALL غير مباشرة. للأسف معظم الاستدعاءات والقفزات العادية نسبية (0xE8/0xE9/0xEB) وتبقى غير متأثرة. لكن هذه التقنية مفيدة بشكل خاص مع مرحلة الموزّع لأن كل شيء هناك يستخدم قفزات غير مباشرة. أما بالنسبة للاستدعاءات فليست مفيدة بنفس القدر، فالاستدعاءات الوحيدة المتأثرة بهذا هي غير المباشرة، وتحديدًا الاستدعاءات الافتراضية (virtual calls)، والاستدعاءات عبر مؤشرات الدوال، وبعض استدعاءات المكتبات الخارجية.
يرمي كل المتغيرات المحلية المخزنة على المكدس في الدالة إلى مخزن مكدس مشترك كبير واحد تُحسب له الفهارس (indices) في وقت التشغيل. بهذه الطريقة لا تستطيع أدوات فك الترجمة الربط بين المتغيرات (alias)، مما يجعل الوصول إلى المتغير نفسه مرات متعددة يظهر وكأنه وصول إلى قيم مختلفة. وهذا يتناغم بشكل رائع مع الموزّع لأنه يخفض السجلات إلى المكدس، لذا سيكون هناك الكثير من هذه الخانات في المكدس.
![]() قبل المرحلة |
![]() بعد |
يشوّش تدفق التحكم عبر الاستثناءات. يستبدل جميع الاستدعاءات بمصائد int3. عندما يتم تفعيل المصيدة، ينتقل تدفق التحكم إلى معالج الاستثناء الذي يضبط RIP على الاستدعاء الفعلي. كما يُدرج بايتات غير صالحة مباشرة بعد المصيدة لزيادة فقدان التزامن لدى أداة التفكيك.
![]() قبل المرحلة |
![]() بعد |
بدمج كل المراحل يصبح التحليل الساكن صعبًا جدًا دون بعض الأدوات الإضافية التي تزيل هذا التشويش. حتى لو نجحت بطريقة ما في تحويل كل البايتات غير الصالحة إلى no-op وتمكنت من فك ترجمة هذا إلى شبه كود (pseudocode) أو على الأقل الحصول على عرض رسم بياني، فسيتبقى لك تشويش تدفق التحكم عبر الاستثناءات والموزّع، وحتى إذا اخترقت ذلك، فهناك جبل من عمليات MBA الزائدة والكتل الزائفة ومتغيرات المكدس وتشفير السلاسل النصية التي تشوّش العمليات الفعلية. فيما يلي لقطات شاشة لنقطة الدخول الرئيسية لبرنامج بسيط يحتوي تلك الدالة البسيطة Foo التي تعمل بـ xor والمذكورة سابقًا، في أدوات التفكيك الثلاث الكبرى. وكما ترى، فهي لا تستطيع استخراج الكثير منه.
|
كنت أرغب حقًا في تضمين نوع من نتائج أدوات إزالة التشويش وهي تحاول فهم هذا، لكن للأسف لم أتمكن من العثور على أي منها يعمل ويستخدم التنفيذ الرمزي (symbolic execution) لرؤية تدفق التحكم الفعلي. كل ما تمكنت من العثور عليه إمّا محدود جدًا بحالات استخدام محددة (مثل إزالة تشويش VMProtect تحديدًا)، أو قديم جدًا ومهجور ومكسور (تقريبًا كل إضافة IDA/BN جربتها)، أو أدوات ضخمة تتطلب توجيهًا وإعدادًا يدويًا كثيرًا عبر واجهات برمجة تطبيقات (APIs) لست معتادًا عليها (Angr, Triton, IntelPin). إذا كنت تعرف أي أدوات إزالة تشويش للملفات الثنائية العشوائية قادرة على استخراج أي شيء على الإطلاق، فأخبرني.
بالطبع إدخال كل هذا الهراء في الملف الثنائي سيبطئه بشكل هائل، أكثر من 200 مرة أبطأ على الإعدادات الافتراضية في المتوسط:

لكنه ليس بالسوء الذي يبدو عليه لسببين. 1. حوالي 95% من كلفة الأداء هنا سببها nanomites، لأن المقاطعات (interrupts) بطيئة ببساطة. على الاستثناء أن يغادر إلى النواة (kernel) ثم يعود إلى التطبيق، وهذا يستغرق وقتًا. بدون nanomites يصبح أبطأ بمقدار 7.5x فقط:

لذا أنصح بشدة بأن تحدّد يدويًا الدوال والاستدعاءات التي تريد تشويشها بـ nanomites بدلًا من ضبطه على كل شيء. تشويش كل استدعاء داخل ملف ثنائي أمر غير مجدٍ ومكلف جدًا. والسبب الثاني: في معظم الأحيان لا تهتم حقًا بأداء الأشياء التي تريد إخفاءها. تمتلك أداة التشويش هذه القدرة على التفعيل والتعطيل الانتقائي. لذا يمكنك تعطيلها للأجزاء الحرجة من حيث الأداء في كودك وتفعيلها حيثما تكون مطلوبة فعلًا. لا أحد يهتم سواء استغرق فحص الترخيص 1ms أو 0.001ms، فهو ما يزال غير ملحوظ بالنسبة للإنسان.
للاطلاع على خيارات الإعداد والدليل الكامل راجع الويكي.
يمكنك أيضًا وضع علامة LEET_SKIP على جميع الدوال المتعلقة بمعالج الاستثناء داخل Leet.h، وبهذه الطريقة لن يتم تشويش معالج الاستثناء بواسطة معظم المراحل، مما يخفض كلفة nanomites إلى النصف (على الإعدادات الافتراضية)، لكنه يتركك أيضًا مع معالج استثناء غير مُشوّش، وهو ما أعتقد أنه أسوأ من بطء طفيف.
المتطلبات:
-DLLVM_USE_LINKER=mold من cmake. لكنه سيكون أسرع مع mold)git clone https://github.com/Zydak/LeetObfuscator.git --recursive
cd LeetObfuscator
mkdir build
cd build
cmake ../leet-llvm-project/llvm -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DLLVM_USE_LINKER=mold -DLLVM_USE_SPLIT_DWARF=ON -DLLVM_ENABLE_ASSERTIONS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DLLVM_ENABLE_PROJECTS=clang -DLLVM_TARGETS_TO_BUILD=X86 -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
ninja clang
سيكون المترجم المعدّل داخل build/bin/، فقط استخدمه لتجميع الكود المصدري الذي تريد تشويشه.
كما لا يتعين عليك بناؤه، فهناك ملف ثنائي مُجمَّع مسبقًا في الإصدارات، فقط نزّله وفك ضغطه.
بناء هذا المشروع لنظام Windows غير مدعوم حاليًا. لكن الترجمة المتقاطعة (crosscompiling) بهذا المشروع إلى Windows مدعومة. لذا إذا كنت تريد فعلًا، فيمكنك أخذ النسخة الثنائية الخاصة بـ Linux والترجمة المتقاطعة للتطبيق المُشوّش لنظام Windows من Linux أو WSL.
للدليل الكامل حول كيفية استخدام هذا بالضبط راجع الويكي.
مثال على الاستخدام:
انسخ Leet.h إلى مشروعك، ثم داخل ملف .c/.cpp واحد أدرجه وعرّف LEET_IMPLEMENTATION
لا تعرّف هذا في عدة وحدات (modules)!
#define LEET_IMPLEMENTATION
#include "Leet.h"
ثم فقط قم بتجميع المصدر باستخدام المترجم المبني:
./build/bin/clang++ ./test.cpp -o test -fno-exceptions
![]() Binary Ninja Personal 5.2 |
![]() Ghidra 12.1.2 |