← Назад в базу знаний

Как антифрод-системы маркетплейсов вычисляют прокси по TLS-отпечатку ClientHello

Как антифрод-системы маркетплейсов вычисляют прокси по TLS-отпечатку ClientHello

Что вообще видят антифрод-системы

Когда браузер открывает соединение с маркетплейсом, первое, что уходит на сервер — это ClientHello. Пакет TLS-рукопожатия. До того как отправится хоть один HTTP-заголовок, до того как прозвучит cookie, до JavaScript. Сервер уже знает про клиента кучу всего.

Маркетплейсы (Ozon, Wildberries, Amazon, eBay) держат на входе WAF и антифрод-слой. Эти системы снимают отпечаток ClientHello и сравнивают с эталоном. Если клиент говорит, что он Chrome 124 на Windows 11, а отпечаток у него от Python-скрипта — привет, флаг.

Прокси тут палится не потому, что он прокси. А потому что за прокси сидит клиент, чей TLS-стек не совпадает с заявленным User-Agent. Это и есть основная зацепка.

Анатомия ClientHello

ClientHello — не монолит. Это набор полей с расширениями. Антифрод смотрит на порядок и содержимое каждого.

Ключевые поля:

- `tls_version` — обычно 0x0303 (TLS 1.2) даже при TLS 1.3

- `cipher_suites` — список шифров в строгом порядке

- `extensions` — список расширений тоже в порядке

- `supported_groups` — эллиптические кривые

- `ec_point_formats`

- `signature_algorithms`

- `ALPN` — h2, http/1.1

Порядок критичен. Chrome всегда отправляет расширения в одном порядке, Firefox — в другом, Safari — в третьем. OpenSSL — вообще в четвёртом. Python `requests` с urllib3 — в пятом.

Вот как выглядит ClientHello от curl с OpenSSL 3.0:

```

16 03 01 02 00 01 00 01 fc 03 03 ...

```

А вот от Chrome 124:

```

16 03 01 02 00 01 00 01 fc 03 03 ... // начало похоже

```

Дальше начинаются различия. Chrome отправляет `GREASE`-значения (0x0a0a, 0x1a1a и т.д.) в случайных местах. OpenSSL их не шлёт. Уже первая зацепка.

JA3 и JA4 — как считают отпечаток

JA3 — хеш от пяти полей ClientHello, соединённых запятыми:

```

TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats

```

Пример JA3 для Chrome 120:

```

771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0

```

MD5 от этой строки — `cd08e31494f9531f560d64c695473da9`. Это и есть JA3-хеш Chrome 120.

JA4 — новее, устойчивее к рандомизации. Формат:

```

a_b_c

```

Где `a` — тип (t — TCP, q — QUIC), версия TLS, SNI, число шифров, число расширений, ALPN. `b` — отсортированные шифры. `c` — сигнатуры и расширения.

JA4 не ломается от перестановки расширений. JA3 ломается. Поэтому антифроды переходят на JA4.

Почему прокси палится

Прокси сам по себе TLS не трогает, если это CONNECT-туннель. Клиент внутри прокси устанавливает TLS напрямую с маркетплейсом. Отпечаток идёт от клиента.

Проблема в другом. Автоматизаторы, сидящие за прокси, часто используют:

- Python `requests` — JA3 от urllib3

- Node.js `axios` — JA3 от Node TLS

- Go `net/http` — JA3 от Go crypto/tls

- curl — JA3 от OpenSSL

- Scrapy — вообще свой стек

Ни один из них не совпадает с Chrome. Антифрод видит: User-Agent говорит Chrome, JA3 говорит Python. Флаг.

Второй момент. Прокси-пулы часто переиспользуют TLS-сессии. Session ID или session ticket от одного клиента уходит другому. Server-side session resumption ломается, антифрод видит аномалию.

Третий момент. HTTP/2 fingerprint. Если клиент заявляет Chrome, но не шлёт `SETTINGS` с параметрами Chrome (`HEADER_TABLE_SIZE=65536`, `MAX_CONCURRENT_STREAMS=1000`, `INITIAL_WINDOW_SIZE=6291456`), это тоже флаг.

Реальный пример: Python requests против Chrome

Запускаю `requests` с User-Agent от Chrome 124:

```python

import requests

headers = {

"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "

"AppleWebKit/537.36 (KHTML, like Gecko) "

"Chrome/124.0.0.0 Safari/537.36"

}

r = requests.get("https://example.com", headers=headers)

```

