Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
datadome-vm — Обратная разработка новой виртуальной машины Datadome 🔥 | Kitploit
Инструменты/GitHubGitHub/xkiian/datadome-vm
Динамический анализ (песочница)Обратная инженерияАнализ Бинарных ФайловОбучение и ОбразованиеАнти-БотОбход CAPTCHA
GitHubxkiian/datadome-vm

datadome-vm

Обратная разработка новой виртуальной машины Datadome 🔥

Репозиторий
12519106 месяцев назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Анализ виртуальной машины DataDome

Краткое описание

Этот репозиторий содержит первую публичную версию анализа встроенной в браузер JavaScript виртуальной машины (VM) DataDome, используемой в их CAPTCHA/промежуточном потоке. Анализ охватывает:

  • Механизмы загрузки и декодирования байткода
  • Структуру памяти и архитектуру ВМ
  • Прототип дизассемблера
  • Замечания по анализу потока управления

Примечание: Этот репозиторий охватывает только одну (статическую) версию ВМ и предназначен для целей исследования безопасности и анализа. Он не включает динамические решатели или реализации промышленных решателей.

Предыстория

14 января 2026 года DataDome начала внедрять новый компонент на основе ВМ в свой клиентский тег.

Деобфускация

Код ВМ был извлечён из задачи капчи в vm.js (доступен в этом репозитории).

Первым шагом была деобфускация скрипта:

Обфускация проста: оценить каждую переменную и заменить её фактическим значением. Скрипт деобфускации доступен в deobf.js.

Первоначальный анализ

Диспетчер — это основной цикл ВМ, который выполняется до тех пор, пока не будет установлен `exit_flag`:

Запуск деобфусцированного кода (out.js) в DevTools показывает ожидаемый вывод ВМ:

Вывод — это JSON-объект, содержащий два числа и строку. Теперь давайте углубимся в реализацию самой ВМ.

Декодирование байткода

В начале функции Q.exports видно, как декодируется байткод:

  1. Входная строка декодируется из base64
  2. Создаётся массив длиной 129 263
  3. Каждый индекс проверяется на соответствие определённому диапазону:
    • Если индекс попадает в диапазон, значение декодируется
    • В противном случае возвращается случайное число (с помощью B(), генератора псевдослучайных чисел) -> D содержит декодированный байткод с некоторым случайным «шумом»

Архитектура ВМ

Прокручивая вниз, можно найти точку входа ВМ: функция с двумя параметрами A (байткод) и Q (пустой словарь для обработки ошибок).

Структура памяти

Наиболее интересный аспект этой ВМ — её архитектура: всё хранится в одном массиве (A). Этот массив содержит:

  • Стек
  • Регистры
  • Опкоды
  • Сам байткод
  • Указатель инструкций

Такая конструкция отражает реальную архитектуру компьютера с отдельными областями памяти. Следующий шаг — нанести на карту каждое смещение, чтобы понять, что где хранится:

root@kitploit:~
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

После нанесения этих смещений структура ВМ становится ясной.

Вспомогательные функции

ВМ начинается с набора вспомогательных функций, которые обрабатывают:

  • Чтение типизированных значений из стека
  • Перемещение данных между стеком и «регистрами»

Инициализация ВМ

После вспомогательных функций ВМ инициализирует основные значения:

  • Все указатели (стек, инструкции, базовый кадр)
  • Флаг выхода
  • Регистр последнего результата

Ниже инициализации находятся все обработчики инструкций.

Цикл диспетчера

  • I представляет текущую инструкцию
  • P — фактическое смещение в массиве (с учётом обфускации)
  • Цикл устанавливает текущую инструкцию на current_opcode_handler и обновляет current_opcode_id

Реализация опкодов

Как работают опкоды

Вот базовый пример обработчика опкода:

  1. Извлекает непосредственное значение из байткода
  2. Получает верхнее значение из стека
  3. Выполняет операцию (например, %= или ^=)
  4. Вызывает функцию fetch() в конце

Интересные опкоды

Опкод 4919: Создание функции/замыкания

Один из самых сложных опкодов создаёт замыкания/функции:

root@kitploit:~
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();
};

Этот опкод:

  1. Считывает количество ожидаемых аргументов
  2. Захватывает переменные из текущей области видимости (замыкание)
  3. Создаёт функцию, которая настраивает новый кадр стека с правильными соглашениями вызова
  4. Обрабатывает недостающие аргументы, заполняя их undefined
  5. Сохраняет адрес возврата и указатель кадра для корректного возврата

Опкод 5003: Динамический вызов функции

Этот опкод создаёт обёртку для вызовов функций, которая обрабатывает как обычные вызовы, так и вызовы конструктора:

root@kitploit:~
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();
};

Этот опкод:

  1. Извлекает функцию и контекст из стека
  2. Создаёт обёртку, которую можно вызывать с аргументами
  3. Определяет, является ли вызов конструктором (new) или обычным
  4. Применяет функцию с правильным контекстом и обработкой ошибок
  5. Помещает результат обратно в стек

Опкод 4961: Построение литерала объекта

root@kitploit:~
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();
};

