Назад в Центр знаний
Отпечатки

Отпечатки TLS: JA3, JA4 и кадр HTTP/2

Как ClientHello превращается в JA3 или JA4, почему одно значение меняется между запусками, что сообщает кадр SETTINGS и как проверить свой клиент.

BotBrowser Team

Документация

Нужна структурированная документация по теме Отпечатки?

Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.

Отпечаток на уровне соединения вычисляется по байтам, которые существуют раньше любого документа. BotBrowser предоставляет объявленный профиль, который поддерживает согласованность поверхностей, доступных странице, и не может раскрыть ни ClientHello протокола TLS, ни кадр настроек HTTP/2, потому что ни то, ни другое не является поверхностью страницы. Разбор опирается на один перехваченный ClientHello и один декодированный кадр настроек, показывает, как собирается каждый отпечаток, и объясняет, почему два запуска одного браузера дают устойчивое значение при одной конструкции и другое значение при другой.

Кратко

  • JA3 объединяет пять полей ClientHello в порядке их появления и хеширует результат, поэтому порядок расширений входит в значение.
  • Перехваченный ClientHello браузера Chrome 157 дал t13d1517h2 в качестве префикса JA4: TLS 1.3, имя домена присутствует, 15 шифров, 17 расширений и h2 из первого имени протокола в списке.
  • Два запуска одного браузера дали разные значения JA3 и одинаковое значение JA4, потому что Chrome перемешивает порядок расширений, а JA4 сортирует перед хешированием.
  • Порядок расширений между запусками изменился полностью, а отсортированный набор остался прежним, поэтому неотсортированный хеш измеряет порядок, который клиент не фиксирует.
  • Клиент TLS 1.3 сообщает legacy_version 0x0303 и помещает настоящую версию в расширение поддерживаемых версий, поэтому версия, прочитанная только из устаревшего поля, указывает TLS 1.2 для рукопожатия TLS 1.3.
  • Кадр настроек протокола HTTP/2 состоит из записей по шесть байт: двухбайтовый идентификатор и четырёхбайтовое значение; клиент и сервер отправляют разные наборы.
  • Два рукопожатия, различающиеся только списком предложенных протоколов, приходят к одной версии TLS с одним шифром и согласуют разные протоколы; страница может прочитать согласованное имя, но не список, предложенный за ним.
  • Скрипт страницы не может прочитать ни одно из этих значений, и никакая настройка на стороне страницы их не меняет.
Запись ClientHello разделена на пять полей JA3, список расширений показан в двух порядках, затем префикс JA4 разложен на версию, наличие имени, число шифров, число расширений и инициалы протоколов, а кадр SETTINGS представлен записями по шесть байт.

Содержание

ClientHello, его поля и проблема порядка в JA3

Первое сообщение соединения TLS это ClientHello. Оно объявляет версии, которые поддерживает клиент, наборы шифров, которые он принимает, и список пронумерованных расширений. Перехват из локального слушателя, а не из объекта библиотеки, делает остальной разбор проверяемым, потому что байты являются входом, который действительно читает вторая сторона.

record_bytes=1949 handshake_bytes=1944
legacy_version=0x0303
offered_versions=0x0304,0x0303
sni=localhost
alpn=h2,http/1.1
cipher_count=15 extension_count=17
groups=11ec,001d,0017,0018
point_formats=0

Две строки этого вывода создают почти всю путаницу в теме. Первая это legacy_version=0x0303, значение TLS 1.2, хотя перед нами клиент TLS 1.3; настоящий ответ находится в расширении поддерживаемых версий, в 0x0304. Инструмент, который выдаёт устаревшее поле за версию, читает поле правильно и отвечает на другой вопрос, чем задал читатель.

Вторая это значение grease в списке расширений. Клиент вставляет его, чтобы проверить корректную обработку неизвестных номеров сервером, и каждая описанная здесь конструкция отпечатка отфильтровывает grease. Анализатор, который сохраняет эти значения, выдаёт отпечаток, меняющийся при каждом соединении и не совпадающий ни с чем.

JA3 берёт пять полей ClientHello, записывает их в фиксированном порядке, соединяет запятыми и дефисами и вычисляет MD5 результата. Эти поля: версия, список шифров, список расширений, список эллиптических кривых и список форматов точек эллиптических кривых. Конструкция достаточно проста, чтобы описать её полностью, и эта полнота объясняет её широкое распространение.

ja3_string=771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,17613-35-51-13-11-43-65281-5-0-65037-10-45-16-23-51764-27-18,4588-29-23-24,0
ja3=33bb5fa9c4be545f39008976c0f67d67

