Как антифрод-системы маркетплейсов вычисляют прокси по TLS-отпечатку ClientHello
Содержание
- Что вообще видят антифрод-системы
- Анатомия ClientHello
- JA3 и JA4 — как считают отпечаток
- Почему прокси палится
- Реальный пример: Python requests против Chrome
- Почему обычные прокси не спасают
- Что делать: подмена ClientHello
- Что делать: HTTP/2 fingerprint
- Прокси с TLS-терминацией: отдельная история
- Реальный кейс: парсинг Ozon
- Реальный кейс: Wildberries и HTTP/2
- Реальный кейс: Amazon и JA4
- Как проверить свой отпечаток
- Прокси-сервер как часть схемы
- Итог по граблям
Что вообще видят антифрод-системы
Когда браузер открывает соединение с маркетплейсом, первое, что уходит на сервер — это 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.