Reconstrução do fluxo de login do X (antigo Twitter). Episódio 1 — Geração de headers
O post de hoje é o primeiro de uma pequena série sobre a automação de login do X / Twitter. Neste episódio vou fazer engenharia reversa de todos os headers…

O post de hoje é o primeiro de uma pequena série sobre a automação de login do X / Twitter. Neste episódio vou fazer engenharia reversa de todos os headers do X / Twitter necessários para o fluxo de login. No fim da série teremos um script completo de login em python, sem depender de navegador.

Confira o repo do GitHub deste episódio com o estudo completo e o seu módulo Python
X-Client-Transaction-Id
Esse token é gerado para cada requisição com base em 3 coisas principais:
- Detalhes da requisição (METHOD e URL)
- Fingerprint baseado em algo carregado (uma meta tag chamada "twitter-site-verification" e um arquivo js "dinâmico")
- Timestamp atual
O código original se parece com isto

Depois de uma limpada, chegamos a este trecho mais legível
// Calculate seconds elapsed since reference timestamp (April 2023)
var secondsSinceEpoch = Math.floor((Date.now() - 1682924400 * 1000) / 1000);
// Convert timestamp to bytes (Float64 → Uint8Array)
var timestampBytes = new Uint8Array(new Float64Array([secondsSinceEpoch]).buffer);
// Get fingerprint from DOM element
var fingerprint = new Uint8Array(atob(
document.querySelectorAll("[name^=tw]")[0].getAttribute("content")
)["split"]("")["map"](function (n) {
return n["charCodeAt"](0);
}));
var fingerprintHash = generateFingerprintHash(fingerprint);
// Random salt for the payload
var randomSalt = [Math.random() * 256];
// Convert fingerprint and timestamp to arrays
var fingerprintArray = Array.from(fingerprint);
var timestampArray = Array.from(timestampBytes);
// Build the hash input: "param2!param1!timestamp" + "obfiowerehiring" + fingerprintHash
var hashInput = [t, r, secondsSinceEpoch].join("!") + "obfiowerehiring" + fingerprintHash;
// Compute SHA-256 hash asynchronously
var hashResult = crypto.subtle.digest("sha-256", new TextEncoder().encode(hashInput));
// Build the final payload by concatenating:
// [randomSalt, fingerprintArray, timestampArray, transformed(slice16(arrayFrom(hashBytes).concat(HW))), 3]
var hashBytes = new Uint8Array(hashResult);
var hashArray = Array.from(hashBytes);
var transformed = hashArray.concat(HW)["slice"](0, Math.pow(2, 4)); // Take first 16 bytes
var payload = randomSalt.concat(fingerprintArray, timestampArray, transformed, [3]);
// Encode as base64 with optional XOR and return
var finalBytes = new Uint8Array(payload);
// base64 encode with optional XOR
MW = function (n, r, t) {
return r ? n ^ t[0] : n;
}
sW = function (n) {
return btoa(Array.from(n)["map"](function (n) {
return String["fromCharCode"](n);
})["join"](""))["replace"](/=/g, "");
}
var encoded = sW(finalBytes.map(MW));
Você pode encontrar o código completo, bem como a parte do generateFingerprintHash (um fingerprint bem básico baseado em animações css), no meu repo do github.
Então, como resolvemos essa?
Bem direto ao resultado: só precisamos recriar o mesmo hash com base na ação que estamos tomando e implementar nele a chave de tempo e de animação; aqui vai um exemplo
def _generate_transaction_id(self, method: str, path: str) -> str:
# Calculate current time offset
time_now = floor((time() * 1000 - 1682924400000) / 1000)
time_now_bytes = [(time_now >> (i * 8)) & 0xFF for i in range(4)]
# Generate hash
hash_val = sha256(
f"{method}!{path}!{time_now}obfiowerehiring{self.animation_key}".encode()
).digest()
# Build final byte array with XOR encoding
random_num = randint(0, 255)
bytes_arr = [*self.key_bytes, *time_now_bytes, *list(hash_val)[:16], 3]
out = bytearray([random_num, *[b ^ random_num for b in bytes_arr]])
return b64encode(out).decode().rstrip("=")
# Example usage
self._generate_transaction_id(
method='POST',
path='/1.1/onboarding/task.json'
)
A parte da animação, bem como o código completo, podem ser encontrados no meu repo.
X-Guest-Token
Gerado a partir de um
POST https://api.x.com/1.1/guest/activate.json
bem simples e direto, podemos pular essa e seguir em frente.
X-Xp-Forwarded-For
Vamos dar uma olhada no código-fonte:

Então, precisamos analisar esse window.XPForwardedForSDK que, à primeira vista, parece ter alguns atributos e há um script WASM envolvido

Os principais atributos que são importantes para nós são:
- init(environment)
Carrega o runtime WASM - getForwardedForStr()
Nos retorna um dict contendo a string importante e o tempo de expiração
Obv, como é praticamente tudo baseado em wasm, precisamos entender o que ele faz.
1. Decompilando o script WASM
Depois de extrair e fazer o parse do script via um trecho simples como
const fs = require('fs');
const code = fs.readFileSync('x_xp_forwarded_for.js', 'utf8');
// Find the WASM bytes array using regex
const match = code.match(
/864585:\s*e\s*=>\s*\{[^}]*var\s+t\s*=\s*new\s+ArrayBuffer\((\d+)\);\s*new\s+Uint8Array\(t\)\.set\(\[([^\]]+)\]/s
);
if (match) {
const size = parseInt(match[1]);
console.log('WASM size:', size, 'bytes');
// Parse the bytes
const allBytes = match[2].split(',').map(b => parseInt(b.trim()));
const buffer = Buffer.from(allBytes);
// Save to file
fs.writeFileSync('xpforwarded.wasm', buffer);
console.log('Saved to xpforwarded.wasm');
}
Podemos prosseguir convertendo o WASM binário para texto legível via a útil ferramenta wasm2wat como
wasm2wat xpforwarded.wasm -o xpforwarded.wat
Agora temos uma visão bem melhor da situação

