Reconstruction du flow de login X (anciennement Twitter). Épisode 2 — UI Metrics
Voici mon deuxième épisode sur le flow de login X / Twitter et toute la génération nécessaire pour créer un script basé sur des requêtes. Si vous avez…

Voici mon deuxième épisode sur le flow de login X / Twitter et toute la génération nécessaire pour créer un script basé sur des requêtes. Si vous avez manqué l'épisode précédent, n'hésitez pas à le consulter
Consultez le dépôt GitHub de cet épisode avec l'étude complète et ses implémentations Node/Go/Python
L'objectif d'aujourd'hui est de comprendre et de résoudre correctement le défi ui_metrics chargé depuis le script https://twitter.com/i/js_inst?c_name=ui_metrics

Le comprendre
Lors d'une requête de « username check » (sur l'endpoint https://api.x.com/1.1/onboarding/task.json?flow_name=login), vous obtiendrez en réponse le subtask_id LoginJSInstrumentationSubtask

Cela signifie que le sdk twitter va charger le script js depuis https://twitter.com/i/js_inst?c_name=ui_metrics
Un script JS dynamique qui renverra un long script JS dynamique obfusqué ressemblant à

En jetant un œil à ce fatras, on peut déjà identifier quelques informations clés :
- Des variables utilisant des chaînes hex de 6x caractères (approche de type SHA256)
- Un objet de retour avec rf (valeurs calculées) et s (une signature/token statique)
- Le script crée des champs input cachés nommés ui_metrics avec le résultat JSON
La fonction de calcul principale utilise des opérations bitwise sur 4 variables numériques, puis renvoie le résultat final. Mais avant de continuer, nous devons comprendre 2 patterns d'obfuscation « complexes ».
1. Identifier les patterns d'obfuscation
Prototype XOR
Un pattern plutôt propre qui utilise la chaîne de prototypes JavaScript afin d'effectuer des opérations XOR de manière « obfusquée »
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);
Décomposons-le :
- Un constructeur UkRmJ est créé, qui stocke une méthode XOR
- Un objet vjVer est créé avec mUxbW défini sur le troisième argument
- Une instance est créée, puis son mUxbW est écrasé avec le deuxième argument
- Le prototype est modifié APRÈS la création de la première instance
- Premier appel : this.mUxbW provient de la propriété d'instance (arg2), XORé avec arg1
- Deuxième appel : this.mUxbW provient du prototype (arg3), XORé avec arg2
Cela peut se convertir facilement en
arg2 ^ arg1 | arg3 ^ arg2
Calcul par arbre DOM
Maintenant on devient créatif. En utilisant un vrai DOM, un calcul est effectué
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);
Sa logique est :
- Crée une div cachée et l'ajoute au body
- QvFOY construit une structure d'arbre à partir de la représentation binaire de la valeur d'entrée — pour chacun des 8 bits, il crée une div enfant et change conditionnellement le « parent courant » selon que le bit est à 0
- Ceci est appelé trois fois en chaîne pour construire des structures d'arbre imbriquées pour les trois valeurs
- bUZeJ remonte récursivement l'arbre, en additionnant les valeurs innerText et en renvoyant le résultat modulo 256
- La div cachée est nettoyée après le calcul
Au fond, un calcul « caché » sous forme de manipulation du DOM, probablement conçu pour bloquer les navigateurs headless.
Date XOR
Il existe aussi un troisième pattern, plus simple. Il ressemble à ceci
value = value ^ new Date(value * 10000000000).getUTCDate();
Celui-ci se contente de faire un XOR entre la variable et le jour du mois UTC dérivé de la valeur elle-même. Plutôt direct, et cela signifie que la sortie est indépendante du temps, puisque le timestamp est dérivé de la valeur, et non de la date courante.
2. La déobfuscation
Maintenant que nous comprenons ce que font réellement les patterns d'obfuscation, nous pouvons ignorer complètement le navigateur et réimplémenter la logique dans n'importe quel langage. Continuons.
L'approche est simple : parser le script ligne par ligne à l'aide de regex, détecter le pattern auquel on a affaire, et l'exécuter nativement.
Pour le Prototype XOR, nous connaissons déjà la formule ; donc, chaque fois que nous détectons une IIFE contenant « prototype », nous extrayons les trois arguments et calculons directement le résultat.
Pour l'arbre DOM, nous le détectons en cherchant « createElement » dans le corps de la fonction. Ensuite, au lieu de réellement construire des nœuds DOM, nous simulons l'arbre avec un simple tableau
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)
Chaque nœud n'est qu'un dict avec innerText et parentIndex. build_tree parcourt les 8 bits de la valeur et soit s'imbrique plus profondément (bit pair), soit reste au même niveau (bit impair). traverse_and_sum remonte en additionnant les valeurs.
La logique JavaScript agaçante
Il y a une chose critique qui mérite d'être mentionnée. Notez l'appel à js_mod au lieu d'un simple % 256. En Python, l'opérateur modulo renvoie toujours un résultat non négatif pour un diviseur positif
# Python
-208 % 256 # returns 48
Tandis qu'en javascript, le signe du dividende est préservé
// JavaScript
-208 % 256 // returns -208
Le correctif est un petit helper
def js_mod(a, b):
if a >= 0:
return a % b
return -((-a) % b) if (-a) % b != 0 else 0
3. Le solveur
En rassemblant le tout, le solveur fonctionne en 4 étapes :
- Extraire les valeurs des 4 variables initiales à l'aide d'une regex sur les déclarations var
- Extraire la chaîne statique s depuis l'instruction return
- Parcourir le corps de la fonction ligne par ligne, en détectant le type de chaque opération
- Renvoyer les valeurs finales des variables sous forme d'objet rf, couplées à la chaîne s
Ainsi, en comprenant le type d'opération via cette table
| 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 (`^`, `&`, `\|`, `~`) |
Nous pouvons résoudre ce défi de manière directe avec juste des regex et de l'arithmétique. En obtenant au final un dict de sortie comme
{
"rf": {
"a426be92eaca1e3378ae3....c72d05ac7757212fc": 22,
"b068e1e02c6e975194343...bcd335f40257cab": 207,
"da6a57a0e5a772c8f414e...4f082ec3ef525f25d23d1c2": -195,
"ae09b38c692fe2ba158f6...8cc7413f58b5fdf886b2ca5c": 20
},
"s": "eJKvRt3Dg_63RqBBrX_w9EfBvNuG8b4O..."
}
Que vous devrez renvoyer à twitter
Et ensuite ?
Nous avons terminé le défi ui_metrics, n'oubliez pas de consulter le dépôt complet sur Github où vous trouverez les solveurs pour Python, NodeJS et Golang. Pensez à lui mettre une étoile.
La prochaine étape consiste à intégrer le tout dans le flow de login complet, aux côtés de la génération du Castle Token. Pensez à vous abonner à ma newsletter pour ne pas le manquer : un script de login complet basé sur des requêtes sera publié.
Me contacter
Si vous avez apprécié cet article, suivez-moi sur GitHub et sur Medium pour recevoir des notifications chaque fois que je publie ou que j'open-source quelque chose, ou offrez-moi un café pour me garder en forme.
Sautez l'étape de la rétro-ingénierie.
Takion renvoie des cookies, en-têtes et tokens frais pour tous les grands murs antibot. Un seul POST, aucun navigateur, premier appel dans l'heure.