Как мы TLS Fingerprint обходили…
Немного истории
В один день одна из крупных досок объявлений начала возвращать фейковые характеристики объявлений, когда понимала, что мы — бот.
Видимо сайт добавил наш прокси в 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
. Сервер имеет два эндпоинта:
Переходим к коду на C#. Мы написали библиотеку Haraba.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 Cookies { get; set; }
/// Список заголовков
public Dictionary 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 Cookies { get; set; }
/// Список заголовков
public Dictionary 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 и предоставили библиотеку для удобного использования.
Благодаря решению в виде прокси сервера, обход можно использовать на любом языке
Полезные ссылки: