Todos os posts
7 min de leituraglizzykingdreko

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…

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 episódio anterior, não deixe de conferi-lo

Confira o repo do GitHub deste episódio com o estudo completo e suas implementações em Node/Go/Python

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:

  1. Um construtor UkRmJ é criado, armazenando um método de XOR
  2. Um objeto vjVer é criado com mUxbW definido como o terceiro argumento
  3. Uma instância é criada e então seu mUxbW é sobrescrito com o segundo argumento
  4. O prototype é alterado DEPOIS de criar a primeira instância
  5. Primeira chamada: this.mUxbW vem da propriedade da instância (arg2), submetida a XOR com arg1
  6. 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 é:

  1. Cria um div oculto e o anexa ao body
  2. 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
  3. Isso é chamado três vezes em cadeia para construir estruturas de árvore aninhadas para os três valores
  4. bUZeJ percorre recursivamente a árvore de baixo para cima, somando os valores de innerText e retornando o resultado mod 256
  5. 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:

  1. Extrair os 4 valores iniciais das variáveis usando um regex nas declarações var
  2. Extrair a string estática s do return statement
  3. Percorrer o corpo da função linha por linha, detectando cada tipo de operação
  4. 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é.

twitterreverse-engineeringweb-automationxsui-metrics

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.