
مجموعة أدوات الهندسة العكسية لـ PerimeterX's bytecode VM، تشتمل على CFG-based disassembler وخط فك تشفير من 5 طبقات وإعادة بناء جدول opcode ومنظف stack emulation cleaner لأبحاث الأمان حول بصمة كشف البوتات.
يوثق هذا المستودع الهندسة العكسية لـ auditor.js من PerimeterX، وهي آلة افتراضية تعمل بالبايت كود تُستخدم كطبقة ثانية لأخذ البصمات في خط كشف البوتات الخاص بـ PX. يغطي هذا التحليل ما يلي:
ملاحظة: يغطي هذا المستودع نسخة واحدة (ثابتة) فقط من الآلة الافتراضية وهو مخصص لأغراض البحث الأمني والتحليل. لا يتضمن حلولاً ديناميكية أو تطبيقات حلول إنتاجية.
يوم الخميس 2 أبريل 2026، نشرت PerimeterX آلة افتراضية جديدة بالبايت كود كجزء من خط كشف البوتات الخاص بها.
لا يبدو auditor.js مثل سكريبت مستشعر PX العادي. بدلاً من عمليات البحث عن الخصائص المبهمة ووظائف الجمع المعتادة:
_fg0 إلى _fg7)، برنامج الآلة الافتراضية المشفر مقسم عبر المتغيرات_dp) بمفتاح خاص بالموقع (_pk) تقوم بفك ضغط برنامج JSON_0x8df7) تقوم بتجزئة مصدر الآلة الافتراضية لاشتقاق مفتاح فك التشفير، لذا أي تعديل يكسر فك تشفير البايت كود بصمتبرنامج الآلة الافتراضية مقسم عبر 8 متغيرات، يتم دمجها ثم فك تشفيرها عبر _dp() باستخدام تشفير XOR خاص بالموقع مفتاحه _pk:
var _pk = 893686289;
function _dp(_b) {
var _r = atob(_b), _o = new Array(_r.length);
for (var _i = 0; _i < _r.length; _i++) {
_o[_i] = String.fromCharCode(
_r.charCodeAt(_i) ^ (((_pk >>> (8 * (_i % 4))) ^ Math.imul(_i + 1, 0x6B8B4567)) & 0xFF)
);
}
return _o.join("");
}
النتيجة هي كائن JSON بأسماء مفاتيح مبهمة مكونة من حرفين (مثل "uo" للبذرة، "dk" للـ nonce). جدول تعيين يحولها إلى أسماء قياسية.
node extractor.js
# -> program.json
| الحقل | الوصف |
|---|---|
s | البذرة (12755)، تدفع جميع العمليات التشفيرية |
n | الـ nonce (1603730985)، عشوائية خاصة بالبرنامج |
g | علامة المولد، تفعيل طبقة فك تشفير تجزئة التكامل |
x | علامة التشفير، يتم تشفير الثوابت بـ XOR |
c | مجمع الثوابت، 1230 مدخلاً |
f | الدوال، 112 مدخلاً مع بايت كود مشفر |
e | نقطة الدخول، فهرس الدالة 0 |
جميع سلاسل الثوابت الـ 1095 مشفرة بطبقتين:
الطبقة 1: تشفير XOR ثابت باستخدام Murmur بمفتاح 4008000571، معتمد على الموضع.
الطبقة 2: تشفير XOR بتدفق PRNG باستخدام LCG من glibc، مبذر بدمج بذرة البرنامج مع فهرس كل ثابت عبر ضرب Knuth التجزئي.
قبل فك التشفير، يتم XOR البذرة مع بصمة البيئة (_0xaf48)، وهي قناع بتات 8 بت يتم حسابه عن طريق فحص واجهات برمجة المتصفح:
| بت | الاختبار | Chrome |
|---|---|---|
| 0 | typeof window.matchMedia === "function" | 1 |
| 1 | وجود document.elementFromPoint | 1 |
| 2 | typeof window.requestAnimationFrame === "function" | 1 |
| 3 | typeof window.getComputedStyle === "function" | 1 |
| 4 | وجود CSS.supports | 1 |
| 5 | وجود navigator.sendBeacon | 1 |
| 6 | وجود document.execCommand | 1 |
| 7 | وجود process.versions.node (Node.js) | 0 |
بالنسبة لـ Chrome: _0xaf48 = 0b01111111 = 127، مما يعطي بذرة فعالة = 12755 ^ 127 = 12716.
هذا يعني أن نفس البرنامج ينتج نتائج فك تشفير مختلفة في بيئات مختلفة. تشغيله في Node.js مقابل Chrome مقابل Firefox يعطي بذورًا مختلفة.
node decrypt_constants.js
# -> program_decrypted.json, constants_table.txt
تخبرنا السلاسل المفكوكة بالضبط بما تلتقطه الآلة الافتراضية كبصمات:
بصمات المتصفح: screenWidth, screenHeight, innerWidth, innerHeight, devicePixelRatio, colorDepth, platform, userAgent, language, timezone, timezoneOffset, forcedColors, highContrast
توقيت الأداء: navigationStart, domComplete, domLoading, fetchStart, requestStart, responseEnd, secureConnectionStart, serverTiming
تشفير RSA: BigInt, modPow, AQAB (65537 في base64), modulusLength, shiftLeft, shiftRight, getRandomValues
تحقيقات DOM/SVG: http://www.w3.org/2000/svg, createElementNS, getBoundingClientRect, getTotalLength, getBBox
أسماء حقول PX: mtr, tst, mst, enc, sbx, fstec, pdc, prb, wvi, wva, pti, dis, los, cv, sc, jd, ads, enve, init
مراجع نقاط النهاية: https://fst-ec.perimeterx.net/?id=
مضاد للمصحح: _CMP_RCX_07;_JNZ_0x0A_EB_CC, CC|CD-04|BREAKPOINT-005
107 أوبكود أساسي تغطي لغة JavaScript بأكملها، بالإضافة إلى ضوضاء مولدة ديناميكيًا:
40 أوبكود عسل هي تطبيقات بديلة لعمليات حسابية/مقارنة باستخدام تعابير متكافئة رياضياً ولكن مختلفة نحويًا. قد تظهر ADD كـ (a^b) + 2*(a&b) أو -((-a)-b) أو a-(-b). يمكن أن يظهر كل أوبكود أساسي بما يصل إلى 3 متغيرات، مولدة حتميًا من البذرة. يمكن أن تظهر تعليمة ADD بسيطة كـ 4 قيم بايت كود مختلفة داخل نفس البرنامج، مما يكسر طرق مطابقة الأنماط.
24 أوبكود حشو مخصصة في التبديل ولكن ليس لها معالجات ولا يتم إصدارها أبداً. توجد لتوسيع مساحة الأوبكود وجعل الخلط أكثر صعوبة في العكس.
16 مجموعة تعليمات فائقة هي أهم ميزة مضادة للتحليل. عندما تحل حلقة التوزيع أوبكودًا ما إلى قائد تعليمة فائقة، يقرأ المعالج بايتًا إضافيًا واحدًا من تيار البايت كود ويوزع إلى معالج فرعي. يمكن أن يكون المعالج الفرعي عملية مختلفة تمامًا:
| القائد يحل كـ | البايت الفرعي | ينفذ فعليًا |
|---|---|---|
FOR_IN_NEXT | 74 | FOR_IN_NEXT |
FOR_IN_NEXT | 100 | MAKE_CLOSURE |
ASSIGN_OP_VAR | 165 | ASSIGN_OP_VAR |
ASSIGN_OP_VAR | 37 | JMP |
GET_VAR_PROP_C | 143 | SET_VAR_POP |
GET_VAR_PROP_C | 23 | JMP_NULLISH |
يتم خلط جدول الأوبكود عبر Fisher-Yates مبذر بالبذرة الفعالة، لذا تختلف قيم البايت كود لكل بناء.
node build_opcodes.js
# -> opcode_table.json, opcode_table.txt
جوهر مجموعة الأدوات. cfg.js ينشئ رسمًا بيانيًا لتدفق التحكم باتباع جميع مسارات التنفيذ من PC=0، فك تشفير كل تعليمة بسياق التشفير الصحيح.
تستخدم PX تعليمات متداخلة عند حدود الكتل. نفس البايتات تفك كمعاملات على مسار تنفيذ وكأوبكود على مسار آخر، حسب سياق تشفير الكتلة. المسح الخطي يفك تشفير كل موضع بايت مرة واحدة ويفتقد المسار البديل. يتبع CFG كلاً من الحواف الساقطة وحواف القفز، ويفك تشفير كل مسار بشكل مستقل.
_0x3ca8): تشفير XOR ثابت باستخدام Murmur على بايتات base64 الخام، بمفتاح 4008000571_0xece1): تشفير XOR خاص بالدالة بطبقتين فرعيتين: مفتاح ثابت معتمد على الموضع + مفتاح تجزئة تكامل الشيفرة_0x427d): تشفير XOR دوار خاص بالكتلة. كل كتلة تشفير (محددة بحدود fn.bl) تحصل على XOR إضافي مشتق من مفتاح الدالة وفهرس الكتلة. الكتلة 0 غير مشفرة عند الوصول الأول؛ الكتل 1+ مشفرة. هذا هو سبب عمل المفكك الخطي للكتلة الأولى ولكنه ينتج هراء للكتل اللاحقة.يطبق CFG جميع الطبقات الخمس بطريقة غير مدمرة (XOR المعامل يحسب أثناء التنفيذ، وليس في المكان) حتى لا تفسد مناطق التعليمات المتداخلة بعضها البعض.
لكل قائد تعليمة فائقة، يقرأ CFG البايت الفرعي، ويبحث عن المعالج الفعلي في super_groups.json، ويفك تشفير المعامل للأوبكود الحقيقي. يتم اتباع أهداف القفز من أوبكودات القفز المدمجة (مثل ما يبدو كـ ASSIGN_OP_VAR ولكنه في الواقع JMP) بشكل صحيح.
node cfg.js # جميع الدوال -> cfg_output/
node cfg.js 79 # دالة واحدة إلى stdout