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

Это мой второй эпизод про поток входа в 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);
Разберём по шагам:
- Создаётся конструктор UkRmJ, хранящий метод XOR
- Создаётся объект vjVer с mUxbW, установленным в третий аргумент
- Создаётся экземпляр, затем его mUxbW перезаписывается вторым аргументом
- Прототип меняется ПОСЛЕ создания первого экземпляра
- Первый вызов: this.mUxbW берётся из свойства экземпляра (arg2), XOR с arg1
- Второй вызов: 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);
Его логика такова:
- Создаёт скрытый div и добавляет его в body
- QvFOY строит древовидную структуру на основе двоичного представления входного значения — для каждого из 8 битов создаётся дочерний div и по условию меняется «текущий родитель» в зависимости от того, равен ли бит 0
- Это вызывается три раза по цепочке, чтобы построить вложенные древовидные структуры для всех трёх значений
- bUZeJ рекурсивно поднимается вверх по дереву, суммируя значения innerText, и возвращает результат по модулю 256
- Скрытый 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 шага:
- Извлечь 4 начальных значения переменных с помощью regex по объявлениям var
- Извлечь статическую строку s из оператора return
- Пройти по телу функции построчно, определяя тип каждой операции
- Вернуть финальные значения переменных в виде объекта 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, или купите мне кофе, чтобы поддержать меня.
Пропустите реверс-инжиниринг.
Takion возвращает свежие cookie, заголовки и токены для любой крупной антибот-стены. Один POST, без браузера, первый вызов в течение часа.