Прочитайте второе поле этой строки, и причина хрупкости значения станет видимой. Шифры перечислены в том порядке, в котором клиент их предложил, и порядок входит в хеш. Тот же набор из пятнадцати шифров, записанный в другой последовательности, даёт другой MD5 без изменения того, что поддерживает клиент.

Поле расширений несёт ту же слабость сильнее. Расширения образуют список, и клиент, который перемешивает список, по-прежнему говорит о тех же возможностях. JA3 хеширует перемешивание. Это не дефект определения, потому что определение говорит то, что говорит; это ограничение того, что значение может означать, и читатель, который принимает изменённый JA3 за доказательство другого клиента, читает порядок как идентичность.

JA4, что означает префикс и что скрывает TLS 1.3

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

ja4_prefix=t13d1517h2
ja4=t13d1517h2_8daaf6152771_cb7bf5808d99

Символ t обозначает соединение TCP. 13 это согласованная версия TLS, прочитанная из расширения поддерживаемых версий, а не из устаревшего поля. d сообщает, что имя домена присутствовало, в отличие от i, когда клиент подключился по адресу и имени не отправил. Двузначное 15 после этого является числом шифров, а вторая пара, 17, это число расширений. Завершающее h2 берётся из первого имени протокола в предложенном списке по первому и последнему буквенно-цифровому символу, поэтому h2 даёт h2, а список, начинающийся с http/1.1, даёт h1.

Значение после первого подчёркивания хеширует шифры, а после второго хеширует расширения вместе с алгоритмами подписи. Поскольку оба списка сначала сортируются, их переупорядочивание не сдвигает ни один хеш. Именно это изменение отделяет значение, описывающее клиент, от значения, описывающего конкретное сообщение.

Число в префиксе и число в хеше не совпадают. Префикс считает каждое расширение, кроме grease, включая расширение имени домена и расширение согласования протокола, поэтому перехват с семнадцатью расширениями без GREASE даёт 17 в префиксе и хеширует оставшиеся пятнадцать. Хешируемая часть отбрасывает эти два, потому что префикс уже их сообщает, и хеширует оставшийся список вместе с алгоритмами подписи. Читатель, пересчитывающий оба вручную, должен держать два счёта раздельно, и префикс на две единицы ниже обычно означает, что исключение для хеша применили к префиксу.

Соединение HTTP/2 начинается с кадра настроек, и этот кадр является списком записей по шесть байт: двухбайтовый идентификатор и четырёхбайтовое значение. Определено шесть идентификаторов, и конкретный клиент отправляет только часть из них.

settings:chrome_like bytes=24 HEADER_TABLE_SIZE=65536 ENABLE_PUSH=0 INITIAL_WINDOW_SIZE=6291456 MAX_HEADER_LIST_SIZE=262144
settings:node_like bytes=30 HEADER_TABLE_SIZE=4096 ENABLE_PUSH=0 INITIAL_WINDOW_SIZE=65535 MAX_FRAME_SIZE=16384 MAX_CONCURRENT_STREAMS=4294967295
settings_distinct=true

Два клиента, которые оба говорят на HTTP/2, поэтому отправляют заметно разные кадры. Первый объявляет окно 6291456 и предел списка заголовков 262144; второй объявляет окно 65535 и размер кадра 16384. Длина кадра тоже различается, 24 байта против 30, потому что первый отправляет четыре записи, а второй пять.

Направление важно так же, как содержимое. Клиент отправляет один набор и читает другой набор от сервера:

server_sees_client_settings -> headerTableSize=4096 enablePush=true initialWindowSize=65535 maxFrameSize=16384 maxConcurrentStreams=100

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

Переход на TLS 1.3 изменил предмет фингерпринтинга так, что это легко сформулировать и легко прочитать неверно. Почти всё рукопожатие после ClientHello зашифровано, включая сертификат, который отправляет сервер. Поэтому пассивный наблюдатель на пути видит ClientHello открытым текстом, а затем видит шифротекст. Это реальное сокращение того, что узнаёт наблюдатель, и у него есть конкретное следствие: ClientHello стал самым информативным сообщением соединения, потому что только он остаётся читаемым.

Следствие для отпечатка уже, чем кажется. JA3 читает только ClientHello, поэтому TLS 1.3 не удалил его входные данные; он сделал их большей долей общего объёма. JA4 создавался с этим в виду и читает расширение поддерживаемых версий, чтобы клиент TLS 1.3 помечался 13, а не 12, которое предполагает его устаревшее поле. Оба значения описывают первое сообщение, и ни одно не описывает последующий зашифрованный обмен.

