التوثيق
/
وصفات عملية
/

إخفاء أسماء الدوال عن تحليل نماذج LLM

إخفاء أسماء الدوال عن تحليل نماذج LLM

المشكلة

فعّلت vmObfuscation: true، وشغّلته على ملف يحتوي دالة مثل validateLicense، ولاحظت أن الناتج المشوَّش لا يزال يحتوي النص الحرفي validateLicense - فالجسم اختفى واستُبدل ببايت كود، لكن الاسم نفسه ماثل هناك على مرأى من الجميع.

// Input
function validateLicense(token) {
    const decoded = decodeBase64(token);
    return verifySignature(decoded);
}

// Output - name is preserved, body is bytecode
function validateLicense(b) {
    return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
}

اضرب ذلك عبر قاعدة كود حقيقية فتحصل على قائمة بأسماء دوال مثل validateLicense وdecryptPayload وprocessPayment وcheckSubscription. ولا يحتاج نموذج LLM إلى كسر البايت كود ليفهم ما يفعله البرنامج - فالأسماء وحدها تكفيه لإنتاج ملخص واثق ودقيق لسلوك الوحدة. فالبايت كود مبهم؛ أما جدول المحتويات فلا.

لماذا يُبقي تشويش VM هذه الأسماء

مع vmTargetFunctionsMode: 'root' (الافتراضي)، يحوّل المشوِّش جسم كل دالة على المستوى الجذري إلى بايت كود VM لكنه يترك الاسم كما هو عمدًا. فإعلان الدالة على المستوى الجذري هو، دلاليًا، ارتباط بالنطاق المحيط - وهو للسكربت يعني الكائن العام، وللوحدة يعني فضاء أسماء الوحدة. ولا يستطيع المشوِّش إعادة تسميته بأمان لأنه لا سبيل له لمعرفة من يشير إليه غير ذلك: حزمة أخرى، أو <script> مضمَّن، أو خاصية HTML onclick="validateLicense(...)"، أو بحث ديناميكي window['validateLicense']، وغير ذلك.

فالمفاضلة التي يقيمها الافتراضي هي: احمِ التنفيذ، واحفظ السطح العام. وهذا يُبقي التكامل سليمًا، لكنه يعني أيضًا أن نموذج LLM يحصل على فهرس مجاني بكل نقطة دخول.

لماذا يهمّ هذا للهندسة العكسية المدعومة بنماذج LLM

المهاجم البشري الذي يواجه بضع مئات من أسطر توزيع البايت كود سيستسلم عادةً. أما نموذج LLM الذي يُعطى الملف نفسه فلن يكلّف نفسه عناء مهاجمة البايت كود البتة - بل سيقرأ الأسماء، ويربط بينها وبين النصوص الحرفية القليلة التي يستطيع رؤيتها، وينتج شيئًا مثل:

هذا الملخص يكفي مهاجمًا للتخطيط لتجاوز موجَّه دون أن يمسّ الآلة الافتراضية إطلاقًا. فالأسماء هي التسريب.

الحل: غلّف كودك في IIFE

أبسط الطرق وأمتنها لإزالة هذا التسريب هي دفع دوالك الحساسة مستوىً أعمق في شجرة النطاقات. فالدوال المعلَنة داخل دالة أخرى ليست على المستوى الجذري، ولذلك يكون للمشوِّش حرية إعادة تسميتها وضم إعلاناتها في البايت كود مثل أي عبارة أخرى.

وتعبير IIFE (تعبير دالة يُستدعى فور تعريفه) هو أخفّ وسيلة لفعل ذلك - إذ يضيف دالة تغليف واحدة تعمل مرة واحدة ولا تكشف شيئًا بالاسم.

قبل - أسماء مكشوفة

function validateLicense(token) {
    const decoded = decodeBase64(token);
    return verifySignature(decoded);
}

function checkExpiry(license) {
    return Date.now() < license.expiresAt;
}

document.querySelector('#activate').addEventListener('click', () => {
    const token = document.querySelector('#token').value;
    if (validateLicense(token)) {
        unlockUI();
    }
});

بعد تشويش VM، تبقى validateLicense وcheckExpiry بالاسم في الناتج.

بعد - أسماء مخبَّأة خلف IIFE

