Немного истории

В один день одна из крупных досок объявлений начала возвращать фейковые характеристики объявлений, когда понимала, что мы - бот.

Видимо сайт добавил наш прокси в 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 и предоставили библиотеку для удобного использования.

Благодаря решению в виде прокси сервера, обход можно использовать на любом языке

Полезные ссылки:

Комментарии (31)


  1. FreddyFox
    21.02.2022 21:01

    Годная статья ????


  1. buris
    21.02.2022 21:55
    +1

    Спасибо за инфу. Кстати в одном пакете (пик 4) видно что за доска объявлений.


    1. Skyuzi Автор
      21.02.2022 21:59

      Поправил, спасибо!


      1. qw1
        22.02.2022 00:14
        +1

        Всё равно видно, в hex-дампе ))


        1. Skyuzi Автор
          22.02.2022 00:33
          +1

          Кто не поленится перевести, для того не жалко ;)


          1. Saiv46
            22.02.2022 09:10

            По длине домена (префикс перед строкой 07) уже понятно что за сайт. У остальных популярных досок объявлении домен немного длиннее


            1. Skyuzi Автор
              22.02.2022 10:01

              Есть известный домен, который меньше него


            1. Dzzzen
              23.02.2022 12:39

              Для тех, кому тоже интересно: 6175746f2e7275


  1. ifap
    21.02.2022 23:13
    +12

    Получилось подменить, но, к сожалению, следующий пакет возвращает ошибку "Получено непредвиденное сообщение или оно имеет неправильный формат".

    Чего ж тут удивительного? Вы "поздоровались" списком расширений и шифронаборов от Хрома, тогда как по факту половина из них не поддерживалась клиентом. Вангую, что сервер ответил на приветствие, например, фактически неподдерживаемым шифронабором, а Ваш .NET, игнорируя указание сервера, использовал другой шифронабор. Казалось бы, что могло пойти не так? ;)


  1. ByIgor
    21.02.2022 23:15

    Не смотрели в сторону этой библиотеки? https://github.com/refraction-networking/utls


    1. Skyuzi Автор
      21.02.2022 23:16

      Как раз под капотом CycleTLS, используется эта либа. CycleTLS - более удобная обертка над utls, мне легче было подключить ее и немного подтюнить под свои нужды


  1. shai_hulud
    22.02.2022 00:13
    +1

    Под .NET bouncy castle имеет полностью настраиваемый TLS клиент.


    1. Skyuzi Автор
      22.02.2022 00:16

      Верно, это был один из вариантов, но это только TCP. А как же обертка для работы с HTTP? Самому писать не очень хотелось, сроки поджимали, клиенты возмущались, к тому же не смог найти адекватное решение использование прокси


      1. shai_hulud
        22.02.2022 11:25

        Реализовать HTTP 1.1 для цели скраппинга один вечер, вам же не все фичи нужны, а только запрос страниц последовательно. Прелесть старых протоколов в простоте.


        1. force
          22.02.2022 18:30
          +2

          Думаю, что если сайт ловит даже по TLS Fingerprint, то поведение тупых скачивалок должен засекать мгновенно.

          HTTP достаточно сложный протокол, чтобы его на коленке сделать и это не очень сильно отличалось от поведения браузера. Тот же keep-alive уже большая боль.


  1. Wesha
    22.02.2022 00:44
    +5

    В один день одна из крупных досок объявлений начала возвращать фейковые характеристики объявлений, когда понимала, что мы - бот.

    И на что только люди не пойдут, лишь бы API не предоставлять.


    1. THQSql
      22.02.2022 08:02
      +1

      Причем платную. Затраты с обеих сторон получаются на много выше.

      И с помощью API можно контролировать данные. Многим даже контактные данные не нужны, достаточно самого факта появления нового объявления, далее клиент сам уже заходят на сайт.


      1. Wesha
        22.02.2022 09:47
        +2

        Ведь как бы Сарочка ни легла, её всё равно отксрейпят. Но можно сделать это безболезненно для обеих сторон, предоставив API, а можно так, как описано в статье. В результате получается хуже для всех.

        Кстати, под API-вызовы можно завести ноду, отдельную от сайта, обслуживающего юзеров. И содержать её за малую часть той самой платы-за-API.

        Но нет: мыши, кактус, плак-плак.


        1. Skyuzi Автор
          22.02.2022 10:00
          +1

          Есть апи, но оно, к сожалению, нам не подходит. Запрашивать данные можно в очень маленьком количестве и раз в определенное время ~1час. Нам же нужно максимально быстро получать изменения/новые объявления


          1. Wesha
            24.02.2022 03:16

            Ну так я и говорю: есть спрос (Вы). Почему на этот спрос нет предложения? У нас капитализмус или где?


            1. qw1
              24.02.2022 10:46

              Платёжеспособного спроса нет. Сайт свои данные оценивает в N миллионов рублей, т.е. в 10 руб. за один вызов API, а скрейперы хотят всё бесплатно.


  1. JenyaRostov
    22.02.2022 08:01
    +3

    Спасибо за статью! Встречал похожую проблему, но с CloudFlare, решение нашёл на просторах интернета

    Решение заключается в использовании библиотеки BoringSSL

    Достаточно просто сделать HTTP-сервер на плюсах с этой библиотекой(использовать libCURL) и перебрасывать запросы через него, и сайты будут думать что вы - браузер (BoringSSL используется в хромиуме)


    1. Skyuzi Автор
      22.02.2022 12:18

      Слышал, что BoringSSL используется в Andoid-е, но не думал, что и в хромиуме. Спасибо за информацию!


  1. MuhammadDev
    22.02.2022 11:52
    +1

    Спасибо. Статья просто ????


  1. akomarova
    22.02.2022 11:53
    +1

    Спасибо за статью! Очень вовремя попалась )


  1. Vladimir_Putin
    22.02.2022 20:43

    Я пытался написать бота для шахмат, но так и не смог подключиться так, чтобы меня не распознали как бота. Видимо, вот в чем было дело????


    1. Skyuzi Автор
      23.02.2022 14:16
      +1

      За годы разработки парсеров крупных досок объявлений, встречались разные и довольно интересные защиты, нам есть чем поделиться. Если читателям хабра будет интересно, можно сделать еще несколько статей на эту тему


      1. Dzzzen
        23.02.2022 15:53
        +1

        Больше всех интерес должны проявить авторы этого поста: https://habr.com/ru/company/proto/blog/652861/


  1. yoigo
    23.02.2022 22:01
    +2

    Очень интересно, пишите ещё. Огромное спасибо. Сам работаю в компании по скрапингу цен. Где нету API или http request банят, используем playwright. Браузеры chromium, chromium-headless, brave, brave-block, brave-headless. Иногда и старикан селениум с htmlunit выручает. Ну а так да, cloudflare самый проблематичный обойти.


    1. Skyuzi Автор
      23.02.2022 22:06

      Ого, впервые встретил человека с похожим бэкграундом. Очень приятно! Возможно следующая статья будет о том, как обойти ProofOfWork на сайтах


      1. yoigo
        23.02.2022 23:30

        Да в мире скрапинга много чего интересного. Из таких странных блокировок, это когда заходишь на продукт напрямую (раньше не посещая этот сайт, без cookies, даже не incognito) и тебе прилетает бан. А если зайти на home -> продукт всё Ок! Кто это придумал? Или это такая фича datadome? Одним словом весело. Кто хочет попробовать сайт: 6361727265666F75722E6672. Может я тоже когда соберусь с мыслями и запилю пост на хабре, что-то интересное.