Já podemos apontar o fato de que é um script baseado em GO, o que torna nosso processo de debug bem mais fácil.
2. Analisando os Data Segments do WAT
Os arquivos WAT contêm data segments com strings embutidas
(data (;0;) (i32.const 65536) "expand 32-byte k...")
(data (;1;) (i32.const 65842) "meta\00invalid syntax0123456789abcefz...")
as principais descobertas que fiz foram:
| String | Interpretation |
|---------------------------------------------|------------------------------|
| `forwarded-for-sdk/javascript_fingerprint` | Go package name |
| `crypto/aes`, `crypto/internal/fips140/aes` | AES encryption used |
| `crypto/internal/fips140/aes/gcm` | GCM mode specifically |
| `crypto/internal/fips140/sha256` | SHA-256 hashing |
| `documentcookie;=guest_id` | Cookie extraction logic |
| `navigatoruserActivationhasBeenActive` | User interaction check |
| `userAgentwebdriverundefined` | Bot detection via webdriver |
| `getForwardedForStr` | Exported JS function name |
| `strexpiryTimeMillis` | Return object properties |
| `json:"navigator_properties"` | JSON field tag |
| `json:"created_at"` | JSON field tag |
Estamos quase prontos; vamos tentar extrair a chave de criptografia usada para a criptografia AES para termos tudo
strings xpforwarded.wasm | grep -E "^[0-9a-f]{64}$"
# that will output
0e6be1f1e21ffc33590b888fd4dc81b19713e570e805d4e5df80a493c9571a05
3. Entendendo o Runtime do Go
Um pouco de teoria é sempre necessário para entender a situação
O Go compila para WASM usando o target GOOS=js GOARCH=wasm. Isso requer:
- Um arquivo JavaScript que implementa os imports do gojs
- O Go gerencia sua própria memória dentro da memória linear do WASM, então um gerenciador é necessário
- O pacote syscall/js provê a comunicação bidirecional
**Sistema de Referência de Valores
**O Go WASM usa um sistema de referência para passar valores entre Go e JavaScript:
// Values are stored in an array, indexed by IDs
this._values = [
NaN, // 0: reserved
0, // 1: zero
null, // 2: null
true, // 3: true
false, // 4: false
globalThis, // 5: global object
this // 6: Go instance
];
Quando o Go chama syscall/js.valueGet(obj, "property"):
- O Go passa o ID de referência do objeto
- O código de glue do JS busca o objeto em
_values - Pega a propriedade
- Armazena o resultado em
_valuese retorna o novo ID
**Transformação Asyncify
**O módulo WASM usa a transformação asyncify do Binaryen para lidar com operações JavaScript assíncronas:
(export "asyncify_start_unwind" (func 436))
(export "asyncify_stop_unwind" (func 437))
(export "asyncify_start_rewind" (func 438))
Isso permite que o código Go dê await em Promises do JavaScript, o que é necessário para:
- crypto.subtle.encrypt()
- crypto.subtle.digest()
4. A metodologia de engenharia reversa
A abordagem que vou usar para recriar esse wasm corretamente é sem usar o arquivo diretamente de forma alguma. Seria fácil demais só fazer um script JS simples rodando o arquivo com o env correto. Na verdade eu quero recriar tudo do zero. Vamos prosseguir.
**Evidência 1: Strings Embutidas
**A técnica mais poderosa para analisar binários GO é extrair e analisar as strings embutidas, como:
- Nomes de pacotes
- Mensagens de erro
- Tags de struct JSON
- Nomes de tipos
Vamos extrair tudo via um único comando
strings xpforwarded.wasm | grep -v "^.$" | sort -u > all_strings.txt
Agora vamos estudá-las
| String Found | What It Tells Us
|----------------------------------------|
| `json:"navigator_properties"` | There's a Go struct with a field that serializes to `navigator_properties`
| `json:"user_agent"` | Nested struct has `user_agent` field
| `json:"has_been_active"` | Boolean field for user activation
| `json:"webdriver"` | Boolean field for bot detection
| `json:"created_at"` | Timestamp field
| `navigatoruserActivationhasBeenActive` | Concatenated JS property access path
| `userAgentwebdriverundefined` | More JS properties being accessed
| `getForwardedForStr` | The exported function name
| `strexpiryTimeMillis` | Return object has `str` and `expiryTimeMillis` keys
A lógica que vamos usar é
json:"navigator_properties" + json:"user_agent" + json:"has_been_active" + json:"webdriver"
↓
NavigatorProperties struct with 3 fields
json:"navigator_properties" + json:"created_at"
↓
ClientSignals struct wrapping NavigatorProperties
**Evidência 2: Caminhos de pacote
**Binários Go embutem caminhos completos de pacote:
strings xpforwarded.wasm | grep "crypto/"
# Returns
crypto/aes -> Uses AES encryption
crypto/cipher -> Uses cipher modes (block cipher interface)
crypto/internal/fips140/aes
crypto/internal/fips140/aes/gcm -> Specifically GCM mode
crypto/internal/fips140/sha256
**Evidência 3: Mensagens de erro
**Erros nos revelam os comportamentos das funções
strings xpforwarded.wasm | grep -i "error"
// returns
error creating AES cipher:
error creating GCM:
Error encrypting data:
Então já podíamos supor que há uma lógica como
// Error message "error creating AES cipher:" implies:
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("error creating AES cipher: %w", err)
}
// Error message "error creating GCM:" implies:
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("error creating GCM: %w", err)
}
E assim por diante; dê uma olhada no repo do github sobre isso para uma lógica de reconstrução completa do script GO original e a conversão para Python / NodeJS. Bem como o módulo python twitter-generator para gerar esses headers facilmente
E o que vem a seguir?
Terminamos a parte de geração de headers; não deixe de conferir o repo completo no Github.
O próximo passo é prosseguir com o primeiro desafio antibot. Não deixe de assinar a minha newsletter para não perdê-lo.
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.