Отладка защищенных по SSL/TLS интеграций у Java приложений порой становится весьма нетривиальной задачей: соединение не ставится/рвется, а прикладные логи могут оказаться скудными, доступа к правке исходных кодов может не быть, перехват трафика Wireshark'ом и попытка дешифрации приватным ключом сервера (даже если он есть) может провалиться, если в канале применялся шифр с PFS; прокси-сервер вроде Fiddler или Burp может не подойти, так как приложение не умеет ходить через прокси или на отрез отказывается верить подсунутому ему сертификату…
Недавно на Хабре появилась публикация от ValdikSS о том, как можно с помощью Wireshark расшифровать любой трафик от браузеров Firefox и Chrome без обладания приватным ключом сервера, без подмены сертификатов и без прокси. Она натолкнула автора нынешней статьи на мысль — можно ли применить такой подход к Java приложениям, использовав вместо файла сессионных ключей отладочные записи JVM? Оказалось — можно, и сегодня, уважаемые однохабряне, я расскажу, как это сделать.
Идея рецепта
С недавних версий браузеры Firefox и Chrome научились выводить в специально задаваемый файлик данные, достаточные для деривации (получения) сессионных ключей, которыми шифруется передаваемый ими трафик (равно как и принимаемый трафик, поскольку внутри SSL/TLS используется симметричное шифрование). Строго говоря, делают это не сами браузеры, а библиотечка NSS в их составе; именно она задает формат записываемых файлов. Этот формат умеет читаться и использоваться Wireshark'ом, чтобы расшифровывать SSL-записи, зашифрованные соответствующими ключами. Идея нашего «блюда» заключается в том, чтобы с той же целью научиться формировать подобные файлы самостоятельно для Java-приложений, опираясь в качестве источника на отладочные логи, которые пишутся в стандартный вывод при наличии JVM-опции javax.net.debug.
Ингредиенты
Нам понадобится:
- Java-приложение, для которого мы можем задавать параметры запуска (опции JVM).
Для определенности полагаем, что приложение при установлении соединения выступает в роли клиента.
Версию JDK (JRE) желательно иметь 1.6 или 1.7, на других (пока) не проверялось; - Wireshark версии 1.6.0 или выше;
- Текстовый редактор, например, Notepad++;
- Щепотка терпения, внимательности и времени.
Замес
Параметры запуска
Поскольку одним из главных источников информации для нас будут являться логи, перво-наперво нужно правильно настроить их получение. Вполне рабочим вариантом будет опция JVM javax.net.debug=ssl:handshake:data. Сразу оговоримся, что именно такое значение она иметь не обязана, можно (наверно) обойтись и универсальным javax.net.debug=all, но работать с результатами такого выбора может быть сложновато (объем логов может оказаться гигантским). Наш же выбор объясняется следующим:
- ssl — чтобы в лог писались сообщения, касающиеся только SSL;
- handshake — чтобы видеть каждое сообщение в рамках главного для нас этапа — handshake;
- data — для ленивых, чтобы вручную не переводить некоторые значения из десятеричной системы счисления в шестнадцатеричную (hex);
Наличие такой опции должно обеспечить нам вывод в лог (или стандартный вывод) отладочной информации, к которой мы вернемся чуть позже.
Настройка снифера
Снюхивать трафик целевого приложения Wireshark'ом можно лишь после простановки указанной выше опции, так как в противном случае мы не будем обладать сессионными ключами. К тому же надо помнить, что ключи «эфемерны» — они пригодны лишь для одной SSL-сессии, то есть лог от одного сеанса связи не подойдет для дешифрации трафика другого сеанса. Ну и чтобы дышалось совсем легко, рекомендую сразу при старте снифера указать хост, с которым планируется обмен данными; это позволит еще «на берегу» отбросить ненужные пакеты, проходящие через прослушиваемый сетевой интерфейс:
Съём и запись
После задания нужных параметров запуска приложения и настроек снифера, можно стартовать само приложение и включать захват пакетов в Wireshark. Тогда при попытке приложения выйти на (защищенную) связь с каким-либо сервером наши «кастрюльки» начнут заполняться. С точки зрения Wireshark это будет выглядеть примерно так:
Как видно, Wireshark явно определяет некоторые SSL-записи как зашифрованные; такими же являются и записи с типом Application Data.
А с позиций стандартного вывода (логов) приложения — примерно так:
...
*** ClientHello, TLSv1
RandomCookie: GMT: 1427238714 bytes = { 246, 5, 6, 214, 168, 159, ... , 140, 141, 50, 196 }
Session ID: {}
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5, SSL_RSA_WITH_RC4_128_SHA, ..., SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA]
Compression Methods: { 0 }
...
На самом деле в лог будет выведено еще много всякого (формат отладочного вывода нигде не специфицирован и меняется от версии Java к версии), но нам пока достаточно увидеть хотя бы это.
«Расстойка»
Обладая отладочными записями от сеанса связи по SSL/TLS, мы можем сформировать файл сессионных ключей в формате NSS. Для этого нам в первую очередь потребуется определить, какой метод распространения сессионных ключей использовался в нашем сеансе связи: метод обмена (aka RSA) или метод генерации (aka DH или PFS, хоть это и разные вещи). В чем их суть и отличия можно почитать в замечательной работе Sally Vandeven'а. Нам же достаточно знать лишь сам метод, а определить его можно, как минимум, двумя путями:
- По названию шифра, выведенному в лог или определенному снифером. Например, по такому выводу
видно, что шифр не имеет в названии никаких упоминаний DH, зато явно называет себя RSA. Однако такая ясность присутствует далеко не всегда, поэтому можно воспользоваться планом Б:*** ServerHello, TLSv1 RandomCookie: GMT: 1037995915 bytes = { 168, 183, ... 204, 178 } Session ID: {141, 155, ... 214, 36} Cipher Suite: SSL_RSA_WITH_RC4_128_SHA ...
- По наличию SSL-сообщения ServerKeyExchange в перехвате трафика в Wireshark (см. скриншот в подразделе «Съём и запись») — оно присутствует для методов DH и отсутствует для RSA (объяснение почему — за рамками данной статьи). Безусловно, наличие этого сообщения можно определить и по тем же логам.
Определив метод распространения сессионных ключей, обратимся к описанию формата файлов NSS, который предписывает нам различать строки каждого файла как раз по этим двум методам. Рассмотрим каждый из них по-подробнее.
Для начала предположим, что в нашем сеансе связи использовался какой-либо шифр на основе RSA. Согласно описанию формата, соответствующая строка файла должна начинаться с текста RSA, за которым через пробел должны последовать 16 байт HEX-кодированного шифрованного ключа PreMasterSecret, а еще через пробел — 96 байт HEX-кодированного нешифрованного ключа PreMasterSecret (т.е. его же). Этот ключ является основой для генерации главного ключа — MasterSecret и передается от клиента к серверу в сообщении ClientKeyExchange, будучи зашифрованным публичным ключом сервера. Это значит, что первую часть строки (шифрованное представление этого ключа) должно быть видно в Wireshark. Находим нужное сообщение и убеждаемся — да, он есть:
Заметка для буквоедов
Опытный хабровед вправе прервать повествование вопросом — почему в формате файла NSS требуется лишь 16 байт, в то время как длина зашифрованного ключа составляет 256 байт?
Это объясняется тем, что шифрованное значение используется Wireshark'ом просто как индекс — оно нужно лишь для того, чтобы из потенциального множества строк в NSS-файле найти именно ту запись, в которой содержится подходящий MasterSecret. Сделать это можно путем последовательного сопоставления шифрованной версии этого ключа (взятой из перехваченного трафика) с первым (после «RSA») элементом каждой строки файла. Собственно, это Wireshark и делает, и сопоставлять ключ по всей длине в этом случае вовсе необязательно, вполне хватает и 16-ти байт.
Это объясняется тем, что шифрованное значение используется Wireshark'ом просто как индекс — оно нужно лишь для того, чтобы из потенциального множества строк в NSS-файле найти именно ту запись, в которой содержится подходящий MasterSecret. Сделать это можно путем последовательного сопоставления шифрованной версии этого ключа (взятой из перехваченного трафика) с первым (после «RSA») элементом каждой строки файла. Собственно, это Wireshark и делает, и сопоставлять ключ по всей длине в этом случае вовсе необязательно, вполне хватает и 16-ти байт.
К слову, это же значение можно получить и из логов приложения, и здесь нам как раз пригодится подзначение JVM-опции ":data":
Найденное значение (16 байт) можно вставлять в формируемый NSS-файл. Теперь провернем аналогичную операцию для второго элемента строки — собственного значения ключа PreMasterSecret. Поскольку оно, очевидно, никогда не передается по сети в открытом виде (собственно, поэтому оно и называется ...Secret), выуживать его придется только из логов. Благо, с недвусмысленными подсказками от JVM сделать это не особо сложно:
Теперь нужно добавить это значение к формируемой нами строке NSS-файла и «причесать» строку так, чтобы получилось что-то вроде этого (комментарии в стандартной нотации вполне допустимы):
# SSL/TLS secrets log file, generated by Toparvion
RSA 75ff866e23beca1c 03012aede74befa88233253e3207bb1320935ab206696512674df5c6dee7dfaa2156932bc559631c8f3bb46ae38a71ff
Тем, для кого RSA — «как раз тот случай» (как правило, это приложения до Java 7), уже можно переходить к разделу «Дегустация». Тем же, кому довелось столкнуться с PFS (зачастую это Java 7 и выше), придется читать дальше…
Аналогично методу RSA, строка для дешифрации записей на основе PSF должна состоять из трех элементов, разделенных пробелом:
- Тип записи; в данном случае он должен быть равен CLIENT_RANDOM;
- 64 байт HEX-кодированного клиентского случайного числа Random;
- 96 байт HEX-кодированного главного ключа MasterSecret;
Источники данных для второго и третьего элемента также аналогичны предыдущему методу, но есть некоторые тонкости. Клиентское случайное число является конкатенацией текущего времени в миллисекундах по GMT и собственно случайного значения. В случае обращения к Wireshark это отчетливо видно:
А вот при обращении к логам легко ошибиться, но можно пользоваться вот такой подсказкой:
Здесь также вхождение ":data" в значении JVM-опции javax.net.debug избавляет нас от необходимости ручной конвертации систем счисления. Всего нам потребуется 64 байта случайного числа, то все оно целиком (а не только начало, как было с RSA). Оно будет также играть роль индекса при поиске Wireshark'ом подходящей записи в NSS-файле.
Третий элемент строки — главный секретный ключ MasterSecret — также может быть извлечен из логов приложения:
После извлечения главного ключа из логов, дополняем им формируемую строку, «причесываем» и получаем нечто вроде:
# SSL/TLS secrets log file, generated by Toparvion
CLIENT_RANDOM 551435582740bdc1386b20b7fcb51428fe3042e06c8e6e94c910786f577a2ada 976dc1d54dd74d3c2e715109c8a4fb8e743efc084614abc0e12fdb78e472c30e3590ac5eb383424b2d8fa3de84c8b0f5
Нельзя не заметить, что Wireshark весьма чувствителен к формату NSS-файла, поэтому лучше уже сейчас тщательно перепроверить, сходится ли число байт в каждом элементе строки и нет ли где-либо лишних пробелов; это может сэкономить время в будущем.
Дегустация
Теперь, когда все «ручные» шаги выполнены, пора дать слово Wireshark'у — преподносим ему созданный файл в точности также, как это описано в упомянутой в начале статье:
- Открываем в Wireshark контекстное меню на любом SSL/TLS пакете;
- Выбираем Protocol Preferences -> Secure Socket Layer Preferences...;
- В открывшемся окне в графе (Pre-)Master Secret log filnename указываем путь к сформированному нами NSS-файлу.
Нажимаем OK и смотрим на изменения в перехваченном трафике:
Если дешифрация выполнена успешно, то пакеты, ранее имевшие в названиях слово Encrypted, обретут конкретные имена. Именно так стало с командами Finished, приведенными на рисунке выше.
Кроме того (и это, пожалуй, самый приятный момент) теперь можно выбрать любой пакет с протоколом SSL или TLS и в его контекстном меню щелкнуть на Follow SSL Stream — результат не должен требовать комментариев:
Как видно, несмотря на обращение по HTTPS, мы видим передаваемый трафик и можем экспортировать его для дальнейшего анализа.
Если все же не получилось...
Ошибок на этом скользком пути можно сделать множество. Одним из самых ценных источников информации является лог самого Wireshark'а — он ведется в том случае, если будет указан путь к файлу лога в графе SSL debug file все в том же окне настроек SSL.
Важно отметить, что никакой фильтрации на лог не предусмотрено, а Wireshark весьма подробен, поэтому если держать лог постоянно включенным, он, во-первых, очень быстро вырастет, во-вторых, может привести к торможениям самого Wireshark'а (т.к., видимо, пишется синхронно). В связи с этим, лучше всего указывать файл лога лишь перед самым применением NSS-файла, а по окончании анализа — чистить его (но не удалять).
Заключение
В статье был рассмотрен еще один подход к дешифрации SSL/TLS трафика Java приложений с целью их отладки.
В приведенном виде подход едва ли применим на практике, так как требует существенных затрат времени и наличия определенных знаний и навыков у исполнителя. Однако представленное описание позволит формализовать, а значит, и автоматизировать (запрограммировать) этот подход и, таким образом, поставить его на службу людям. Такая работа уже начата автором. Если эта затея интересна и вам — милости просим! Не забудьте рассказать об этом на Хабре.
Спасибо за чтение!
Update
Дорогие однохабряне, для тех, кому интересен описанный в статье подход, но
Синтаксис ее запуска прост, нужно указать лишь исходный файл лога JVM:
java -jar nssjavamaker.jar some/directory/java-ssl-debug.log
На выходе в текущей директории будет создан NSS-файл session-keys.nss, готовый для импорта в Wireshark. Для изменения этого пути и других параметров обратитесь к файлу Readme или запустите утилиту совсем без параметров.
Готовый для запуска JAR можно скачать на странице с последней версией.
Пожелания/предложения/комментарии/замечания по утилите приветствуются в разделе заявок на странице проекта, а также по адресу toparvion@gmx.com. Успехов в работе!