Reconstrução do fluxo de login do X (antigo Twitter). Episódio 2 — UI Metrics
Este é o meu segundo episódio sobre o fluxo de login do X / Twitter e toda a geração necessária para criar um script baseado em requests. Se você perdeu o…

Este é o meu segundo episódio sobre o fluxo de login do X / Twitter e toda a geração necessária para criar um script baseado em requests. Se você perdeu o episódio anterior, não deixe de conferi-lo
O objetivo de hoje é entender e resolver corretamente o desafio ui_metrics carregado a partir do script https://twitter.com/i/js_inst?c_name=ui_metrics

Entendendo o desafio
Durante uma requisição de "username check" ( no endpoint https://api.x.com/1.1/onboarding/task.json?flow_name=login ) você vai receber como resposta o subtask_id LoginJSInstrumentationSubtask

Isso significa que o sdk do twitter vai carregar o script js de https://twitter.com/i/js_inst?c_name=ui_metrics
Um script JS dinâmico que vai retornar um script JS ofuscado dinâmico e longo que se parece com

Ao dar uma olhada nessa bagunça, já conseguimos identificar algumas informações-chave:
- Variáveis usam 6 caracteres hex (abordagem estilo SHA256)
- Um objeto de retorno com rf (valores computados) e s (uma assinatura/token estático)
- O script cria um campo de input oculto chamado ui_metrics com o resultado JSON
A função principal de cálculo usa algumas operações bitwise em 4 variáveis numéricas e então retorna o resultado final. Mas antes de prosseguir precisamos entender 2 padrões "complexos" de ofuscação.
1. Identificando os padrões de ofuscação
Prototype XOR
Um padrão bem limpo que usa a cadeia de prototype do Javascript para realizar operações de XOR de uma forma "ofuscada"
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);
Vamos detalhar:
- Um construtor UkRmJ é criado, armazenando um método de XOR
- Um objeto vjVer é criado com mUxbW definido como o terceiro argumento
- Uma instância é criada e então seu mUxbW é sobrescrito com o segundo argumento
- O prototype é alterado DEPOIS de criar a primeira instância
- Primeira chamada: this.mUxbW vem da propriedade da instância (arg2), submetida a XOR com arg1
- Segunda chamada: this.mUxbW vem do prototype (arg3), submetida a XOR com arg2
Isso pode ser facilmente convertido em
arg2 ^ arg1 | arg3 ^ arg2
Cálculo por Árvore DOM
Agora ficamos criativos. Usando um DOM de verdade, um cálculo é feito
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);
A lógica dele é:
- Cria um div oculto e o anexa ao body
- QvFOY constrói uma estrutura de árvore com base na representação binária do valor de entrada - para cada um dos 8 bits, ele cria um div filho e condicionalmente muda o "pai atual" dependendo de o bit ser 0
- Isso é chamado três vezes em cadeia para construir estruturas de árvore aninhadas para os três valores
- bUZeJ percorre recursivamente a árvore de baixo para cima, somando os valores de innerText e retornando o resultado mod 256
- O div oculto é limpo após o cálculo
Basicamente uma computação "escondida" como uma manipulação de DOM, provavelmente feita para bloquear navegadores headless.
Date XOR
Há também um terceiro padrão, mais simples. Ele se parece com isto
value = value ^ new Date(value * 10000000000).getUTCDate();
Esse só faz XOR da variável com o dia do mês em UTC derivado do próprio valor. Bem direto, e significa que a saída é independente do tempo, já que o timestamp é derivado do valor, não da data atual.
2. A Deobfuscation
Agora que entendemos o que os padrões de ofuscação realmente fazem, podemos pular o navegador por completo e reimplementar a lógica em qualquer linguagem. Vamos prosseguir.
A abordagem é simples: fazer o parse do script linha por linha usando regex, detectar com qual padrão estamos lidando e executá-lo nativamente.
Para o Prototype XOR já sabemos a fórmula, então sempre que detectamos uma IIFE de função contendo "prototype", extraímos os três argumentos e computamos o resultado diretamente.
Para a Árvore DOM nós a detectamos procurando por "createElement" no corpo da função. Então, em vez de realmente construir nós DOM, simulamos a árvore com um simples array
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)
Cada nó é apenas um dict com innerText e parentIndex. build_tree percorre os 8 bits do valor e ou aninha mais fundo (bit par) ou permanece no mesmo nível (bit ímpar). traverse_and_sum sobe de volta somando os valores.
A lógica chata do Javascript
Há uma coisa crítica que vale mencionar. Note a chamada js_mod em vez de um simples % 256. Em Python, o operador de módulo sempre retorna um resultado não negativo para um divisor positivo
# Python
-208 % 256 # returns 48
Enquanto em javascript, o sinal do dividendo é preservado
// JavaScript
-208 % 256 // returns -208
A correção é um pequeno helper
def js_mod(a, b):
if a >= 0:
return a % b
return -((-a) % b) if (-a) % b != 0 else 0
3. O solver
Juntando tudo, o solver funciona em 4 passos:
- Extrair os 4 valores iniciais das variáveis usando um regex nas declarações var
- Extrair a string estática s do return statement
- Percorrer o corpo da função linha por linha, detectando cada tipo de operação
- Retornar os valores finais das variáveis como o objeto rf, pareado com a string s
Então, entendendo o tipo de operação por meio desta tabela
| 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 (`^`, `&`, `\|`, `~`) |
Podemos resolver esse desafio de forma direta com apenas regex e aritmética. Tendo no fim um dict de saída como
{
"rf": {
"a426be92eaca1e3378ae3....c72d05ac7757212fc": 22,
"b068e1e02c6e975194343...bcd335f40257cab": 207,
"da6a57a0e5a772c8f414e...4f082ec3ef525f25d23d1c2": -195,
"ae09b38c692fe2ba158f6...8cc7413f58b5fdf886b2ca5c": 20
},
"s": "eJKvRt3Dg_63RqBBrX_w9EfBvNuG8b4O..."
}
Que você vai precisar enviar de volta ao twitter
E o que vem a seguir?
Terminamos o desafio ui_metrics; não deixe de conferir o repo completo no Github onde você pode encontrar os solvers para Python, NodeJS e Golang. Não deixe de dar uma star.
O próximo passo é prosseguir com a integração de tudo no fluxo de login completo, junto com a geração do Castle Token. Não deixe de assinar a minha newsletter para não perdê-lo; um script de login completo baseado em requests será postado.
Conecte-se comigo
Se você gostou deste artigo, me siga no GitHub e no Medium para receber notificações sempre que eu postar ou abrir o código de algo, ou me pague um café para me manter de pé.
Pule a engenharia reversa.
O Takion retorna cookies, headers e tokens novos para cada grande muro antibot. Um POST, sem navegador, primeira chamada em menos de uma hora.