Этот опкод строит литералы объектов, выполняя:

  1. Чтение количества пар свойств из байткода
  2. Извлечение пар из стека (первый pop становится ключом)
  3. Построение объекта: object[firstPop] = secondPop
  4. Помещение полученного объекта в стек

Функция Fetch

Каждая инструкция завершается вызовом fetch(), который подготавливает следующую инструкцию:

root@kitploit:~
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;
}

Эта функция:

  1. Считывает указатель инструкций
  2. Извлекает следующий опкод из байткода
  3. Увеличивает указатель инструкций
  4. Получает обработчик опкода
  5. Обновляет current_opcode_handler и current_opcode_id

Это повторяет логику диспетчера, создавая цикл выборки-декодирования-исполнения, характерный для архитектур ВМ.

Реализация дизассемблера

Чтобы облегчить анализ, был разработан прототип дизассемблера (disasm.js), который преобразует байткод ВМ в читаемый ассемблер.

Подход

Дизассемблер работает в два прохода:

Проход 1: Обнаружение меток

Первый проход сканирует байткод, чтобы определить все целевые точки перехода. Сюда входят:

  • Прямые и обратные переходы (JMP_FWD, JMP_BACK)
  • Условные переходы (JZ, JNZ_KEEP, JZ_KEEP)
  • Границы замыканий (тела функций и их конечные точки)

Каждый адрес цели помечается меткой (например, L_0042), чтобы упростить отслеживание потока управления.

Проход 2: Дизассемблирование

Второй проход преобразует каждую инструкцию в вывод, похожий на ассемблер:

root@kitploit:~
000042:  fa 00 0a           PUSH_IMM 10
000045:  19 00 19           PUSH_REG 25
000048:  eb                 ADD

Каждая строка включает:

  • Адрес: Шестнадцатеричное смещение в байткоде
  • Сырые байты: Фактические байты, составляющие инструкцию (полезно для проверки) и просто выглядит круто
  • Опкод: Мнемоническое имя инструкции
  • Аргументы: Декодированные операнды (номера регистров, непосредственные значения, цели переходов)

Декодирование значений

Одним из наиболее сложных аспектов является декодирование непосредственных значений, встроенных в байткод. ВМ использует маркеры типа, чтобы указать, как интерпретировать следующие байты:

Простые типы (без дополнительных данных):

  • 0x28 → true
  • 0x7D → false
  • 0x4C → null
  • 0x3D → undefined

Маленькие целые числа (0-127): Кодируются с установленным старшим битом

  • 0x85 → 5 (0x80 | 5)

Строки: XOR-кодированные и завершающиеся нулём

  • Строки ASCII: маркер 0x67, начальный ключ XOR 183
  • Строки UTF-8: маркер 0x27, начальный ключ XOR 46

Числовые типы:

  • 8-битное знаковое: 0x6F + 1 байт
  • 16-битное знаковое: 0x61 + 2 байта (big-endian)
  • 24-битное знаковое: 0x65 + 3 байта (big-endian)
  • 32-битное знаковое: 0x54 + 4 байта (big-endian)
  • IEEE 754 двойной точности: 0x05 + 8 байт

XOR-кодирование для строк простое, но предотвращает поверхностный просмотр:

root@kitploit:~
let str = '';
let xorKey = 183;  // Начальный ключ для строк ASCII
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
    str += String.fromCharCode(ch);
}

Специальные опкоды

Некоторые опкоды требуют специальной обработки:

CLOSURE (опкод 136): Создаёт функции/замыкания с захваченными переменными

root@kitploit:~
Формат: CLOSURE locals, capture_count, [capture_indices...], skip_offset

Смещение пропуска указывает на конец тела функции, позволяя ВМ пропустить определение функции при линейном выполнении.

PUSH_MULTI_IMM (опкод 96): Помещает несколько значений сразу

root@kitploit:~
Формат: PUSH_MULTI_IMM count, val1, val2, ...

Использование

root@kitploit:~
# Дизассемблировать из файла
node disasm.js bytecode.txt

Пример вывода

root@kitploit:~
; 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
...

Такой формат вывода позволяет:

  • Проследить поток выполнения, переходя по меткам переходов
  • Определить границы функций с помощью опкодов CLOSURE
  • Увидеть, какие именно значения помещаются и обрабатываются
  • Сверить с фактической реализацией ВМ

Примечания

Я всё ещё довольно новичок в ВМ, так что относитесь ко всему с долей скептицизма. ИИ использовался для документирования кода и написания частей этого README (потому что писать документацию — это боль).

Отказ от ответственности

Это исключительно для образовательных целей / целей исследования безопасности. Никаких решателей или обходов — только документирование того, как работает ВМ, потому что это действительно интересно.

DataDome: если вы это читаете, привет!!! Это просто я пытаюсь получить стипендию 🙏. Пожалуйста, не судите меня, я на мели. Если у вас есть какие-либо проблемы с этим репозиторием, просто дайте мне знать, и мы сможем обсудить это :)

извините тех, кто думал, что я буду рассказывать о внутренностях ВМ... этого не будет

НЕ ПИШИТЕ МНЕ В ЛИЧКУ С ПРОСЬБОЙ ДАТЬ API DATADOME, Я НЕ БУДУ ПОМОГАТЬ

Скачать инструмент