Немного истории
В один день одна из крупных досок объявлений начала возвращать фейковые характеристики объявлений, когда понимала, что мы - бот.
Видимо сайт добавил наш прокси в blacklist, но в нашем пуле около 100к проксей, все прокси попали в blacklist?
Попробовав запустить парсер на другом сервере, HTTP запросы возвращали корректные данные. Спустя неделю, ситуация повторилась.
Мы также попробовали отправить запрос на локальной машине с "забанненым" прокси, на удивление, данные пришли корректные, но отправив запрос с этим же прокси на сервере, получили фейковые. Отсюда вытекает вопрос: как сайт определяет, что запросы посылаются с одной машины, если используются прокси?
Прошарив весь гугл, мы узнали об интересной технологии под названием TLS Fingerprint и теперь хотим поделиться что это такое и как обойти.
TLS
TLS (transport layer security — Протокол защиты транспортного уровня), как и его предшественник SSL (secure sockets layer — слой защищённых сокетов), — криптографические протоколы, обеспечивающие защищённую передачу данных между узлами в сети Интернет. TLS и SSL используют асимметричное шифрование для аутентификации, симметричное шифрование для конфиденциальности и коды аутентичности сообщений для сохранения целостности сообщений.
Перед тем, как начать обмен данными через TLS, клиент и сервер должны согласовать параметры соединения, а именно: версия используемого протокола, способ шифрования данных, а также проверить сертификаты, если это необходимо. Подробнее можно почитать по ссылке
TLS fingerprinting
Суть технологии в захвате элементов пакета приветствия клиента, которые остаются статичными от сеанса к сеансу для каждого клиента. На их основе можно создать «отпечаток пальца» для распознавания конкретного клиента в последующих сеансах. Записываются следующие поля:
версия TLS;
версия записи TLS;
наборы шифров;
параметры сжатия;
список расширений.
Кроме того, данные собираются из трех конкретных расширений (если они доступны):
алгоритмы подписи;
алгоритм для шифрования данных;
хэш функция для проверки содержимого.
Использование такого набора данных позволяет с большой точностью идентифицировать используемый клиент (например, браузер).
Подробнее можно почитать по ссылке
Обход TLS Fingerprint
Отлично, теперь мы знаем в чем дело. Давайте взглянем на C# TLS Client Hello. В этом нам поможет Wireshark. В фильтре указываем ip.dst == ipaddress, где ipaddress - интересующий нас сайт
Выполняем обычный HTTP GET запрос
using var httpClient = new HttpClient();
await httpClient.GetAsync(new Uri("САЙТ"));
Идем в Wireshark. Появился список пакетов. Нас интересует Client Hello
Нам удалось перехватить стандартный приветственный пакет TLS C#, теперь получим пакет браузера для сравнения. Выполняем те же шаги, но теперь вместо HTTP GET запроса просто переходим на сайт с браузера.
Как мы можем заметить, пакеты сильно различаются. Наша задача сделать полностью идентичный приветственному пакету TLS браузера. В таком случае нас не смогут забанить, а если захотят, то забанив нас, никто не сможет зайти на сайт с Google Chrome. Вот и решение, осталось лишь подменить пакет.
Обход TLS Fingerprint с помощью .NET (Попытка)
Изучив все апи для работы с HTTP, выяснилось, что наш любимый язык C# не позволяет манипулировать параметрами на таком низком уровне сети. Попробуем добавить такую возможность сами. Благо .NET опенсоурсный, скачиваем runtime. Нас интересует библиотека System.Net.Security
. В классе SslStream.Implementation
есть метод ForceAuthenticationAsync
. Взглянем на кусок кода:
message = _context!.NextMessage(reAuthenticationData);
if (message.Size > 0)
{
await adapter.WriteAsync(message.Payload!, 0, message.Size).ConfigureAwait(false);
await adapter.FlushAsync().ConfigureAwait(false);
if (NetEventSource.Log.IsEnabled())
NetEventSource.Log.SentFrame(this, message.Payload);
}
В методе NextMessage
инициализируется контекст безопасности между клиентским приложением и удаленным узлом, который присваевает дефолтный Payload
. У каждой ОС свой поставщик услуг обеспечения безопасности. У Windows это Security Support Provider Interface (SSPI).
Если перевести массив байтов Payload
в шестнадцатеричную систему счисления и сравнить с пакетом, становится ясно, что Payload
и есть наш пакет:
Копируем как шестнадцатеричный дамп (в Wireshark нет возможности сдампить в десятичную систему счисления) пакет браузера, переводим в десятичную систему счисления. Попробуем подсунуть в поле Payload
скопированные данные:
message = _context!.NextMessage(reAuthenticationData);
message.Size = TlsPayload.Length;
byte[] tlsPayload = new byte[TlsPayload.Length];
TlsPayload.CopyTo(tlsPayload, 0);
// update random from message.Payload
for (int i = 11; i < 43; i++)
{
tlsPayload[i] = message.Payload![i];
}
message.Payload = tlsPayload;
if (message.Size > 0)
{
await adapter.WriteAsync(message.Payload!, 0, message.Size).ConfigureAwait(false);
await adapter.FlushAsync().ConfigureAwait(false);
if (NetEventSource.Log.IsEnabled())
NetEventSource.Log.SentFrame(this, message.Payload);
}
Добавляем кастомную библиотеку System.Net.Security
в наш проект и пробуем отправить запрос.
Получилось подменить, но, к сожалению, следующий пакет возвращает ошибку "Получено непредвиденное сообщение или оно имеет неправильный формат". Порывшись пару недель, я так и не смог разобраться с этим. Возможно, данный вариант был слишком наивным, но думаю умельцы хабра смогут допилить, а мы переходим к следующему.
Обходим TLS Fingerprint с помощью Golang
Наш любимый гугл привел к библиотеке, которая позволяет манипулировать параметрами приветственного пакета. Есть одно но, библиотека написана на языке Golang.
Посидев пару вечеров за изучением языка, удалось написать "прокси" сервер. Скачиваем проект, запускаем командой go run main.go port
. Сервер имеет два эндпоинта:
check-status
- проверка состоянияhandle
- обработчик http запросов
Переходим к коду на C#. Мы написали библиотеку GoProxy для удобства, добавляем в проект через nuget manager. Теперь попробуем подменить пакет.
Инициализируем новый класс GoHttpRequest
с помощью метода GoHttpRequest.Create
. Метод принимает ссылку на обработчик запроса. GoHttpRequest
позволяет задать параметры запроса:
public class GoHttpRequest
{
/// Ссылка на обработчик запроса
public string GoProxyUrl { get; set; }
/// Ссылка на ресурс
public string Url { get; set; }
/// Полный отпечаток Ja3
/// Можно получить свой полный отпечаток на сайте https://ja3er.com/json
public string Ja3 { get; set; }
/// Тело запроса, если посылается POST запрос
public string Body { get; set; }
/// Прокси
/// http://IP:PORT
/// http://LOGIN:PASS@IP:PORT
public string Proxy { get; set; }
/// Метод запроса
public string Method { get; set; }
/// Идентификатор клиентского приложения
public string UserAgent { get; set; }
/// Таймаут в секундах
public int TimeOut { get; set; }
/// Список куков
public List<GoCookie> Cookies { get; set; }
/// Список заголовков
public Dictionary<string, string> Headers { get; set; }
}
Устанавливаем полный отпечаток JA3 (JA3 и JA3S – это методы снятия отпечатков TLS. JA3 отслеживает способ, которым клиентское приложение обменивается данными через TLS, а JA3S отслеживает ответ сервера. Вместе они создают отпечаток криптографического согласования между клиентом и сервером), свой отпечаток можно найти по ссылке https://ja3er.com/json.
После установки нужных параметров вызываем метод GetResponseAsync
, он принимает следующие параметры:
Url
- ссылка на ресурсMethod
- тип запроса (GET|POST|PUT|HEAD)ThrowIfNotSuccessCode
- выкинуть ошибку, если ответ отрицательный
// Ссылка на обработчик запросов
const string GoProxyUrl = "http://localhost:8000/handle";
const string ChromeUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36";
const string ChromeJa3 = "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-21,29-23-24,0";
var response = await GoHttpRequest.Create(GoProxyUrl)
.WithJa3(ChromeJa3)
.WithHeader("Accept", "*")
.WithProxy("http://IP:PORT" | "http://LOGIN:PASS@IP:PORT")
.WithUserAgent(ChromeUserAgent)
.GetResponseAsync("https://ja3er.com/json");
В ответ получаем класс GoHttpResponse
:
public class GoHttpResponse
{
/// Статус операции
public bool Success { get; set; }
/// Текст ошибки
public string Error { get; set; }
/// Полезная нагрузка
public GoHttpResponsePayload Payload { get; set; }
}
public class GoHttpResponsePayload
{
/// Статус HTTP запроса
public int Status { get; set; }
/// Конечная ссылка
public string Url { get; set; }
/// Контент
public string Content { get; set; }
/// Список куков с заголовков Set-Cookie
public List<Cookie> Cookies { get; set; }
/// Список заголовков
public Dictionary<string, string> Headers { get; set; }
}
Взглянем на response.Payload.Content
:
{
"ja3_hash": "b32309a26951912be7dba376398abc3b",
"ja3": "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-21,29-23-24,0",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"
}
JA3 успешно применился. Вернемся в Wireshark, находим приветсвенный пакет
Как мы видим, пакет идентичен браузерному. Мы успешно подменили приветственный пакет TLS!
Итог
В этой статье мы поделились опытом обхода TLS Fingerprint и предоставили библиотеку для удобного использования.
Благодаря решению в виде прокси сервера, обход можно использовать на любом языке
Полезные ссылки:
SpoofingTlsFingerprint - прокси сервер для обхода TLS Fingerprint на языке Golang
GoProxy (github) - библиотека для удобного обхода TLS Fingerprint. Обертка над SpoofingTlsFingerprint
Комментарии (31)
buris
21.02.2022 21:55+1Спасибо за инфу. Кстати в одном пакете (пик 4) видно что за доска объявлений.
ifap
21.02.2022 23:13+12Получилось подменить, но, к сожалению, следующий пакет возвращает ошибку "Получено непредвиденное сообщение или оно имеет неправильный формат".
Чего ж тут удивительного? Вы "поздоровались" списком расширений и шифронаборов от Хрома, тогда как по факту половина из них не поддерживалась клиентом. Вангую, что сервер ответил на приветствие, например, фактически неподдерживаемым шифронабором, а Ваш .NET, игнорируя указание сервера, использовал другой шифронабор. Казалось бы, что могло пойти не так? ;)
shai_hulud
22.02.2022 00:13+1Под .NET bouncy castle имеет полностью настраиваемый TLS клиент.
Skyuzi Автор
22.02.2022 00:16Верно, это был один из вариантов, но это только TCP. А как же обертка для работы с HTTP? Самому писать не очень хотелось, сроки поджимали, клиенты возмущались, к тому же не смог найти адекватное решение использование прокси
shai_hulud
22.02.2022 11:25Реализовать HTTP 1.1 для цели скраппинга один вечер, вам же не все фичи нужны, а только запрос страниц последовательно. Прелесть старых протоколов в простоте.
force
22.02.2022 18:30+2Думаю, что если сайт ловит даже по TLS Fingerprint, то поведение тупых скачивалок должен засекать мгновенно.
HTTP достаточно сложный протокол, чтобы его на коленке сделать и это не очень сильно отличалось от поведения браузера. Тот же keep-alive уже большая боль.
Wesha
22.02.2022 00:44+5В один день одна из крупных досок объявлений начала возвращать фейковые характеристики объявлений, когда понимала, что мы - бот.
И на что только люди не пойдут, лишь бы API не предоставлять.
THQSql
22.02.2022 08:02+1Причем платную. Затраты с обеих сторон получаются на много выше.
И с помощью API можно контролировать данные. Многим даже контактные данные не нужны, достаточно самого факта появления нового объявления, далее клиент сам уже заходят на сайт.
Wesha
22.02.2022 09:47+2Ведь как бы Сарочка ни легла, её всё равно отксрейпят. Но можно сделать это безболезненно для обеих сторон, предоставив API, а можно так, как описано в статье. В результате получается хуже для всех.
Кстати, под API-вызовы можно завести ноду, отдельную от сайта, обслуживающего юзеров. И содержать её за малую часть той самой платы-за-API.
Но нет: мыши, кактус, плак-плак.
Skyuzi Автор
22.02.2022 10:00+1Есть апи, но оно, к сожалению, нам не подходит. Запрашивать данные можно в очень маленьком количестве и раз в определенное время ~1час. Нам же нужно максимально быстро получать изменения/новые объявления
JenyaRostov
22.02.2022 08:01+3Спасибо за статью! Встречал похожую проблему, но с CloudFlare, решение нашёл на просторах интернета
Решение заключается в использовании библиотеки BoringSSL
Достаточно просто сделать HTTP-сервер на плюсах с этой библиотекой(использовать libCURL) и перебрасывать запросы через него, и сайты будут думать что вы - браузер (BoringSSL используется в хромиуме)
Skyuzi Автор
22.02.2022 12:18Слышал, что BoringSSL используется в Andoid-е, но не думал, что и в хромиуме. Спасибо за информацию!
Vladimir_Putin
22.02.2022 20:43Я пытался написать бота для шахмат, но так и не смог подключиться так, чтобы меня не распознали как бота. Видимо, вот в чем было дело????
Skyuzi Автор
23.02.2022 14:16+1За годы разработки парсеров крупных досок объявлений, встречались разные и довольно интересные защиты, нам есть чем поделиться. Если читателям хабра будет интересно, можно сделать еще несколько статей на эту тему
Dzzzen
23.02.2022 15:53+1Больше всех интерес должны проявить авторы этого поста: https://habr.com/ru/company/proto/blog/652861/
yoigo
23.02.2022 22:01+2Очень интересно, пишите ещё. Огромное спасибо. Сам работаю в компании по скрапингу цен. Где нету API или http request банят, используем playwright. Браузеры chromium, chromium-headless, brave, brave-block, brave-headless. Иногда и старикан селениум с htmlunit выручает. Ну а так да, cloudflare самый проблематичный обойти.
Skyuzi Автор
23.02.2022 22:06Ого, впервые встретил человека с похожим бэкграундом. Очень приятно! Возможно следующая статья будет о том, как обойти ProofOfWork на сайтах
yoigo
23.02.2022 23:30Да в мире скрапинга много чего интересного. Из таких странных блокировок, это когда заходишь на продукт напрямую (раньше не посещая этот сайт, без cookies, даже не incognito) и тебе прилетает бан. А если зайти на home -> продукт всё Ок! Кто это придумал? Или это такая фича datadome? Одним словом весело. Кто хочет попробовать сайт: 6361727265666F75722E6672. Может я тоже когда соберусь с мыслями и запилю пост на хабре, что-то интересное.
FreddyFox
Годная статья ????