
يُعتّم كود C/C++ عبر تمريرات LLVM: تشفير السلاسل النصية، وتسطيح تدفق التحكم، وإعادة كتابة MBA، ومضاد التحليل لهزيمة الهندسة العكسية.
██╗ ███████╗███████╗████████╗
██║ ██╔════╝██╔════╝╚══██╔══╝
██║ █████╗ █████╗ ██║
██║ ██╔══╝ ██╔══╝ ██║
███████╗███████╗███████╗ ██║
╚══════╝╚══════╝╚══════╝ ╚═╝
██████╗ ██████╗ ███████╗██╗ ██╗███████╗ ██████╗ █████╗ ████████╗ ██████╗ ██████╗
██╔═══██╗██╔══██╗██╔════╝██║ ██║██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗
██║ ██║██████╔╝█████╗ ██║ ██║███████╗██║ ███████║ ██║ ██║ ██║██████╔╝
██║ ██║██╔══██╗██╔══╝ ██║ ██║╚════██║██║ ██╔══██║ ██║ ██║ ██║██╔══██╗
╚██████╔╝██████╔╝██║ ╚██████╔╝███████║╚██████╗██║ ██║ ██║ ╚██████╔╝██║ ██║
╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
A simple obfuscator for C/C++ x64 and x86 code
هذا مصنوع كتفرع من LLVM، لذا تحتاج إلى المصدر الفعلي لتطبيق التشويش. إنه ليس مُشوِّشًا تعسفيًا للملفات التنفيذية. إنه نسخة معدلة من المترجم.
إذا أردت التحقق من النتائج، فقد صنعت وشوّشت اثنين من crackmes باستخدام هذا. يمكنك الحصول عليهما في releases إلى جانب ملف clang الثنائي المُجمَّع مسبقًا.
تم تجميع كل هذا باستخدام علامة -O3 وجميع لقطات الشاشة تأتي من IDA Pro 9.4. اختبرته أيضًا مع Binary Ninja وGhidra، وكانت النتائج إما نفسها أو أسوأ.
يشفّر النصوص في وقت التجميع ويُدرج دالة فك تشفير عند كل استخدام للنص المشفّر. هذا يعطّل تمامًا القدرة على البحث عن أي نصوص في الملف الثنائي. ولكل نص مفتاحه الفريد المُضمَّن في دالة فك التشفير مما يجعل استخراجها وفك تشفيرها أصعب بكثير. كما سيتم تضمين دوال فك التشفير هذه (inlining). (غير مُضمَّنة أدناه فقط للعرض)
![]() قبل التمريرة |
![]() بعد |
يستبدل العمليات الحسابية بمكافئاتها من 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). ما لم يتتبع فاكّك الترجمة (decompiler) حالة CF (وهو أمر مستحيل أحيانًا) فسيصاب بارتباك شديد ولن يتمكن من طيّ هذه التعبيرات. وهو يتناسب بشكل جميل جدًا مع تمريرة MBA السابقة التي تُعتّم الحساب أكثر فأكثر. وكما ترى أدناه، أنشأ فاكّك الترجمة بعض المتغيرات الإضافية ويستخدم الكثير من استدعاءات __PAIR64__ و __CFADD__، لذا يصبح لصق ذلك في أدوات مثل CoBRA أصعب بكثير (لكنه ليس مستحيلًا). كما أن إضافة gooMBA في IDA لا تساعد في تبسيط هذا. كذلك، فإن فاكّك ترجمة IDA يتتبع علم الحمل إلى حدٍّ ما، لكن دمج هذا مع تعتيم تدفق التحكم يجعل التتبع مستحيلًا تقريبًا دون تنفيذ، لأنك لا تعرف ما كانت العملية السابقة، فربما ضبطت CF على 1، وربما لم تفعل، لذا ستضيف التمريرات اللاحقة المزيد فوق هذه التمريرة.
![]() قبل التمريرة |
![]() بعد |
يجمع كل الكتل داخل دالة ويصنع منها آلة حالات عملاقة واحدة، فينشئ جدول قفز في بداية الدالة ويضع فيه جميع مؤشرات الكتل. ثم بدلًا من قفزة عادية في نهاية كل كتلة، يُوجَّه كل شيء عبر الموزِّع (dispatcher) الذي يستخدم قفزات غير مباشرة، وهذه تكاد تكون مستحيلة الحل ثابتًا دون أي تنفيذ. كما يُنزل السجلات إلى المكدس، فإذا قسّمت كتلة إلى نصفين، ستكون جميع المتغيرات من الكتلة السابقة على المكدس، ما يعني وجود عدد كبير جدًا من المتغيرات في كل تعبير. وإذا مرّرت عمليات كتلة واحدة عبر CoBRA فلن يتمكن من إزالة التعتيم عنها كثيرًا لأن عدد المتغيرات المجهولة سيكون كبيرًا جدًا.
ينشئ مجموعة من الكتل الوهمية التي تحتوي على لغة تجميع غير صالحة. وهذا يُربك فاكّات الترجمة (disassemblers) بشكل هائل، لأنه إذا صادف فاكّ الترجمة بايتًا غير صالح تقنيًا لا يُنفَّذ أبدًا، فسيظل يحاول إعطاءه معنى. فإذا كان البايت غير مكتمل، فسيُنشئ تعليمة من أي بايتات تصادف وجودها بعده، مستهلكًا إياها فعليًا. وهذا يُنشئ فقدان تزامن (desync) يُدمّر كل تعليمة بعده. في ثنائيات Windows سيتمكن IDA من التعافي من هذا إلى حدٍّ ما، وفي حالات نادرة سيتمكن من توليد رسم بياني وفك ترجمة ما يستطيع (وإن كان ذلك سيكون معطوبًا وغير مكتمل)، بينما في ثنائيات Linux فإنه يُعطّل عرض الرسم البياني تمامًا ويعطّل فك الترجمة. بالإضافة إلى ذلك، سيُدرج مؤقت RDTSC وبعض فحوصات مكافحة التنقيح الأخرى، فإذا اكتشف مُنقِّحًا (debugger) فسيُحدث انهيارًا.
![]() Windows |
![]() Linux |
علاوة على ذلك، إذا صادف هذا التمرير أي تعليمة تبدأ بـ 0xFF، فإنه يُدرج بايتًا واحدًا 0xEB قبلها. سيؤدي هذا إلى إنشاء JMP RIP+1، لذا يبقى تدفق التحكم دون تغيير (ببساطة يتقدم RIP بايتًا واحدًا داخل التعليمة الأصلية)، لكن أدوات فك التجميع تصبح غير متزامنة مرة أخرى.
التعليمات التي تبدأ بـ 0xFF هي في الغالب INC/DEC وJMP/CALL غير المباشرة. للأسف، معظم الاستدعاءات والقفزات العادية نسبية (0xE8/0xE9/0xEB) وتبقى غير متأثرة. لكن التقنية مفيدة بشكل خاص مع تمرير الموزّع (dispatcher) لأن كل شيء هناك يستخدم قفزات غير مباشرة. إنها ليست مفيدة بنفس القدر للاستدعاءات، فالاستدعاءات الوحيدة المتأثرة بهذا هي غير المباشرة، وعادةً ما تكون الاستدعاءات الافتراضية، والاستدعاءات عبر مؤشرات الدوال، وبعض الاستدعاءات الخارجية/المكتبية.