إخفاء أسماء الدوال عن تحليل نماذج LLM
المشكلة
فعّلت vmObfuscation: true، وشغّلته على ملف يحتوي على دالة مثل validateLicense، ولاحظت أن الناتج المشوَّش لا يزال
يحتوي على النص الحرفي validateLicense: اختفى جسم الدالة وحلّ محله البايت كود، لكن الاسم نفسه باقٍ على مرأى من
الجميع.
ضاعِف ذلك على امتداد قاعدة كود حقيقية وستحصل على قائمة بأسماء دوال مثل validateLicense وdecryptPayload
وprocessPayment وcheckSubscription. لا يحتاج نموذج LLM إلى كسر البايت كود ليفهم ما يفعله البرنامج؛ فالأسماء وحدها
تكفيه لإنتاج ملخّص واثق ودقيق لسلوك الوحدة. البايت كود معتم، أما فهرس المحتويات فليس كذلك.
لماذا يُبقي تشويش VM هذه الأسماء
مع vmTargetFunctionsMode: 'root' (القيمة الافتراضية)، يحوّل المشوِّش جسم كل دالة على مستوى الجذر إلى بايت كود
VM، لكنه يترك الاسم عمدًا دون تغيير. فتصريح الدالة على مستوى الجذر هو من الناحية الدلالية ربطٌ في النطاق المحيط:
في السكربت يعني ذلك الكائن العام، وفي الوحدة يعني نطاق الوحدة
(فالدالة غير المصدَّرة على المستوى الأعلى تكون ضمن نطاق الوحدة لا عامة، لكنها تظل على مستوى الجذر في الملف). ولا يستطيع المشوِّش إعادة تسميته بأمان لأنه لا
يملك وسيلة لمعرفة من يشير إليه أيضًا: حزمة أخرى، أو <script> مضمَّن، أو سمة HTML مثل onclick="validateLicense(...)"،
أو بحث ديناميكي مثل window['validateLicense']، وما إلى ذلك.
إذن فالمقايضة التي يجريها الوضع الافتراضي هي: احمِ التنفيذ، وحافظ على الواجهة العامة. يُبقي ذلك التكامل سليمًا، لكنه
يعني أيضًا أن نموذج LLM يحصل مجانًا على فهرس بكل نقاط الدخول. وعندما يحدث ذلك، تُبلغ نتيجة التشويش عن تحذير
VMGlobalFunctionNamesNotRenamed يسرد الأسماء التي بقيت مقروءة.
لماذا يهم هذا في الهندسة العكسية بمساعدة نماذج LLM
المهاجم البشري الذي يواجه بضع مئات من أسطر توزيع البايت كود سيستسلم عادةً. أما نموذج LLM الذي يُعطى الملف نفسه فلن يكلّف نفسه عناء مهاجمة البايت كود أصلًا: سيقرأ الأسماء، ويقاطعها مع النصوص الحرفية القليلة التي يستطيع رؤيتها، ثم يُخرج شيئًا من قبيل:
"تتحكم هذه الوحدة في ميزة مدفوعة. تتحقق validateLicense من رمز موقَّع، وترفض checkExpiry التراخيص
المنتهية، وتفتح activateFeature واجهة المستخدم بمجرد نجاح الفحص. أما دالة فك التشفير المساعدة فموجودة في
decryptPayload."
هذا الملخّص كافٍ للمهاجم ليخطّط لتجاوز موجَّه دون أن يلمس الآلة الافتراضية إطلاقًا. الأسماء هي موضع التسريب.
الحل: غلّف الكود داخل IIFE
أبسط الطرق وأمتنها لإزالة هذا التسريب هي دفع دوالك الحساسة مستوى واحدًا أعمق في شجرة النطاقات. فالدوال المصرَّح بها داخل دالة أخرى ليست على مستوى الجذر، لذا يستطيع المشوِّش إعادة تسميتها بحرية ودمج تصريحاتها في البايت كود كأي عبارة أخرى.
دالة IIFE (تعبير دالة يُستدعى فورًا) هي أخف طريقة لتحقيق ذلك: فهي تضيف دالة تغليف واحدة تعمل مرة واحدة ولا تكشف أي شيء باسمه.
قبل: الأسماء مكشوفة
بعد تشويش VM، تبقى كلٌّ من validateLicense وcheckExpiry بأسمائها في الناتج.
بعد: الأسماء مخفية خلف IIFE
صار تصريحا الدالتين الآن داخل جسم دالة IIFE. ودالة IIFE نفسها هي البنية الوحيدة على مستوى الجذر، ودالة IIFE المجهولة لا تملك اسمًا يمكن تسريبه. بعد تشويش VM تُعاد تسمية الدوال وتُحوَّل أجسامها إلى بايت كود.
ما الذي تغيّر. لم تعد الدوال متاحة بوصفها متغيرات عامة (وهذا بالضبط ما كان يسرّب أسماءها). ولا يزال بالإمكان
استدعاؤها، لكن من داخل دالة IIFE نفسها فقط. فإن كان شيء ما يحتاج فعلًا إلى استدعاء validateLicense من الخارج، فلم يعد
بإمكانه الوصول إليها؛ راجع نمط الترامبولين أدناه. وإن لم يكن هناك شيء كهذا، فلم تخسر شيئًا.
قد تظل الأسماء المصدَّرة والمعرِّفات المحجوزة والنصوص والسلوك القابل للملاحظة مرئية. اختبر عمليات التكامل بعد التغليف، ولا سيما المتغيرات العامة وصادرات الوحدات والكود الذي يفحص أسماء الدوال.
مقايضة ينبغي معرفتها: إذا احتوى جسم دالة IIFE على استدعاء مباشر للدالة eval، أو على استدعاء new Function(...) / Function(...)
بجسم ديناميكي،
فإن تشويش VM يتخطى تلك الدالة بأكملها وكل ما هو متداخل فيها (مع تحذير VMDynamicCodeSkipped)، لأن المصدر المبني أثناء
التشغيل قد يشير إلى معرِّفات أعاد المشوِّش تسميتها. وعندئذ يعود الكود الذي نقلته للتو إلى داخل دالة IIFE إلى التشويش العادي
ويفقد حماية البايت كود. استخدم eval غير المباشر - (0, eval)(...) - أو راجع
استدعاء eval المباشر مع تشويش VM للاطلاع على الخيارات المتاحة.
ماذا لو كانت دالة ما تحتاج فعلًا إلى أن تكون عامة؟
أحيانًا تكون الدالة فعلًا نقطة دخول عامة: معالج أحداث مضمَّن، أو رد نداء JSONP، أو خطّاف لحزمة SDK من طرف ثالث. أمامك خياران:
اكشف دالة عبور رفيعة، وأبقِ المنطق داخل دالة IIFE. صرّح بغلاف عام صغير مهمته الوحيدة استدعاء التنفيذ الموجود في نطاق دالة IIFE. سيظل اسم دالة العبور مسرَّبًا، لكنه لا يحمل أي معلومة دلالية، فسمِّه
__entry1أو ما شابه، ويبقى كل المنطق ذي المعنى مخفيًا.أعد كتابة موضع الاستدعاء. إذا كان المتغير العام موجودًا فقط لأن معالجًا مضمَّنًا مثل
onclick="validateLicense(...)"يحتاج إليه، فاستبدل المعالج المضمَّن باستدعاءaddEventListenerمن داخل دالة IIFE. يتوقف HTML عن تسمية الدالة، وتتوقف الدالة عن الحاجة إلى أن تكون عامة، ويختفي التسريب تمامًا.
مُهيِّئات المتغيرات على المستوى الأعلى: vmWrapTopLevelInitializers
تصريحات الدوال ليست الشيء الوحيد الذي يوجد في جذر الملف. فمُهيِّئات المتغيرات على المستوى الأعلى، من ثوابت نصية
وكائنات إعداد وجداول بحث، مقروءة بالقدر نفسه في الناتج عندما تبقى JavaScript عاديًا. فسطر مثل
const API_BASE = '/api/v2/license' يخبر نموذج LLM بقدر ما تخبره function validateLicense.
يغلّف الخيار vmWrapTopLevelInitializers (منطقي، القيمة الافتراضية false؛ وتفعّله إعدادات VM المسبقة الحالية)
المُهيِّئات المؤهلة على المستوى الأعلى داخل IIFE، فتُحسب القيمة نفسها ببايت كود VM أثناء التشغيل بدلًا من أن تبقى في
المصدر كقيمة حرفية. وفي وضع الجذر، يُبلَغ عن المُهيِّئات التي تبقى JavaScript عاديًا بتحذير
VMTopLevelInitializerNotVirtualized.
بدون الخيار
مع vmWrapTopLevelInitializers: true
يبقى اسم الربط (MY_STRING) على مستوى الجذر للسبب نفسه الذي تبقى من أجله أسماء الدوال، إذ قد يشير إليه شيء خارج
الملف، لكن القيمة التي يحملها صارت الآن تُنتجها الآلة الافتراضية ولم تعد تظهر كنص مقروء.
لا يكون فعّالًا إلا عندما تكون قيمة vmTargetFunctionsMode هي 'root' (القيمة الافتراضية) ويكون vmAsyncExecutor معطَّلًا. في وضع التعليقات
لا يكون للخيار أي أثر ولا يُبلَّغ عن أي تحذير. وفي وضع الجذر مع vmAsyncExecutor (الذي لا يحوّل إلى الآلة الافتراضية إلا الدوال
غير المتزامنة) لا يكون له أثر كذلك، ويُبلِّغ البناء عن VMTopLevelInitializerNotVirtualized للمُهيِّئات المتروكة بصيغة JavaScript عادية.
إذا لم يحتوِ ملف على أي دالة تحوّلها الآلة الافتراضية، فستُبلغ النتيجة عن VMNoFunctionsToVirtualize؛ فغلّف الكود
الذي تريد حمايته داخل دالة، أو استخدم وضع التعليقات لاختيار الدوال الحساسة صراحةً.
عندما لا يكون هذا كافيًا
- الأسماء المستوردة من وحدات أخرى. إذا كنت تحزم عدة ملفات وكانت إحدى الوحدات تصدّر
validateLicenseلتستوردها وحدة أخرى، فستُبقي أداة الحزم ذلك الاسم مرئيًا في الناتج المحزوم كما تبقى الدوال على مستوى الجذر مرئية. غلّف الحزمة نفسها داخل IIFE (تستطيع معظم أدوات الحزم فعل ذلك)، أو انقل التصدير إلى داخل IIFE وأعد كشفه عبر دالة عبور لا معنى لاسمها. - بيئة التشغيل تظل قابلة للملاحظة. يزيد التشويش الجهد اللازم لفهم الكود وتعديله، لكنه لا يضمن استحالة الهندسة العكسية. أبقِ الأسرار وقرارات الأمان الحاسمة على الخادم.
لا تلجأ إلى renameGlobals أولًا. هذا الخيار موجود، وسيعيد تسمية المعرِّفات على مستوى الجذر، لكنه لا يملك
وسيلة لمعرفة أيٍّ من تلك المعرِّفات يُشار إليه من خارج الملف (حزم أخرى، أو HTML مضمَّن، أو عمليات بحث ديناميكية).
وكثيرًا ما يكسر تفعيله التكامل بطرق خفية. أما التغليف داخل IIFE فهو أكثر أمانًا: فهو لا يعيد تسمية أي شيء عام، بل
يتوقف فقط عن إنشاء متغيرات عامة لم تكن بحاجة إليها.
