
هندسة عكسية لـ 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();
};