JA3 этого запроса — что-то вроде:

```

771,4866-4867-4865-49196-49200-49195-49199-52393-52392-49188-49192-49187-49191-49162-49172-49161-49171-157-156-61-60-53-47-255,0-11-10-35-16-22-23-13-43-45-51-21,29-23-30-25-24-256-257-258-259-260,0-1-2

```

MD5 — `3b5074b1b5d032e5620f69f9f700ff0e`. Это JA3 Python. Chrome 124 даёт `cd08e31494f9531f560d64c695473da9`. Разница видна за миллисекунду.

Антифрод маркетплейса сверяет эти два значения. Не совпало — challenge (капча), потом блок.

Почему обычные прокси не спасают

Прокси меняет IP. Всё. TLS-отпечаток идёт от клиента. Если клиент — Python, отпечаток Python. Прокси тут ни при чём.

Есть три уровня маскировки:

1. IP-уровень — прокси, VPN

2. TLS-уровень — JA3/JA4 маскировка

3. HTTP-уровень — заголовки, порядок, HTTP/2 fingerprint

Большинство прокси-сервисов закрывают только первый уровень. Маркетплейсы давно научились смотреть на второй и третий.

Что делать: подмена ClientHello

Первый путь — использовать библиотеки, которые умеют подделывать ClientHello. В Python это `curl_cffi` (обёртка над curl-impersonate), в Go — `utls`, в Node — `cycletls`.

Пример с `curl_cffi`:

```python

from curl_cffi import requests

r = requests.get(

"https://example.com",

impersonate="chrome124"

)

print(r.status_code)

```

`curl_cffi` подменяет ClientHello на Chrome 124, включая GREASE, порядок расширений, HTTP/2 SETTINGS. JA3 совпадает с настоящим Chrome.

В Go через `utls`:

```go

package main

import (

"fmt"

"io"

"net/http"

"github.com/refraction-networking/utls"

)

func main() {

config := &utls.Config{ServerName: "example.com"}

conn, _ := utls.Dial("tcp", "example.com:443", config)

defer conn.Close()

req, _ := http.NewRequest("GET", "https://example.com", nil)

req.Header.Set("User-Agent", "Mozilla/5.0 ... Chrome/124.0.0.0 ...")

req.Write(conn)

resp, _ := http.ReadResponse(bufio.NewReader(conn), req)

body, _ := io.ReadAll(resp.Body)

fmt.Println(len(body))

}

```

`utls` позволяет вручную собрать ClientHello с нужным отпечатком. Есть готовые профили: `HelloChrome_124`, `HelloFirefox_125`, `HelloSafari_17_0`.

Что делать: HTTP/2 fingerprint

TLS — половина дела. Вторая половина — HTTP/2. Chrome отправляет `SETTINGS` фрейм с конкретными значениями. Порядок фреймов тоже фиксирован: `SETTINGS`, `WINDOW_UPDATE`, `PRIORITY`, `HEADERS`.

Проверить свой HTTP/2 fingerprint можно через `https://tls.peet.ws/api/all`. Он покажет и JA3, и JA4, и Akamai HTTP/2 fingerprint.

Пример ответа:

```json

{

"ja3": "cd08e31494f9531f560d64c695473da9",

"ja3_hash": "cd08e31494f9531f560d64c695473da9",

"ja4": "t13d1516h2_8daaf6152771_b186095e22b6",

"http2": {

"akamai_fingerprint": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p",

"settings": {

"HEADER_TABLE_SIZE": 65536,

"MAX_CONCURRENT_STREAMS": 1000,

"INITIAL_WINDOW_SIZE": 6291456,

"MAX_HEADER_LIST_SIZE": 262144

}

}

}

```

Если `akamai_fingerprint` не совпадает с Chrome — антифрод это увидит. `curl_cffi` и `utls` умеют подделывать и HTTP/2.

Прокси с TLS-терминацией: отдельная история

Есть прокси, которые сами терминируют TLS. То есть клиент шлёт TLS прокси, прокси расшифровывает, потом шлёт свой TLS маркетплейсу. Это MITM-прокси.

Плюс: клиент может быть любым, прокси подставит нужный отпечаток. Минус: сертификат. Прокси должен подписать сертификат маркетплейса своим CA. Браузер это увидит, если CA не в trust store.

Для автоматизации это работает. Клиент — Python, прокси — mitmproxy с плагином подмены ClientHello. Но это медленнее: два TLS-рукопожатия вместо одного. Плюс 20-40 мс задержки на каждый запрос.

