
Обратная разработка новой виртуальной машины Datadome 🔥
Этот репозиторий содержит первую публичную версию анализа встроенной в браузер JavaScript виртуальной машины (VM) DataDome, используемой в их CAPTCHA/промежуточном потоке. Анализ охватывает:
Примечание: Этот репозиторий охватывает только одну (статическую) версию ВМ и предназначен для целей исследования безопасности и анализа. Он не включает динамические решатели или реализации промышленных решателей.
14 января 2026 года DataDome начала внедрять новый компонент на основе ВМ в свой клиентский тег.
Код ВМ был извлечён из задачи капчи в vm.js (доступен в этом репозитории).
Первым шагом была деобфускация скрипта:
Обфускация проста: оценить каждую переменную и заменить её фактическим значением. Скрипт деобфускации доступен в deobf.js.
Запуск деобфусцированного кода (out.js) в DevTools показывает ожидаемый вывод ВМ:
Вывод — это JSON-объект, содержащий два числа и строку. Теперь давайте углубимся в реализацию самой ВМ.
В начале функции Q.exports видно, как декодируется байткод:
B(), генератора псевдослучайных чисел)
-> D содержит декодированный байткод с некоторым случайным «шумом»
Прокручивая вниз, можно найти точку входа ВМ: функция с двумя параметрами A (байткод) и Q (пустой словарь для обработки ошибок).
Наиболее интересный аспект этой ВМ — её архитектура: всё хранится в одном массиве (A). Этот массив содержит:
Такая конструкция отражает реальную архитектуру компьютера с отдельными областями памяти. Следующий шаг — нанести на карту каждое смещение, чтобы понять, что где хранится:
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Этот опкод создаёт обёртку для вызовов функций, которая обрабатывает как обычные вызовы, так и вызовы конструктора:
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) или обычным