Есть второе, менее очевидное следствие. В TLS 1.3 клиент отправляет свой ключевой материал в расширении, поэтому клиент, который угадал предпочтительную группу сервера, может отправить данные на один круговой обход раньше. Набор групп, которые предлагает клиент, и порядок их предложения поэтому отчасти являются решением о производительности, а не только о совместимости, и сборка, которая меняет группу по умолчанию, может изменить близкую к шифрам часть рукопожатия, не меняя ничего, что может прочитать страница. Читатель, сравнивающий два отпечатка при обновлении версии, должен ожидать движения в списке групп и не считать это дрейфом конфигурации.

Соединение через прокси ставит вопрос, какого клиента описывает отпечаток, и ответ определяется тем, где завершается TLS. Когда прокси туннелирует необработанные байты, собственный ClientHello клиента доходит до цели без изменений, и отпечаток принадлежит клиенту. Когда прокси завершает TLS и открывает собственное второе соединение, цель видит ClientHello прокси, и отпечаток принадлежит прокси.

tunnel_mode    -> target reads the client's ClientHello; value describes the client
terminate_mode -> target reads the proxy's ClientHello; value describes the proxy

Так объясняется отчёт, который иначе выглядит невозможным: две машины с разными браузерами дают один отпечаток, или одна машина даёт два разных отпечатка при чередующихся запросах. В первом случае оба браузера выходят через один прокси, который завершает TLS и подставляет собственный стек. Во втором случае запросы идут через разных участников пула, собранных с разными библиотеками TLS. Ошибки измерения здесь нет ни в одном из вариантов.

Практическая проверка это тот же перехват, описанный выше, выполненный на обоих концах. Перехватите у клиента и перехватите то, что получает цель; если они различаются, что-то на пути перестроило рукопожатие, и любой последующий вывод о клиенте теперь опирается на неверное сообщение. Поэтому сравнение отпечатков между двумя сетями должно указывать маршрут, а не только браузер, поскольку маршрут может быть переменной, которая изменилась.

Кадр настроек, изменение между запусками и завершение на прокси

Самое полезное измерение набора то, которое удивляет людей. Один браузер, одна версия, одни настройки профиля, запущенные дважды против одного слушателя, дали два разных значения JA3 и одно одинаковое значение JA4.

run1: bytes=1949 ja3=b488a1aa72f7622264c0b7ecca9dcbf0 ja4=t13d1517h2_8daaf6152771_cb7bf5808d99
run2: bytes=1917 ja3=c88062ec9c84b56a124df18d005b1d9b ja4=t13d1517h2_8daaf6152771_cb7bf5808d99
same_ja3=false
same_ja4=true

Более поздняя пара запусков дала 1917 байт против 1949 и те же два результата: разный JA3 каждый раз и общий JA4. Число байт тоже меняется, потому что значения grease в списке расширений различаются по ширине, поэтому равная длина это совпадение одной пары, а не свойство клиента.

Число байт различается между двумя запусками, и это первая подсказка, что значения grease сдвинулись: Chrome выбирает новый номер grease для списка шифров, списка расширений и списка алгоритмов подписи при каждом соединении, и эти заполнители различаются по ширине. После фильтрации grease лежащие в основе наборы совпадают, а необработанный порядок по-прежнему нет, поэтому отсортированное сравнение проходит, а неотсортированное нет.

run1 ext order: 65037-11-35-0-23-13-5-65281-10-18-16-43-45-51764-17613-27-51
run2 ext order: 10-27-45-5-43-18-51-0-23-35-65037-16-11-17613-51764-65281-13
same_raw_ext_order=false
same_sorted_set=true
same_ja4=true

Отсюда следуют три практических вывода. Изменённый JA3 между двумя запусками одного браузера нормален и не является доказательством неверной настройки, что нужно исключить первым делом, прежде чем исследовать что-то ещё. JA4, остающийся постоянным в этих запусках, описывает клиента, а не сообщение. И инструмент, который сообщает JA3, не указывая, из какого сообщения он получен, зафиксировал факт об одном соединении, а не о браузере.

Другие изменения действительно сдвигают оба значения, и их стоит отделять от переупорядочивания. Обновление библиотеки TLS может добавить или убрать шифр, сборка может добавить расширение, а прокси, завершающий TLS, полностью заменяет рукопожатие, поэтому значение принадлежит прокси, а не браузеру за ним. Каждое из этих изменений меняет отсортированный набор, поэтому сдвинувшийся JA4 это событие другого рода, чем сдвинувшийся JA3.

Проверка собственного клиента и почему отпечаток не является вердиктом