(function () {
    function validateLicense(token) {
        const decoded = decodeBase64(token);
        return verifySignature(decoded);
    }

    function checkExpiry(license) {
        return Date.now() < license.expiresAt;
    }

    document.querySelector('#activate').addEventListener('click', () => {
        const token = document.querySelector('#token').value;
        if (validateLicense(token)) {
            unlockUI();
        }
    });
})();

الآن يقيم إعلانا الدالتين داخل جسم تعبير IIFE. وتعبير IIFE نفسه هو البنية الوحيدة على المستوى الجذري، وتعبير IIFE مجهول لا اسم له يُسرَّب. وبعد تشويش VM يُحوَّل الجسم بأكمله - بما فيه كل إعلان داخله - إلى بايت كود ويُرمَّز؛ فلا يبقى داخل تعبير IIFE شيء بوصفه نصًا مقروءًا.

ماذا لو احتاجت دالة حقًا أن تكون متغيرًا عامًا؟

أحيانًا تكون الدالة فعلًا نقطة دخول عامة - معالج حدث مضمَّن، أو دالة رد نداء JSONP، أو دالة ربط SDK طرف ثالث. ولديك خياران:

  • اكشف نقطة انطلاق رفيعة، وأبقِ المنطق داخل IIFE. أعلِن مغلِّفًا عامًا صغيرًا مهمته الوحيدة هي الاستدعاء إلى التنفيذ ذي نطاق IIFE. فاسم نقطة الانطلاق لا يزال يُسرَّب، لكنه لا يحمل أي معلومة دلالية - سمِّه __entry1 أو ما يشبهه - ويبقى كل المنطق ذي المعنى مخبَّأً.

    var __entry1;
    (function () {
        function validateLicense(token) { /* … */ }
        __entry1 = validateLicense;
    })();
    // Outside code calls __entry1(token) instead of validateLicense(token).
  • أعِد كتابة موضع الاستدعاء الذي لا تتحكم فيه. إذا كان المتغير العام موجودًا فقط لأن onclick="validateLicense(...)" مضمَّنًا يحتاجه، فاستبدل المعالج المضمَّن بـ addEventListener من داخل IIFE. فيتوقف الـ HTML عن تسمية الدالة، وتتوقف الدالة عن الحاجة إلى أن تكون عامة، ويختفي التسريب تمامًا.

مُهيّئات المتغيرات على المستوى الأعلى: vmWrapTopLevelInitializers

إعلانات الدوال ليست الشيء الوحيد الذي يقيم في جذر الملف. فمُهيّئات المتغيرات على المستوى الأعلى - ثوابت النصوص، وكائنات الإعداد، وجداول البحث - مقروءة بالقدر نفسه في الناتج افتراضيًا. فسطر مثل const API_BASE = '/api/v2/license' يخبر نموذج LLM بقدر ما يخبره به function validateLicense.

يغلّف الخيار vmWrapTopLevelInitializers (منطقي، الافتراضي false) المُهيّئات المؤهَّلة على المستوى الأعلى في IIFE بحيث تُحسَب القيمة نفسها ببايت كود VM أثناء التشغيل بدلًا من أن تقيم في المصدر بوصفها نصًا حرفيًا.

دون الخيار

// Input
const MY_STRING = 'my-string';

// Output - string is visible
const MY_STRING = 'my-string';

مع vmWrapTopLevelInitializers: true

// Input
const MY_STRING = 'my-string';

// Output - initializer is now a VM call, the string lives inside bytecode
const MY_STRING = (() => {
    return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
})();

يبقى اسم الارتباط (MY_STRING) على المستوى الجذري للسبب نفسه الذي تبقى لأجله أسماء الدوال - إذ قد يشير إليه شيء خارج الملف - لكن القيمة التي يحملها تُنتَج الآن بالآلة الافتراضية ولم تعد تظهر بوصفها نصًا مقروءًا.

حين لا يكفي هذا

  • الأسماء المستوردة من وحدات أخرى. إذا حزمت عدة ملفات وصدّرت إحدى الوحدات validateLicense لتستوردها أخرى، فستُبقي أداة التحزيم ذلك الاسم ظاهرًا في الناتج المحزَّم بالطريقة نفسها التي تظهر بها الدوال على المستوى الجذري. غلّف الحزمة نفسها في IIFE (تستطيع معظم أدوات التحزيم فعل ذلك)، أو انقل التصدير داخل IIFE وأعِد كشفه عبر نقطة انطلاق بلا معنى.