Реальный кейс: парсинг Ozon

Клиент парсит Ozon через резидентные прокси. Python `requests`, User-Agent Chrome. Первые 50 запросов — 200 OK. На 51-м — 403 с капчей.

Причина: Ozon снимает JA3. Первые запросы прошли, потому что антифрод накапливал статистику. На 51-м сработал триггер: JA3 Python при UA Chrome.

Решение: переход на `curl_cffi` с `impersonate="chrome124"`. JA3 совпал. Плюс ротация прокси каждые 10 запросов. Ошибки упали с 40% до 2%.

Технические детали: Ozon использует собственный антифрод на базе JA3 + поведенческих метрик. Заголовки `sec-ch-ua`, `sec-fetch-*` тоже проверяются. Без них — флаг даже при правильном JA3.

Реальный кейс: Wildberries и HTTP/2

Wildberries смотрит на Akamai HTTP/2 fingerprint. Клиент на Go с `net/http` слал правильный JA3 (через `utls`), но HTTP/2 fingerprint от Go не совпадал с Chrome.

Go `net/http` отправляет `SETTINGS` с `INITIAL_WINDOW_SIZE=4194304`, Chrome — `6291456`. Разница. Антифрод видел: TLS от Chrome, HTTP/2 от Go.

Решение: переход на `fhttp` (форк `net/http` с кастомными SETTINGS) или на `curl_cffi` через cgo. После фикса — стабильные 200 OK.

Цифры: до фикса — 30% запросов с 403, после — 0.5%. Задержка выросла на 15 мс из-за cgo-прослойки.

Реальный кейс: Amazon и JA4

Amazon перешёл на JA4 в 2024. Старые JA3-маскировки перестали работать. JA4 устойчив к перестановке расширений, поэтому простой shuffle не помогает.

Клиент использовал `curl-impersonate` версии 0.6.0 с профилем Chrome 120. JA3 совпадал, JA4 — нет. Amazon видел расхождение.

Причина: JA4 учитывает ALPN и SNI в первой части хеша. `curl-impersonate` 0.6.0 не отправлял SNI в нужном формате. Обновление до 0.7.0 с профилем Chrome 124 решило проблему.

Задержка: JA4-проверка добавляет 2-3 мс на стороне Amazon. Для клиента незаметно.

Как проверить свой отпечаток

Перед тем как лезть на маркетплейс, проверь себя. Несколько сервисов:

```bash

curl -s https://tls.peet.ws/api/all | jq '.ja3, .ja4, .http2.akamai_fingerprint'

```

Или через Python:

```python

import requests

r = requests.get("https://tls.peet.ws/api/all")

data = r.json()

print("JA3:", data["ja3"])

print("JA4:", data["ja4"])

print("Akamai:", data["http2"]["akamai_fingerprint"])

```

Сравни с эталоном Chrome. Эталон можно снять, открыв `tls.peet.ws` в настоящем Chrome. Если расходится — антифрод маркетплейса тоже увидит расхождение.

Прокси-сервер как часть схемы

Прокси сам по себе не решает проблему отпечатка. Но правильная связка решает. Схема: клиент с подменённым ClientHello → резидентный прокси → маркетплейс.

Прокси должен:

- не терминировать TLS (иначе свой отпечаток)

- поддерживать HTTP/2 end-to-end

- не добавлять заголовки (`Via`, `X-Forwarded-For`)

- не переиспользовать TLS-сессии между клиентами

Если прокси ломает HTTP/2 и откатывает на HTTP/1.1 — маркетплейс это видит. Chrome всегда использует h2. Откат на h1 — флаг.

Сервисы вроде lexic.ml дают IPv6-прокси с сохранением end-to-end TLS. Клиент сам строит ClientHello, прокси только маршрутизирует пакеты. Отпечаток идёт от клиента, а не от прокси. Это правильная архитектура для обхода антифрода.

Итог по граблям

Антифрод маркетплейсов смотрит на три слоя: IP, TLS, HTTP/2. Прокси закрывает только IP. Без подмены ClientHello и HTTP/2 fingerprint автоматизация палится за десяток запросов.

Инструменты: `curl_cffi` для Python, `utls` для Go, `cycletls` для Node. Плюс проверка через `tls.peet.ws`. Плюс прокси без TLS-терминации.

Если хоть один слой не совпадает с заявленным браузером — капча, 403, бан. Все три должны совпадать. Тогда антифрод видит обычного Chrome с обычного IP.

✔️Купить прокси