Безопасный способ узнать, что отправляет собственный клиент, это прочитать рукопожатие, которым вы владеете. Локального слушателя, который принимает соединение и печатает первую запись, достаточно, и сертификат ему не нужен, потому что рукопожатие никогда не завершается.

import socket
import struct

server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 8443))
server.listen(1)

# Point the client you want to measure at https://localhost:8443 and accept its hello.
connection, _ = server.accept()
record = connection.recv(8192)
connection.close()
server.close()

length = struct.unpack(">H", record[3:5])[0]
print("record_type", record[0], "record_length", length, "captured", len(record))
print("first_bytes", record[5:9].hex())

Ожидайте тип записи 22, то есть запись рукопожатия, и длину, близкую к числу перехваченных байт. Четыре байта после заголовка записи это тип рукопожатия и трёхбайтовая длина. Перехват, который ничего не возвращает, обычно означает, что клиент попробовал адрес, потерпел неудачу и продолжил, что случается, когда имя разрешается в адрес, где вы не слушаете.

Два правила делают перехват пригодным. Направьте клиент на собственный слушатель, а не на сайт, которым вы не управляете, потому что перехват чужой конечной точки измеряет их соединение, а не ваш клиент. И перехватывайте больше одного раза, потому что одно сообщение не скажет, устойчиво ли значение; сравнение между запусками выше это та часть, которая превращает одно число в утверждение.

Описанные здесь значения описательны, и разрыв между описанием клиента и его оценкой это место, где происходит большинство неверных прочтений. JA4, совпадающий с известным браузером, означает, что рукопожатие согласуется со стеком этого браузера. Это не означает, что сеанс легитимен, и несовпадающее значение не означает, что сеанс автоматизирован. Сборка из того же исходного кода с другой библиотекой TLS, корпоративное промежуточное устройство, переписывающее рукопожатие, и браузер за прокси, завершающим TLS, дают расхождение без какого-либо присутствия автоматизации.

Поэтому значение уровня соединения обычно читают вместе с другими свидетельствами, а не отдельно. Одно значение несёт один факт: эти параметры рукопожатия были предложены в этом соединении. Превращение этого факта в утверждение о сеансе требует второго наблюдения с другого уровня, и утверждение настолько сильно, насколько согласуются эти два. Страница, которая сообщает одно окружение, и рукопожатие, которое сообщает другое, это интересный случай, и он интересен именно потому, что одно из двух измеримо неверно.

Для того кто эксплуатирует парк браузеров, полезная привычка это записывать значение вместе с маршрутом и отметкой времени, а не изолированно. Сохранённый отпечаток без маршрута не может ответить на вопрос, который задаст более поздний читатель: изменился клиент или путь. Измеренная пара в предыдущем разделе это наименьшая версия такой записи: два запуска, один маршрут, одно значение сдвинулось, другое нет. Если добавить префикс t13d1517h2 и длину перехвата, строку сможет проверить другой читатель, не повторяя измерение.

Практический вывод

Читайте JA3 как факт об одном сообщении, а JA4 как факт о клиенте, и запутанные отчёты перестанут быть запутанными. Порядок расширений, который хеширует JA3, перемешивается клиентом между соединениями, поэтому изменённый JA3 при неизменном JA4 это ожидаемый результат, а не сбой. Рукопожатие TLS 1.3 сообщает legacy_version 0x0303 намеренно, поэтому версия, прочитанная только из этого поля, занижает протокол. А кадр настроек описывает то, что клиент объявил одной стороне, причём пропущенные идентификаторы остаются на значениях по умолчанию, поэтому два клиента HTTP/2 могут выглядеть по-разному, оставаясь корректными. Какие правила обнаружения нуждаются в значении из трафика показывает, где эти факты входят в решение, а фингерпринтинг сетевой информации охватывает соседние факты, которые страница получает сама.

Ничто из этого не делает профиль на стороне страницы ответственным за рукопожатие. Объявленный профиль может поддерживать согласованность читаемых поверхностей Клиент, работающий с этой сборкой без браузера, разобран в согласовании TLS у curl-impersonate и curl_cffi. между собой, а рукопожатие создаётся под ними стеком TLS, поэтому согласованный документ и устойчивое рукопожатие это два отдельных результата, полученных в двух отдельных местах. Принятие первого за доказательство второго и есть ошибка, превращающая два отдельных результата в один ложный вывод.

Источники

#TLS#JA3#JA4#HTTP2#fingerprinting

Эта статья входит в руководство: Что такое отпечаток браузера?

Выполните ту же проверку в BotBrowser

Загрузите профиль, сохраните внутри браузера те сигналы, о которых только что прочитали, и подтвердите результат на своей машине до раскатки.