Все посты
6 мин чтенияglizzykingdreko

Реконструкция потока входа в X (бывший Twitter). Эпизод 2 — UI Metrics

Это мой второй эпизод про поток входа в X / Twitter и всю необходимую генерацию, требуемую для создания скрипта на основе запросов. Если вы пропустили…

Реконструкция потока входа в X (бывший Twitter). Эпизод 2 — UI Metrics

Это мой второй эпизод про поток входа в X / Twitter и всю необходимую генерацию, требуемую для создания скрипта на основе запросов. Если вы пропустили предыдущий эпизод, обязательно ознакомьтесь с ним

Смотрите к этому эпизоду GitHub-репозиторий с полным разбором и его реализациями на Node/Go/Python

Сегодняшняя цель — правильно понять и решить проверку ui_metrics, загружаемую из скрипта https://twitter.com/i/js_inst?c_name=ui_metrics

Разбираемся

Во время запроса «проверки имени пользователя» (на эндпоинте https://api.x.com/1.1/onboarding/task.json?flow_name=login) в ответе вы получите subtask_id LoginJSInstrumentationSubtask

Это означает, что twitter sdk загрузит js-скрипт с https://twitter.com/i/js_inst?c_name=ui_metrics

Динамический JS-скрипт, который вернёт длинный динамический обфусцированный JS-скрипт, выглядящий так

Заглянув в этот беспорядок, мы уже можем выделить несколько ключевых моментов:

  • Используемые переменные из 6 hex-символов (подход в духе SHA256)
  • Возвращаемый объект с rf (вычисленные значения) и s (статическая подпись/токен)
  • Скрипт создаёт скрытое поле ввода с именем ui_metrics с JSON-результатом

Основная функция вычислений выполняет побитовые операции над 4 числовыми переменными и затем возвращает финальный результат. Но прежде чем идти дальше, нам нужно понять 2 «сложных» паттерна обфускации.

1. Идентификация паттернов обфускации

Prototype XOR

Довольно чистый паттерн, использующий цепочку прототипов JavaScript для выполнения XOR-операций «обфусцированным» способом

a6119e2f973a1d...4ca6d3d91310d0927 = function(POgAM, oFHVk, TvMAz) {
    function UkRmJ(yBLdW) {
        this.TWByd = function() {
            return this.mUxbW ^ yBLdW;
        }
    }
    var vjVer = {
        mUxbW: TvMAz
    };
    var aVDMH = new UkRmJ(POgAM);
    aVDMH.mUxbW = oFHVk;
    UkRmJ.prototype = vjVer;
    return aVDMH.TWByd() | (new UkRmJ(oFHVk)).TWByd();
}(arg1, arg2, arg3);

Разберём по шагам:

  1. Создаётся конструктор UkRmJ, хранящий метод XOR
  2. Создаётся объект vjVer с mUxbW, установленным в третий аргумент
  3. Создаётся экземпляр, затем его mUxbW перезаписывается вторым аргументом
  4. Прототип меняется ПОСЛЕ создания первого экземпляра
  5. Первый вызов: this.mUxbW берётся из свойства экземпляра (arg2), XOR с arg1
  6. Второй вызов: this.mUxbW берётся из прототипа (arg3), XOR с arg2

Это легко преобразуется в

arg2 ^ arg1 | arg3 ^ arg2

DOM Tree Calculation

Теперь становится креативно. С использованием настоящего DOM выполняется вычисление

a4952d9090df64c4e...1e4c7a23d4f827 = function(Bcgrg, UZGVA, iKpwN) {
    var aRkCd = document.createElement('div');
    aRkCd.setAttribute('style', 'display:none;');
    document.getElementsByTagName('body')[0].appendChild(aRkCd);

    function QvFOY(jIbtN, YvKjZ) {
        for (var i = 0; i < 8; i++) {
            var RFTrj = document.createElement('div');
            jIbtN.appendChild(RFTrj);
            RFTrj.innerText = YvKjZ;
            if ((YvKjZ & 1) == 0)
                jIbtN = RFTrj;
            YvKjZ = YvKjZ >> 1;
        }
        return jIbtN;
    }

    function bUZeJ(RFTrj, aRkCd, YvKjZ) {
        if (!RFTrj || RFTrj == aRkCd)
            return YvKjZ % 256;
        while (RFTrj.children.length > 0)
            RFTrj.removeChild(RFTrj.lastElementChild);
        return bUZeJ(RFTrj.parentNode, aRkCd, YvKjZ + parseInt(RFTrj.innerText));
    }

    var YvKjZ = bUZeJ(QvFOY(QvFOY(QvFOY(aRkCd, Bcgrg), UZGVA), iKpwN), aRkCd, 0);
    aRkCd.parentNode.removeChild(aRkCd);
    return YvKjZ;
}(val1, val2, val3);

Его логика такова:

  1. Создаёт скрытый div и добавляет его в body
  2. QvFOY строит древовидную структуру на основе двоичного представления входного значения — для каждого из 8 битов создаётся дочерний div и по условию меняется «текущий родитель» в зависимости от того, равен ли бит 0
  3. Это вызывается три раза по цепочке, чтобы построить вложенные древовидные структуры для всех трёх значений
  4. bUZeJ рекурсивно поднимается вверх по дереву, суммируя значения innerText, и возвращает результат по модулю 256
  5. Скрытый div удаляется после вычисления

По сути, это вычисление, «спрятанное» под манипуляцией с DOM, вероятно сделанное для блокировки headless-браузеров.

Date XOR

Есть и третий, более простой паттерн. Он выглядит так

value = value ^ new Date(value * 10000000000).getUTCDate();

Этот просто выполняет XOR переменной с UTC-днём месяца, выведенным из самого значения. Довольно прямолинейно, и это значит, что вывод не зависит от времени, поскольку timestamp выводится из значения, а не из текущей даты.

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

Теперь, когда мы понимаем, что на самом деле делают паттерны обфускации, можно полностью отказаться от браузера и переписать логику на любом языке. Приступим.

Подход простой: разбираем скрипт построчно с помощью regex, определяем, с каким паттерном имеем дело, и выполняем его нативно.

Для Prototype XOR мы уже знаем формулу, поэтому всякий раз, когда обнаруживаем IIFE-функцию, содержащую "prototype", мы извлекаем три аргумента и вычисляем результат напрямую.

Для DOM Tree мы обнаруживаем его по наличию "createElement" в теле функции. Затем, вместо того чтобы реально строить DOM-узлы, мы симулируем дерево простым массивом

def dom_tree_calc(val1, val2, val3):
    nodes = [{'innerText': 0, 'parentIndex': -1}]

    def build_tree(parent_idx, value):
        current_parent_idx = parent_idx
        for _ in range(8):
            new_node_idx = len(nodes)
            nodes.append({'innerText': value, 'parentIndex': current_parent_idx})
            if (value & 1) == 0:
                current_parent_idx = new_node_idx
            value = value >> 1
        return current_parent_idx

    def traverse_and_sum(node_idx, root_idx, total):
        if node_idx == -1 or node_idx == root_idx:
            return js_mod(total, 256)
        node = nodes[node_idx]
        return traverse_and_sum(node['parentIndex'], root_idx, total + node['innerText'])

    d1 = build_tree(0, val1)
    d2 = build_tree(d1, val2)
    d3 = build_tree(d2, val3)
    return traverse_and_sum(d3, 0, 0)

Каждый узел — это просто dict с innerText и parentIndex. build_tree проходит по 8 битам значения и либо вкладывается глубже (чётный бит), либо остаётся на том же уровне (нечётный бит). traverse_and_sum поднимается обратно вверх, суммируя значения.

Надоедливая логика Javascript

Есть одна критически важная вещь, о которой стоит упомянуть. Обратите внимание на вызов js_mod вместо простого % 256. В Python оператор остатка от деления всегда возвращает неотрицательный результат при положительном делителе

# Python
-208 % 256  # returns 48

В то время как в javascript сохраняется знак делимого

// JavaScript
-208 % 256  // returns -208

Решение — небольшой хелпер

def js_mod(a, b):
    if a >= 0:
        return a % b
    return -((-a) % b) if (-a) % b != 0 else 0

3. Солвер

Собирая всё вместе, солвер работает в 4 шага:

  1. Извлечь 4 начальных значения переменных с помощью regex по объявлениям var
  2. Извлечь статическую строку s из оператора return
  3. Пройти по телу функции построчно, определяя тип каждой операции
  4. Вернуть финальные значения переменных в виде объекта rf в паре со строкой s

Итак, определяя тип операции по этой таблице

| Pattern Detected          | Action                                    |
|---------------------------|-------------------------------------------|
| IIFE with `createElement` | `dom_tree_calc(arg1, arg2, arg3)`         |
| IIFE with `prototype`     | `(arg2 ^ arg1) \| (arg3 ^ arg2)`          |
| Assignment with `new Date`| `value ^ getUTCDate(value * 10000000000)` |
| Simple assignment         | Eval bitwise exp (`^`, `&`, `\|`, `~`)    |

Мы можем прямолинейно решить эту проверку с помощью одного лишь regex и арифметики. Получив в итоге выходной dict вроде

{
  "rf": {
    "a426be92eaca1e3378ae3....c72d05ac7757212fc": 22,
    "b068e1e02c6e975194343...bcd335f40257cab": 207,
    "da6a57a0e5a772c8f414e...4f082ec3ef525f25d23d1c2": -195,
    "ae09b38c692fe2ba158f6...8cc7413f58b5fdf886b2ca5c": 20
  },
  "s": "eJKvRt3Dg_63RqBBrX_w9EfBvNuG8b4O..."
}

Который вам нужно будет отправить обратно в twitter

Что дальше?

Мы завершили проверку ui_metrics, обязательно загляните в полный репозиторий на Github, где вы найдёте солверы для Python, NodeJS и Golang. Не забудьте поставить звезду.

Следующий шаг — интеграция всего в полный поток входа вместе с генерацией Castle Token. Обязательно подпишитесь на мою рассылку, чтобы не пропустить его — будет опубликован полноценный скрипт входа на основе запросов.

Связаться со мной

Если статья вам понравилась, подписывайтесь на меня на GitHub и Medium, чтобы получать уведомления, когда я публикую что-то или выкладываю в open source, или купите мне кофе, чтобы поддержать меня.

twitterreverse-engineeringweb-automationxsui-metrics

Пропустите реверс-инжиниринг.

Takion возвращает свежие cookie, заголовки и токены для любой крупной антибот-стены. Один POST, без браузера, первый вызов в течение часа.