Все посты
7 мин чтенияglizzykingdreko

Реконструкция потока входа в X (бывший Twitter). Эпизод 1 — Генерация заголовков

Сегодняшний пост — первый в небольшой серии об автоматизации входа в X / Twitter. В этом эпизоде я реверс-инжинирю все заголовки X / Twitter, необходимые…

Реконструкция потока входа в X (бывший Twitter). Эпизод 1 — Генерация заголовков

Сегодняшний пост — первый в небольшой серии об автоматизации входа в X / Twitter. В этом эпизоде я реверс-инжинирю все заголовки X / Twitter, необходимые для потока входа. В конце серии у нас будет полноценный скрипт входа на Python, не зависящий от браузера.

Смотрите к этому эпизоду GitHub-репозиторий с полным разбором и его Python-модуль

X-Client-Transaction-Id

Этот токен генерируется для каждого запроса на основе трёх основных вещей:

  • Детали запроса (METHOD и URL)
  • Fingerprint на основе загруженного (мета-тега под названием «twitter-site-verification» и «динамического» js-файла)
  • Текущий timestamp

Исходный код выглядит так

После небольшой очистки мы получаем такой, более читаемый фрагмент


// 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));

Полный код, а также часть generateFingerprintHash (довольно простой fingerprint на основе css-анимаций) вы найдёте в моём github-репозитории.

Итак, как мы это решаем?

Путь к результату довольно прямой: нам просто нужно воссоздать тот же хеш на основе выполняемого действия и заложить в него время и ключ анимации, вот пример

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'
)

Часть с анимацией, а также полный код можно найти в моём репозитории.

X-Guest-Token

Генерируется из

POST https://api.x.com/1.1/guest/activate.json

довольно просто и прямолинейно, этот можно пропустить и идти дальше.

X-Xp-Forwarded-For

Заглянем в исходный код:

Итак, нам нужно проанализировать этот window.XPForwardedForSDK, который на первый взгляд имеет пару атрибутов, и здесь задействован WASM-скрипт

Основные атрибуты, важные для нас:

  • init(environment)
    Загружает WASM-рантайм
  • getForwardedForStr()
    Возвращает нам dict, содержащий важную строку и время истечения

Очевидно, поскольку тут почти всё построено на wasm, нам нужно понять, что оно делает.

1. Декомпиляция WASM-скрипта

После извлечения и разбора скрипта простым фрагментом вроде

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');
}

Дальше можно преобразовать бинарный WASM в читаемый текст с помощью полезного инструмента wasm2wat так

wasm2wat xpforwarded.wasm -o xpforwarded.wat

Теперь у нас гораздо лучшее представление о ситуации

Мы уже можем отметить тот факт, что это скрипт на основе GO, и это значительно упрощает процесс отладки.

2. Анализ сегментов данных WAT

WAT-файлы содержат сегменты данных со встроенными строками

(data (;0;) (i32.const 65536) "expand 32-byte k...")
(data (;1;) (i32.const 65842) "meta\00invalid syntax0123456789abcefz...")

главные находки, которые я сделал:

| 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               |

Мы почти у цели, давайте попробуем извлечь ключ шифрования, используемый для AES-шифрования, чтобы у нас было всё

strings xpforwarded.wasm | grep -E "^[0-9a-f]{64}$"

# that will output
0e6be1f1e21ffc33590b888fd4dc81b19713e570e805d4e5df80a493c9571a05

3. Понимание Go-рантайма

Немного теории всегда нужно, чтобы разобраться в ситуации

Go компилируется в WASM с использованием таргета GOOS=js GOARCH=wasm. Для этого требуется:

  1. JavaScript-файл, реализующий импорты gojs
  2. Go управляет собственной памятью внутри линейной памяти WASM, поэтому нужен менеджер
  3. Пакет syscall/js обеспечивает двустороннюю коммуникацию

**Система ссылок на значения (Value Reference System)
**Go WASM использует систему ссылок для передачи значений между Go и 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
];

Когда Go вызывает syscall/js.valueGet(obj, “property”):

  1. Go передаёт ID ссылки на объект
  2. JS-код-обвязка ищет объект в _values
  3. Получает свойство
  4. Сохраняет результат в _values и возвращает новый ID

**Asyncify-трансформация
**WASM-модуль использует asyncify-трансформацию Binaryen для обработки асинхронных JavaScript-операций:


(export "asyncify_start_unwind" (func 436))
(export "asyncify_stop_unwind" (func 437))
(export "asyncify_start_rewind" (func 438))

Это позволяет Go-коду делать await JavaScript-Promise, что необходимо для:

  • crypto.subtle.encrypt()
  • crypto.subtle.digest()

4. Методология реверс-инжиниринга

Подход, который я собираюсь использовать, чтобы корректно воссоздать этот wasm, — вообще не использовать сам файл напрямую. Было бы слишком просто просто написать несложный JS-скрипт, запускающий файл в правильном окружении. Я действительно хочу воссоздать всё с нуля. Приступим.

**Улика 1: Встроенные строки
**Самая мощная техника анализа GO-бинарников — извлечение и анализ встроенных строк, таких как:

  • Имена пакетов
  • Сообщения об ошибках
  • JSON struct-теги
  • Имена типов

Давайте извлечём всё одной командой

strings xpforwarded.wasm | grep -v "^.$" | sort -u > all_strings.txt

Теперь изучим их

| 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

Логика, которую мы будем использовать, такова

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

**Улика 2: Пути к пакетам
**Go-бинарники встраивают полные пути к пакетам:

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

**Улика 3: Сообщения об ошибках
**Ошибки раскрывают нам поведение функций

strings xpforwarded.wasm | grep -i "error"

// returns
error creating AES cipher:
error creating GCM:
Error encrypting data:

Так что мы уже могли бы предположить, что есть логика вроде

// 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)
}

И так далее — загляните в github-репозиторий, там есть полная логика реконструкции исходного GO-скрипта и его конвертация в Python / NodeJS. А также python-модуль twitter-generator для лёгкой генерации этих заголовков

Что дальше?

Мы завершили часть с генерацией заголовков, обязательно загляните в полный репозиторий на Github.

Следующий шаг — переход к первой антибот-проверке. Обязательно подпишитесь на мою рассылку, чтобы не пропустить его.

Связаться со мной

Если статья вам понравилась, подписывайтесь на меня на GitHub и Medium, чтобы получать уведомления, когда я публикую что-то или выкладываю в open source, или купите мне кофе, чтобы поддержать меня.

reverse-engineeringxswasmtwitterpython-automation

Пропустите реверс-инжиниринг.

Takion возвращает свежие cookie, заголовки и токены для любой крупной антибот-стены. Один POST, без браузера, первый вызов в течение часа.