
هندسة عكسية لـ VM Datadome الجديد 🔥
يُوثّق هذا المستودع أول إصدار عام للآلة الافتراضية (VM) من DataDome التي تعمل داخل المتصفح، والمستخدمة في تدفق الكابتشا/الصفحة الوسيطة. يغطي هذا التحليل ما يلي:
ملاحظة: هذا المستودع يغطي إصدارًا واحدًا فقط (ثابتًا) من الآلة الافتراضية وهو مخصص لأغراض البحث الأمني والتحليل. لا يتضمن حلولًا ديناميكية أو تطبيقات حلول إنتاجية.
في 14 يناير 2026، بدأت DataDome في تضمين مكون جديد قائم على الآلة الافتراضية في علامة العميل الخاصة بها.
تم استخراج كود الآلة الافتراضية من تحدي الكابتشا في ملف vm.js (متاح في هذا المستودع).
كانت الخطوة الأولى هي إزالة التعتيم عن السكريبت:
عملية التعتيم واضحة ومباشرة: يتم تقييم كل متغير واستبداله بقيمته الفعلية. يتوفر سكريبت لإزالة التعتيم في deobf.js.
تشغيل الكود بعد إزالة التعتيم (out.js) في أدوات المطور (DevTools) يكشف عن المخرجات المتوقعة للآلة الافتراضية:
المخرجات هي كائن JSON يحتوي على رقمين وسلسلة نصية. الآن دعنا نتعمق في تطبيق الآلة الافتراضية الفعلي.
في بداية دالة Q.exports، نرى كيف يتم فك تشفير البايت كود:
B()، وهو مولد أرقام شبه عشوائي)
-> D يحمل البايت كود المُفكك مع بعض "الضوضاء" العشوائية
بالتمرير لأسفل نجد نقطة دخول الآلة الافتراضية: دالة ذات معلمتين A (البايت كود) وQ (قاموس فارغ يستخدم لمعالجة الأخطاء).
الجانب الأكثر إثارة للاهتمام في هذه الآلة الافتراضية هو معماريتها: كل شيء يعيش في مصفوفة واحدة (A). تحتوي هذه المصفوفة على:
هذا التصميم يعكس معمارية الحاسوب الحقيقية مع مناطق ذاكرة متميزة. الخطوة التالية هي تحديد كل إزاحة (offset) لفهم ما يُخزن أين:
var stack_pointer = 4593
var instruction_pointer = 4635
var frame_base_pointer = 4674
var last_result = 4633
var exit_flag = 4656
var current_opcode_handler = 4685
var current_opcode_id = 4675
var stack_offset = 124482
var vm_start = 5258
مع تحديد هذه الإزاحات، تصبح بنية الآلة الافتراضية واضحة.
تبدأ الآلة الافتراضية بمجموعة من دوال المساعدة التي تتعامل مع:
بعد دوال المساعدة، تقوم الآلة الافتراضية بتهيئة القيم الأساسية:
أسفل التهيئة توجد جميع معالجات التعليمات.
الموزع هو الحلقة الرئيسية للآلة الافتراضية والتي تستمر حتى يتم تعيين `exit_flag`:
I تمثل التعليمات الحاليةP هو الإزاحة الفعلية داخل المصفوفة (مع مراعاة التعتيم)current_opcode_handler وتحديث current_opcode_id
إليك مثال أساسي لمعالج تعليمة برمجية:
%= أو ^=)fetch() في النهايةواحدة من أكثر التعليقات البرمجية تعقيدًا هي التي تنشئ الإغلاقات/الدوال:
A[4919] = function () {
var Q = readUint8(); // عدد الوسائط المتوقعة
var B = [];
for (var E = readUint8(), D = 0; D < E; D++) {
var g = readUint8();
var a = A[A[frame_base_pointer] + g];
B.push(a); // التقاط المتغيرات من النطاق الحالي
}
var h = A[instruction_pointer] + 3; // حفظ عنوان جسم الدالة
A[A[stack_pointer]++] = function (E) {
// إعداد إطار مكدس جديد عند الاستدعاء
var e = A[stack_pointer] - E;
while (E < Q) {
A[e + E++] = undefined; // ملء الوسائط المفقودة بـ undefined
}
A[stack_pointer] = e + Q;
for (var D = 0; D < B.length; D++) {
var g = B[D];
A[A[stack_pointer]++] = g; // دفع المتغيرات الملتقطة
}
A[e - 2] = A[frame_base_pointer]; // حفظ مؤشر الإطار القديم
A[e - 1] = A[instruction_pointer]; // حفظ عنوان العودة
A[frame_base_pointer] = e;
A[instruction_pointer] = h; // القفز إلى جسم الدالة
};
fetch();
};
هذه التعليمة البرمجية:
undefinedهذه التعليمة البرمجية تنشئ غلافًا لاستدعاءات الدوال التي تتعامل مع كل من الاستدعاءات العادية واستدعاءات المُنشئ (Constructor):
A[5003] = function () {
var Q = A[--A[stack_pointer]]; // POP الدالة
var B = A[--A[stack_pointer]]; // POP سياق 'this'
function E(e) { // e = عدد الوسائط
var D = A[stack_pointer];
var g = A.slice(D - e, D); // الحصول على الوسائط من المكدس
if (this instanceof E) {
// استدعاء المُنشئ (new E(...))
g.unshift(null);
var h = Function.prototype.bind.apply(Q, g);
A[stack_pointer] -= e;
try {
a = new h();
} catch (A) {
a = A.message;
}
A[A[stack_pointer]++] = a;
} else {
// استدعاء دالة عادية
var t;
try {
t = Q.apply(B, g);
} catch (A) {
t = A.message;
}
A[stack_pointer] -= e + 2;
A[A[stack_pointer]++] = t;
}
}
A[A[stack_pointer]++] = E;
fetch();
};
هذه التعليمة البرمجية:
new) أم استدعاء عاديA[4961] = function () {
var Q = {};
for (var E = readUint16(), e = 0; e < E; e++) {
var D = A[--A[stack_pointer]]; // POP أول
var g = A[--A[stack_pointer]]; // POP ثاني
Q[D] = g;
}
A[A[stack_pointer]++] = Q;
fetch();
};
هذه التعليمة البرمجية تبني كائنات حرفية عن طريق:
object[firstPop] = secondPopتختتم كل تعليمة باستدعاء fetch()، التي تُعد التعليمية التالية:
function fetch() {
var Q = A[instruction_pointer];
var B = A[vm_start + Q];
A[instruction_pointer] = Q + 1;
var E = A[4783 + B];
A[current_opcode_handler] = E;
A[current_opcode_id] = B;
}
هذه الدالة:
current_opcode_handler و current_opcode_idهذا يعكس منطق الموزع، مما يُنشئ دورة جلب-فك-تنفيذ (fetch-decode-execute) النمطية في معماريات الآلات الافتراضية.
لتسهيل التحليل، تم تطوير مفكك ترجمة كدليل على المفهوم (disasm.js) لتحويل البايت كود للآلة الافتراضية إلى لغة تجميع قابلة للقراءة البشرية.
يعمل مفكك الترجمة في مسارين:
يمسح المسار الأول البايت كود لتحديد جميع أهداف القفزات. يشمل ذلك:
JMP_FWD، JMP_BACK)JZ، JNZ_KEEP، JZ_KEEP)يتم تمييز كل عنوان هدف بعلامة (مثل L_0042) لتسهيل تتبع تدفق التحكم.
يحول المسار الثاني كل تعليمة إلى مخرجات شبيهة بلغة التجميع:
000042: fa 00 0a PUSH_IMM 10
000045: 19 00 19 PUSH_REG 25
000048: eb ADD
يتضمن كل سطر:
أحد الجوانب الأكثر تعقيدًا هو فك تشفير القيم الفورية المضمنة في البايت كود. تستخدم الآلة الافتراضية علامات نوعية (type markers) للإشارة إلى كيفية تفسير البايتات التالية:
الأنواع البسيطة (بدون بيانات إضافية):
0x28 → true0x7D → false0x4C → null0x3D → undefinedالأعداد الصحيحة الصغيرة (0-127): مشفرة مع تعيين البت الأعلى
0x85 → 5 (0x80 | 5)السلاسل النصية: مشفرة باستخدام XOR ومنتهية بـ null
0x67، مفتاح XOR يبدأ من 1830x27، مفتاح XOR يبدأ من 46الأنواع الرقمية:
0x6F + 1 بايت0x61 + 2 بايت (big-endian)0x65 + 3 بايت (big-endian)0x54 + 4 بايت (big-endian)0x05 + 8 بايتتشفير XOR للسلاسل النصية واضح ومباشر لكنه يمنع الفحص العرضي:
let str = '';
let xorKey = 183; // مفتاح ابتدائي لسلاسل ASCII
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
str += String.fromCharCode(ch);
}
تتطلب بعض التعليقات البرمجية معالجة مخصصة:
CLOSURE (التعليمة 136): ينشئ دوال/إغلاقات مع متغيرات ملتقطة
التنسيق: CLOSURE locals, capture_count, [capture_indices...], skip_offset
يشير إزاحة التخطي إلى ما بعد جسم الدالة، مما يسمح للآلة الافتراضية بتخطي تعريف الدالة أثناء التنفيذ الخطي.
PUSH_MULTI_IMM (التعليمة 96): يدفع قيمًا متعددة دفعة واحدة
التنسيق: PUSH_MULTI_IMM count, val1, val2, ...
# فك التجميع من ملف
node disasm.js bytecode.txt
; DataDome VM Disassembly
; Bytecode size: 5428 bytes
; VM Constants: VM_START=5258, OPCODE_BASE=4783
000000: 88 01 00 04 CLOSURE locals=1, captures=[0, 4], body=L_0006, end=L_0a3f
L_0006:
000006: fa 67 ... PUSH_IMM "window"
00001f: 2b PUSH_WINDOW
000020: 02 SET
000021: fa 67 ... PUSH_IMM "navigator"
00003a: 19 00 00 PUSH_REG 0
00003d: fa 67 ... PUSH_IMM "navigator"
000056: ee GET
000057: 02 SET
...
هذا التنسيق للمخرجات يجعل من الممكن:
لا زلت جديدًا نسبيًا في عالم الآلات الافتراضية، لذا خذ كل شيء بحذر. تم استخدام الذكاء الاصطناعي للمساعدة في توثيق الكود وكتابة أجزاء من هذا الملف (لأن كتابة الوثائق أمر مؤلم).
هذا لأغراض تعليمية/بحثية أمنية بحتة. لا يتضمن أي حلول أو تجاوزات - فقط توثيق لكيفية عمل الآلة الافتراضية لأنه مثير للاهتمام حقًا.
DataDome: إذا كنت تقرأ هذا، مرحبًا!!! هذا مجرد محاولة مني للحصول على منحة دراسية 🙏. من فضلك لا تقاضني، أنا مفلس جدًا. إذا كان لديك أي مشكلة مع هذا المستودع، فقط أخبرني ويمكننا التحدث عنها :)
آسف للأشخاص الذين ظنوا أنني سأتحدث عن داخل الآلة الافتراضية... هذا لن يحدث
لا تراسلني خاص وتطلب واجهة برمجة تطبيقات DataDome، لن أساعدك