В это статье я хочу рассказать как я вскрыл несколько частей WhatsApp VoIP протокола с помощью джейлбрейкнутого iOS устройства и набора разных программ для анализа.
В последнее время Whatsapp привлекает к себе большое внимание из за найденных уязвимостей и возможностей для хакеров.
С этой точки зрения он очень интересен для исследования его безопасности.
Всем кому это тоже интересно, добро пожаловать под кат.
Хотя на официальных страницах Whatsapp есть описание его шифрования, но на самом деле нигде нет подробной информации о том как оно работает и как оно имплементировано в протокол.
Следовательно, нет основы для подробной аналитики безопасности самого Whatsapp.
Мое исследование базировалось на трех вещах:
1. Аналитика сетевого трафика
2. Аналика бинарников
3. Аналитика поведения приложения в разных режимах
Инструментарий
Для анализа клиента Wahtsapp под iOS я использовал следующие инструменты:
— Декриптор бинарников – bfdecrypt
— Дизассемблер бинарных файлов — Hopper Disassembler и radare2
— Анализ сетевого трафика — Wireshark
— Аналитика действий в приложении – Frida
Как я установил джелбрейк на iOS не рассматривается в рамках этой статьи.
Анализируем сетевой трафик
В этой части будем анализировать сетевой трафик Whatsapp клиента во время звонка, который будем записывать с помощью Wireshark.
Для записи такого трафика создал remote virtual network interface.
Команда для Макоси выглядит следующим образом:
rvictl -s Здесь device UUID нужно заменить на UUID устройства с клиентом вацап.
Wireshark обнаруживает использование Session Traversal Utilities for NAT (STUN).
STUN – это протокол сигнализации который необходим для установления peer-to-peer соединения между клиентами.
Здесь WhatsApp клиент использует TCP пакеты для обмена с разными серверами вацапа.
В то же время UDP пакеты используются для обмена между клиентами.
Сотни UDP пакетов проходят за минуту.
Вацап использует Secure Real Time Protocol (SRTP) и очевидно что эти UDP пакеты как раз и содержат SRTP данные о звонке.
SRTP протокол обеспечивает шифрование, аутенификацию и защиту от атак повторного воспроизведения на RTP трафик.
Давайте подробнее рассмотрим SRTP пакеты, которыми обменивается А и Б сторона.
Для этого преобразуем их в шестнадцатеричный вид:
Видно что поля содержат заголовки RTP характерные для SRTP.
Первые четыре байта (выделены красным) — 7 RTP заголовочных поля.
Рассмотрим их более подробно:
0x8078001e =0b10_0_0_0000_0_111100_00000000000011110 =V=10|P=0|X=0|CC=0000|M=0|PT=111100|SEQ=00000000000011110
Первые 2 бита содержат номер версии (V), в нашем случае это вторая версия.
Третий бит – поле для необязательной информации, в нашем случае оно пустое.
Четвертый бит – поле расширения (X) указывает на то что в данном случае нет других заголовков после заголовка RTP пакета.
Биты с 5 по 8 – ссодержат количество CSRC-идентификаторов, следующих за постоянным заголовком.
CSRC (contributing source) это источник потока RTP-пакетов, который вносит вклад в общий поток, формируемый RTP-смесителем. Смеситель вставляет список SSRC-идентификаторов, которые идентифицируют парциальные источники, в заголовок RTP-пакетов. Этот список называется CSRC-списком. Например — аудио-конференция, где смеситель отмечает всех говорящих, чей голос порождает исходящие пакеты. Это позволяет принимающей стороне идентифицировать говорящего, хотя все пакеты имеют один и тот же SSRC-идентификатор.
8 бит – это маркер бит (M). Используется на уровне приложения и определяется профилем. Если это поле установлено, то данные пакета имеют какое-то особое значение для приложения.
Следующие 6 битов — дополнительные коды типа данных. Эти данные не определены в стандартах RTP и SRTP. Значение этих битов скорее всего – кастомное, выбранное Whatsapp.
Последние 17 битов указывает источник синхронизации. Номер по порядку инкрементируется на 1 при посылке очередного RTP-пакета данных, этот код может использоваться получателем для регистрации потерь пакетов и для восстановления истинного порядка присланных фрагментов. По стандарту начальное значение кода является случайным, но эта рекомендация не выполняется вацапом, потому что как мы видим из данных Wireshark начальное значение у вацапа всегда равно 0.
Следующие 4 байта (выделено синим) – метка времени пакета.
4 байта после этого (зеленые) — Поле SSRC. Оно идентифицирует источник синхронизации. Этот идентификатор выбирается случайным образом, так чтобы в пределах одной RTP-сессии не было двух равных SSRC-кодов. Все приложения должны быть способны выявлять случаи равенства SSRC-кодов. Если отправитель изменяет свой транспортный адрес, он должен также сменить и SSRC-идентификатор.
Итак, мы выяснили что Whatsapp использует протокол SRTP для защиты звонков.
Это подтверждается структурой UDP пакетов, которыми обмениваются клиенты вацапа.
Также вацап использует TCP протокол для обмена данными между клиентом и сервером.
Ниже мы увидим как для шифрования этой части используется Noise Pipes Protocol
Аналитика бинарников
Вацап клиент для iOS содержит 2 главных бинарных файла — WhatsApp application binary и WhatsApp core framework.
В этой части мы рассмотрим их подробнее с помощью Hopper Disassembler and radare2.
Эти бинарники зашифрованы при скачивании их с Appstore.
Здесь мы обманули Apple с помощью джейлбрейка устройства на iOS и получили доступ к этим файлам.
Также надо добавить что эти бинарники Whatsapp были расшифрованы с помощью bfdecrypt.
Далее я покажу как я собирал информацию об основах протоколов, алгоритмах и опенсурсных библиотеках, которые использует Whatsapp.
Опенсорсные библиотеки особенно интересны, потому что они могут быть легко проанализированы.
libsignal-protocol-c
Вацап использует libsignal-protocol-c – опенсурсную библиотеку — которую он имплементировал в Signal Protocol.
Протокол базируется на Double Ratchet Algorithm, который занимается шифрованием вацап сообщений.
Эта библиотека была обнаружена в бинарниках Whatsapp по следующим характерным признакам:
r2 WhatsAppCore
[0x0082b517]> / _signal_
Searching 8 bytes in [0x0-0x654000]
hits: 33
0x00837a7b hit2_0 .il_key_data_from_signal_keydispatch_.
0x0083df33 hit2_1 ._torlice_signal_protocol_paramet.
0x008407c0 hit2_2 .d_fac_3key_signal_message_big.
0x00840d50 hit2_3 .mmetric_signal_protocol_paramet.
0x00840e70 hit2_4 .ob_signal_protocol_paramet.
0x00841492 hit2_5 .pre_key_signal_messagesigna.
0x008de24b hit2_6 .agc_reset_alice_signal_protocol_paramet.
0x008de274 hit2_7 .rs_create_alice_signal_protocol_paramet.
0x008de440 hit2_8 .bitno_MRDTX_bob_signal_protocol_paramet.
0x008de467 hit2_9 .ters_create_bob_signal_protocol_paramet.
0x008e311c hit2_10 .pre_big_pre_key_signal_message_copy_pr.
0x008e3139 hit2_11 .ge_copy_pre_key_signal_message_create_.
0x008e3158 hit2_12 ._create_pre_key_signal_message_deserial.
0x008e317c hit2_13 .rialize_pre_key_signal_message_destroy.libsrtp
libsrtp
Вацап также использует libsrtp для имплементации своего Secure Real Time Protocol.
Имена символов были удалены из бинарных файлов приложения Whatsapp, но несмотря на это бинарники содержат строки, которые прямо указывают на их связь с libsrtp:
r2 WhatsApp
[0x1001ada34]> / libsrtp
0x100ee5546 hit1_0 .rc %08XUnknown libsrtp error %duns.
0x100ee57eb hit1_1 .d to initialize libsrtp: %sFailed to r.
0x100ee580a hit1_2 .led to register libsrtp deinit.Failed.
0x100ee5831 hit1_3 .to deinitialize libsrtp: %sAES_CM_128_.
0x100ee5883 hit1_4 .ck crypto Init libsrtp. create pool..
0x100f07b80 hit1_5. packet: %slibsrtpstat test%s: c.
Более того, бинарники вацапа содержат строки которые используются в сырцах кода libsrtp, например “cloning stream (SSRC: 0x%08x)”:
r2 WhatsApp
[0x1013ddb4f]> / cloning stream
Searching 14 bytes in [0x100000000-0x100fb4000]
hits: 1
0x100f07823 hit7_0 .sent!srtp%s: cloning stream (SSRC: 0x%08x).
PJSIP
Также вацап использует PJSIP, в котором имплементированы мультимедия коммуникации, сигнализация и кодирование аудио и видео данных.
Также там имплементирован STUN, что хорошо видно при анализе с помощью Wireshark.
Библиотека была PJSIP идентифицирована по строкам в бинарниках вацапа из за информации о дебагах в PJSIP:
r2 WhatsApp
[0x1013ddb4f]> / pjmedia
Searching 7 bytes in [0x100000000-0x100fb4000]
hits: 180
0x100edd55f hit9_0 .io_piggyback.ccpjmedia_audio_piggyback.
0x100edd591 hit9_1 .r %d, stream %ppjmedia_audio_piggyback.
0x100edd5d4 hit9_2 .d, tx_packet %dpjmedia_audio_piggyback.
0x100edd601 hit9_3 .ideo_enabled %dpjmedia_audio_piggyback.
0x100eddcf3 hit9_4 .ibyuv converterpjmedia_converter_creat.
0x100eddd21 hit9_5 .rter count = %dpjmedia_converter_creat.
0x100ede3e3 hit9_6 .rame, status=%dpjmedia_delay_buf_get_s.
0x100ede46e hit9_7 .%sec_delay_bufpjmedia_echo_create2: %.
0x100ede64d hit9_8 .eUnknown pjmedia-videodev error.
0x100ede90c hit9_9 .o errorUnknown pjmedia-audiodev error.
0x100edebba hit9_10 .ATENCY)Unknown pjmedia error %dUnspec.
0x100ee027e hit9_11 .queue.format.cpjmedia_format_get_vide.
0x100ee02ca hit9_12 .mat info for %dpjmedia_format_get_vide.
0x100ee1446 hit9_13 .c_buf too shortpjmedia_h26x_packetize .
mbed TLS
Также вацап использует опенсурсный mbed TLS для имплементации своего TLS protocol.
Эта библиотека в коде вацапа была идентифицирована по следующим именам функций:
r2 WhatsAppCore
[0x0082b517]> / mbedtls
Searching 7 bytes in [0x814000-0x934000]
hits: 41
0x008e299b hit5_0 .TLSErrorDomain_mbedtls_aes_crypt_cbc_.
0x008e29b2 hit5_1 ._aes_crypt_cbc_mbedtls_aes_crypt_cfb12.
0x008e29cc hit5_2 .s_crypt_cfb128_mbedtls_aes_crypt_cfb8.
0x008e29e4 hit5_3 .aes_crypt_cfb8_mbedtls_aes_crypt_ctr_.
0x008e29fb hit5_4 ._aes_crypt_ctr_mbedtls_aes_crypt_ecb_.
0x008e2a12 hit5_5 ._aes_crypt_ecb_mbedtls_aes_decrypt_mb.
0x008e2a27 hit5_6 .ls_aes_decrypt_mbedtls_aes_encrypt_mb.
0x008e2a3c hit5_7 .ls_aes_encrypt_mbedtls_aes_free_mbedt.
0x008e2a4e hit5_8 .edtls_aes_free_mbedtls_aes_init_mbedt.
0x008e2a60 hit5_9 .edtls_aes_init_mbedtls_aes_setkey_dec.
0x008e2a78 hit5_10 .aes_setkey_dec_mbedtls_aes_setkey_enc.
0x008e2a90 hit5_11 .aes_setkey_enc_mbedtls_cipher_auth_dec.
0x008e2aad hit5_12 .r_auth_decrypt_mbedtls_cipher_auth_enc.
0x008e2aca hit5_13 .r_auth_encrypt_mbedtls_cipher_check_ta.
…
XMPP
Вацап также использует открытый протокол Extensible Messaging and Presence Protocol (XMPP) для обмена асинхронными сообщениями между клиентами.
Это было обнаружено по именам классов в коде вацап, используемых в XMPP:
r2 WhatsApp
[0x1013ddb4f]> / XMPP
Searching 4 bytes in [0x1013ac000-0x1014b4000]
hits: 150
Searching 4 bytes in [0x100fb4000-0x1013ac000]
hits: 150
Searching 4 bytes in [0x100000000-0x100fb4000]
hits: 396
0x1013d05b5 hit12_0 .@_OBJC_CLASS_$_XMPPAckStanza@_.
0x1013d05d6 hit12_1 .@_OBJC_CLASS_$_XMPPBinaryCoder.
0x1013d05fa hit12_2 .@_OBJC_CLASS_$_XMPPCallStanza.
0x1013d0624 hit12_3 .@_OBJC_CLASS_$_XMPPChatStateStanza.
0x1013d064b hit12_4 .@_OBJC_CLASS_$_XMPPConnection.
0x1013d0679 hit12_5 .@_OBJC_CLASS_$_XMPPError.
0x1013d069e hit12_6 .@_OBJC_CLASS_$_XMPPGDPRDeleteReport.
0x1013d06cd hit12_7 .@_OBJC_CLASS_$_XMPPGDPRGetReportSta.
0x1013d0707 hit12_8 .@_OBJC_CLASS_$_XMPPGDPRRequestRepor.
0x1013d0736 hit12_9 .@_OBJC_CLASS_$_XMPPIQStanza.
0x1013d0762 hit12_10 .@_OBJC_CLASS_$_XMPPMessageStanza.
0x1013d0787 hit12_11 .@_OBJC_CLASS_$_XMPPMessageStatusCha.
0x1013d07b9 hit12_12 .@_OBJC_CLASS_$_XMPPMultiReceipt.
0x1013d07dc hit12_13 .@_OBJC_CLASS_$_XMPPNotificationStan.
...
Noise Protocol Framework
Согласно официальным сообщениям вацап, он использует Noise Protocol Framework для безопасного коммуницирования клиентов с серверами.
Noise Protocol Framework был разработан для создания easy-to-use криптографических протоколов с помощью набора отдельных блоков.
Но строго говря, вацап использует только Noise Pipes Protocol, который был взят из более полного Noise Protocol Framework.
Вот такие строки были найдены в бинарниках вацапа:
“Noise_XX_25519_AESGCM_SHA256”,
• “Noise_IK_25519_AESGCM_SHA256”,
• “Noise_XXfallback_25519_AESGCM_SHA256”.
Эти строки содержат хендшейк паттерны имплементированные в вацап клиентах.
Первая строка относится к классу WANoiseFullHandshake.
Вторая – к WANoiseResumeHandshake и последняя к WANoiseFallbackHandshak.
Подробно как работает этот протокол мы не будем рассматривать в рамках этой статьи.
Runtime Analysis
В этой части мы будем исследовать поведение вацап клиента с помощью Frida.
Frida – это так называемый Dinamic Instrumentation Toolkit, то есть набор инструментов, позволяющих на лету внедрять собственный код в другие приложения.
Мы будем подключаться к процессу в приложениии и менять его поведение, используя интерактивную JS-консоль.
Key Transport
В этой части исследуем ключевые механизмы работы вацап протокола.
Согласно официальному описанию от Whatsapp, описывающего шифрование VOIP звонка – инициатор звонка генерирует случайный 32 байтный SRTP мастер секрет.
Затем происходит передача шифрованного сообщения к стороне Б с содержанием этого SRTP мастер секрета.
Затем эта информация используется для реконструкции на стороне Б.
Сначала я сделал трейс, используя слово “secret”:
frida-trace -U WhatsApp -m "*[* *Secret*]" -m "*[* *secret*]"
После инициирования вацап звонка вызываем метод deriveSecretsFromInputKeyMaterial с помощью класса WAHKDF:
+[WAHKDF
deriveSecretsFromInputKeyMaterial: 0x121e08a20
salt: 0x0
info: 0x121e07840
outputLength: 0x2e
withMessageVersion: 0x3
]
Входящие значения 0x121e08a20 и 0x121e07840 указывают на Objective-C объекты.
Frida позволяет создавать proxy Objective-C objects для JavaScript.
Хук deriveSecretsFromInputKeyMaterial использован для принтинга описания объектов:
{
onEnter: function (log, args, state) {
log("+[ WAHKDF deriveSecretsFromInputKeyMaterial: " +
ObjC.Object( args[2] ).toString() + "\n" +
" salt: " + ObjC.Object( args[3] ).toString() + "\n" +
" info: " + ObjC.Object( args[4] ).toString() + "\n" +
" bytes: " + args[5].toInt32 () + "\n" +
" withMessageVersion: " + args[6].toInt32 () + "\n]");
}
}
После выполнения скрипта:
+[WAHKDF deriveSecretsFromInputKeyMaterial: <09a38e76 fe90e4f1 26ed66d0 5a6783ba d48776b6 1daaf7c9 39c005ea 2d8ccdf6>
salt: nil
info: <34393135 39303537 37313632 3040732e 77686174 73617070 2e6e6574>
bytes: 46
withMessageVersion: 3
]
Первый и третий параметры похожи на NSData объекты, которые содержат статический байтовый буфер.
Первый параметр имеет длину 32 байта, как и описано в WhatsApp white paper.
Третий параметр — ASCII строка, содержащая JID инициатора звонка.
В дальнейшем мы увидим что действительно, именно первая строка и содержит мастер секрет.
Шифрование мастер секрета
Согласно WhatsApp white paper, мастер секрет — необходимая часть для защиты call session.
Поэтому он должен быть безопасно доставлен на сторону Б.
Для исследования того как происходит такая доставка, я сделал трейс, содержащий ключевые слова, релевантные для процесса шифрования:
frida-trace -U WhatsApp -m "*[* *crypt*]" -i "*crypt*"
После инициации звонка вызываем функцию signal_encrypt из библиотеки libsignal-protocol-c.
Заголовок signal_encrypt:
plaintext читаем с помощью хука от Frida:
Первые 4 байта испольуются для сериализации мастер секрета с помощью protocol buffers от Google.
(Сериализация — процесс перевода какой-либо структуры данных в последовательность битов.)
Следующие байты — сам мастер секрет.
Последние 13 байт — encryption padding.
Плайнтекст зашифрован с помощью AES-256 в CBC режиме.
Ключи шифрования получены с помощью Double Ratchet Algorithm, который является частью Signal Protocol.
Работа libsignal-protocol-c и Signal Protocol не рассматриваются в этой статье.
Результат выполнения signal_encrypt:
Результат содержит больше байт, т. к. к сообщению добавлен authentication tag, которое использует HMAC-SHA256.
Мы рассмотрели первую часть WhatsApp VoIP протокола.
Подведем промежуточные итоги — мастер секрет сериализован и зашифрован с помощью 256-битного AES ключа в режиме CBC.
Ключ шифрования и аутенификационный ключ получены с помощью опенсурсной библиотеки libsignal-protocol-c.
Разбираем мастер секрет
Давайте посмотрим как происходит шифрование мастер секрета.
Делаем трейс с ключевым словом signal:
frida-trace -U WhatsApp -i “*signal*”
Frida показывает что в шифровании мастер секрета участвует функция textsecure__signal_message__pack.
Функция создает Signal message, содержащее зашифрованный мастер секрет и другие необходимые параметры:
Байты, выделенные зеленым, используется для сериализации.
Синии байты — рэтчет ключ отправителя (используется для сквозного шифрования).
Счетчик сообщений — оранжевые байты.
Наконец зеленым выделены байты мастер секрета.
При трассировке XMPP мы можем увидеть что вызывается метод writeNoiseFrameToSocketWithPayload из класса XMPPStream.
Этот метод шлет XMPP сообщения, зашифрованные Noise Pipes Protocol, используя TCP проктокол, к серверу вацапа.
Здесь я открыл контент в payload:
Это бинарное XMPP сообщение, содержащее сигнальное сообщение, созданное выше.
Для дизассемблирования делеаем трейс класса XMPPBinaryCoder.
Этот класс имеет метод serialize, который создает бинарное представление XMPP строк.
При выводе этих параметров можно увидеть различные пары ключей, добавленные к XMPP сообщению:
-[XMPPBinaryCoder serialize:
[call from=’49**********@s.whatsapp.net’
id=’1555415586-10’
to=’49**********@s.whatsapp.net’
[offer call-id=’45D7827C624353A70084AED9B8C509D3’
call-creator=’49**********@s.whatsapp.net’
[audio rate=’8000’ enc=’opus’]
[audio rate=’16000’ enc=’opus’]
[net medium=’3’]
[capability ver=’1’ {5b}]
[encopt keygen=’2’]
[enc v=’2’ type=’pkmsg’ {201b}]
]
]
] compressed: 0x0]
Мне удалось сделать фейкововое оповещение о пропущенном звонке от А к Б, хотя на самом деле звонок был инициирован Mallory.
Это стало возможным после перезаписи параметров call-creator и from в JID на стороне А.
Хотя имя Mallory показывается в уведомлении.
Когда сторона Б начинает отвечать на такое сообщение то вызывается сторона А вместо Mallory.
Такое поведение будет интереснее проанализировать в дальнейшем.
Подведем промежуточные итоги – в вацапе шифрованный мастер секрет упаковывается в сигнальное сообщение, которое добавляется в XMPP строки.
XMPP строки также содержат ID и JID обоих сторон.
Передача мастер секрета другой стороне
Согласно официальному описанию от вацап клиенты используют Noise Pipes протокол с Curve25519, AESGCM, и SHA256 из Noise Protocol Framework.
Если использовать трейсинг, содержащий ключевые слова, относящиеся к Noise Protocol Framework, то можно увидеть что класс WANoiseStreamCipher используется для шифрования обращений к вацап серверам.
Класс использует метод encryptPlaintext.
После иницирования звонка значение plaintext – это XMPP сообщение, описанное выше.
Затем сообщение снова шифруется с помощью mbed TLS библиотеки mbedtls_gcm_crypt_and_tag.
mbedtls_gcm_setkey используется размером 256 бит, что означает что используется AES-256-GCM.
Ключ шифрования используется из Noise Pipes Protocol, которй не рассматривается в данной статье.
Шифрованный plaintext далее через TCP идет на вацап сервер (это видно с помощью Wireshark).
Затем сервер форвардит это сообщение вызываемой стороне для инициирования звонка.
Формирование ключей
В этой части рассмторим как работает функция Функция формирования ключа (KDF)/
Результаты были получены с помощью Frida при трассировке класса WAHKDF и библиотеки libcommonCrypto.
Класс WAHKDF был применен для выделения ключей, соли и одноразовых кодов при инициализации SRTP потоков.
Перед старотом звонка 10 раз вызывается метод deriveSecretsFromInputKeyMaterial:
+[WAHKDF deriveSecretsFromInputKeyMaterial: <09a38e76 fe90e4f1 26ed66d0 5a6783ba d48776b6 1daaf7c9 39c005ea 2d8ccdf6>, salt: nil, info: <34393135 39303537 37313632 3040732e 77686174 73617070 2e6e6574>, bytes: 46, withMessageVersion: 3] => result: <4633c47f 94d5ed59 93a6dba8 514d5fb8 5092ba90 4256f8d3 4d56e72e 665bcd4c 5b6c418b db811e7f 84a70c83 f401>+[WAHKDF deriveSecretsFromInputKeyMaterial: <09a38e76 fe90e4f1 26ed66d0 5a6783ba d48776b6 1daaf7c9 39c005ea 2d8ccdf6>, salt: nil, info: <34393137 ******** ******** ******** ******** 6170702e 6e6574>, bytes: 46, withMessageVersion: 3] => result: <a174670a e25d8138 4de0ed3b f4ce7f76 c62c1d00 9ece6573 2ecb497b 1f6ed09c 18c444b9 c180fbd3 51713739 761c>+[WAHKDF deriveSecretsFromInputKeyMaterial: <34354437 38323743 36323433 35334137 30303834 41454439 42384335 30394433>, salt: <00000000>, info: <34393135 39303537 37313632 3040732e 77686174 73617070 2e6e6574>, bytes: 4, withMessageVersion: 3] => result: <0ec654fd>+[WAHKDF deriveSecretsFromInputKeyMaterial: <34354437 38323743 36323433 35334137 30303834 41454439 42384335 30394433>, salt: <01000000>, info: <34393135 39303537 37313632 3040732e 77686174 73617070 2e6e6574>, bytes: 4, withMessageVersion: 3] => result: +[WAHKDF deriveSecretsFromInputKeyMaterial: <34354437 38323743 36323433 35334137 30303834 41454439 42384335 30394433>, salt: <04000000>, info: <34393135 39303537 37313632 3040732e 77686174 73617070 2e6e6574>, bytes: 4, withMessageVersion: 3] => result: +[WAHKDF deriveSecretsFromInputKeyMaterial: <34354437 38323743 36323433 35334137 30303834 41454439 42384335 30394433>, salt: <00000000>, info: <34393137 ******** ******** ******** ******** 6170702e 6e6574>, bytes: 4, withMessageVersion: 3] => result: +[WAHKDF deriveSecretsFromInputKeyMaterial: <34354437 38323743 36323433 35334137 30303834 41454439 42384335 30394433>, salt: <01000000>, info: <34393137 ******** ******** ******** ******** 6170702e 6e6574>, bytes: 4, withMessageVersion: 3] => result: +[WAHKDF deriveSecretsFromInputKeyMaterial: <34354437 38323743 36323433 35334137 30303834 41454439 42384335 30394433>, salt: <04000000>, info: <34393137 ******** ******** ******** ******** 6170702e 6e6574>, bytes: 4, withMessageVersion: 3] => result:
Этот метод создает ключи шифрования, соль и одноразовые коды используя мастер секреты и JIDы участников звонка.
Результирующие значения используются сля создания 6 SRTP потоков, по 3 в каждую сторону разговора.
Следующий сниппет показывает функцию реконструкции ключей написанных на JavaScript:
const crypto = require(«crypto»);
// master secret
const keyMaterial = new Buffer(
«09a38e76fe90e4f126ed66d05a6783bad48776b61daaf7c939c005ea2d8ccdf6»,
«hex»
);
// JID param: 4915905771620@s.whatsapp.net
const info = «3439313539303537373136323040732e77686174736170702e6e6574»;
const salt = new Buffer(
«0000000000000000000000000000000000000000000000000000000000000000»,
«hex»
);
const initialKey = crypto.createHmac(«sha256», salt)
.update(keyMaterial)
.digest();
const temp1 = crypto.createHmac(«sha256», initialKey)
.update(new Buffer(info + «01», «hex»))
.digest();
const temp2 = new Buffer(temp1.toString(«hex») + info + «02», «hex»);
const temp3 = crypto.createHmac(«sha256», initialKey)
.update(temp2)
.digest();
const result = Buffer.concat([temp1, temp3.slice(0, 14)]);
console.log(result.toString(«hex»));
// 4633c47f94d5ed5993a6dba8514d5fb85092ba904256f8d34d56e72e665bcd4c5b6c418bdb811e7f84a70
Этот сниппет представляет собой обмен ключами при инициализации одиночного SRTP потока.
Входящие параметры и исходящие результаты были записаны с помощью Frida.
Для реконструкции KDF алгоритма входящие и исходящий функции хеша были проанализированы с помощью libcommonCrypto библитеки.
Для получения финального ключа были использованы 3 вычисления HMAC-SHA256.
Этот KDF базируется на спецификации RFC 5869.
Инициализация звонка
SRTP, имплементированный с помощью libsrtp, применяется вацапом для шифрования VOIP звонков между клиентами после установления связи.
К сожалению, часть символов из библитеки libsrtp была удалена в бинарниках вацапа.
Поэтому нельзя было сделать трейс функций библитеки по этим символам.
Вместо этого я использовал другой подход для аналитики libsrtp библиотеки.
Много функций libsrtp библиотеки содержат операторы отладки, которые несут информацию о внутренних процессах.
Именно эти операторы отладки и были утилизированы для идентификации функционала библиотеки.
Я сделал поиск по строковым константам, которые были в бинарниках вацапа и также обнаружил их в библиотеке libsrtp.
Затем я нашел функции, которые были связаны с этими константами в бинарниках вацапа.
Затем я нашел такое же в библитеке libsrtp и скопировал первые 12 байт в шестнадцетиричном виде.
Затем запустил Frida для поиска этого же шестнадцетеричного представления.
Таким образом я нашел стартовые адреса этих функций которые могли бы быть обработаны Frida.
В качестве примера я покажу как я обнаружил использование вацапом функции srtp_aes_icm_context_init из библитеки libsrtp.
Эта функция используется для инициализации шифрованного SRTP потока на основе AES-ICM.
Другие функции, описанные в этой части, были обнаружены по такой же методике.
Имплементация srtp_aes_icm_context_init, содержащая 2 оператора отладки:
debug_print(srtp_mod_aes_icm, «key: %s»,
srtp_octet_string_hex_string(key, base_key_len));
debug_print(srtp_mod_aes_icm, «offset: %s», v128_hex_string(&c>offset));
Видно что строковые константы debug_print такие же как в бинарниках приложения вацапа.
Далее, при поиске локации появилась возможность ассоциировать эти константы со связанной функцией.
Далее содержание функций было исследовано с помощью Hopper Disassembler:
int sub_100bbda00(int arg0, int arg1) {
r31 = r31 — 0x60;
var_30 = r24;
stack[-56] = r23;
var_20 = r22;
stack[-40] = r21;
var_10 = r20;
stack[-24] = r19;
saved_fp = r29;
stack[-8] = r30;
r19 = arg0;
sub_100bbf094(arg0, arg1 + 0x10);
r20 = r19 + 0x10;
sub_100bbf094(r20, arg1 + 0x10);
*(int16_t *)(r19 + 0x1e) = 0x0;
*(int16_t *)(r19 + 0xe) = 0x0;
if (*(int32_t *)dword_1012b5760 != 0x0) {
sub_100bbf048(&var_40);
sub_100bc085c(0x7, "%s: key: %s\n");
if (*(int32_t *)0x1012b5760 != 0x0) {
sub_100bbf048(r20);
sub_100bc085c(0x7, "%s: offset: %s\n");
}
}
sub_100bbbffc(&var_40, r19 + 0x30);
*(int32_t *)(r19 + 0xe0) = 0x0;
return 0x0;
}
Строчки 19 и 22 содержат отсылку к дебаг константам.
Когда местополоцение целевой функции было выявлено в бинарниках вацапа, мы все равно продолжали искать адреса функций при запуске приложения.
Потому что iOS применяет метод Address Space Layout Randomization (ASLR) в своих девайсах.
Функции меняют свои адреса всякий раз при запуске приложения.
Следующий сниппет демонстрирует как функция srtp_aes_icm_context_init располагается в памяти при запуске:
const apiResolver = new ApiResolver(«objc»);
const resolvedMatches = apiResolver.enumerateMatches(
"+[NSURL URLWithUnicodeString:]"
);
const SCAN_SIZE = 100000;
const scanStart = resolvedMatches[0].address;
const scanResults = Memory.scanSync(
ptr(scanStart),
SCAN_SIZE,
// first bytes of the hexadecimal representation of srtp_aes_icm_context_init
«FF 83 01 D1 F8 5F 02 A9 F6 57 03 A9»
);
// srtp_err_status_t srtp_aes_icm_context_init(void *cv, const uint8_t *key)
const targetPointer = ptr(scanResults[0].address);
const targetFunction = new NativeFunction(targetPointer, «int», [
«pointer»,
«pointer»
]);
console.log(«scan start: » + scanStart);
console.log(«srtp_aes_icm_context_init: » + scanResults[0].address);
Interceptor.attach(targetFunction, {
onEnter: function(args) {
/*
static srtp_err_status_t srtp_aes_icm_context_init(void *cv, const uint8_t *key)
typedef struct {
v128_t counter; holds the counter value
v128_t offset; initial offset value
v128_t keystream_buffer; buffers bytes of keystream
srtp_aes_expanded_key_t expanded_key; the cipher key
int bytes_in_buffer; number of unused bytes in buffer
int key_size; AES key size + 14 byte SALT
} srtp_aes_icm_ctx_t;
*/
console.log(«srtp_aes_icm_context_init » + args[0] + " key:");
console.log(
hexdump(args[1], {
offset: 0,
length: 16
})
);
},
onLeave: function(args) {}
});
С помощью ApiResolver из Frida можно найти начальную точку в памяти и затем сделать линейный поиск в памяти.
Я использовал ApiResolver для поиска начальной точки которая находится близко к искомой функции в бинарниках и имеет символические имена.
Если функция имеет символическое имя то легко сделать трейс с помощью Frida.
Поэтому я сделал трейс URLWithUnicodeString, которая описана в 3 строке.
Когда начальная точка была найдена, я сделал linear search в памяти.
Значение SCAN_SIZE может быть выбрано в зависимости от расстояния между начальной точкой и искомой функцией.
Строка 12 содержит первые 12 байт искомой функции в шестнадцетеричном формате.
Наконец, NativeFunction, созданная в строке 17 может быть оттрасирована Frida (после того как найден шестнадцетеричный паттерн).
Функция принимает 2 параметра — указатель encryption context (cv) и указатель encryption key (key).
Перед звонком srtp_aes_icm_context_init вызывается 6 раз для инициализации 6 SRTP потоков.
Два потока несут в себе мастер секрет как параметр key.
Потоки шифрованы AES-ICM.
Также есть функция srtp_aes_icm_alloc, которая идентифицирована по строковой константе “allocating cipher with key length %d”.
Функция принимает key length параметр, который имеет значение 16 байт для каждого потока.
В результате мы имеем AES-128-ICM для шифрования всех SRTP потоков.
Несмотря на то что 46 байт получено с key derivation function, только 30 байт используются для инициализации первых двух потоков.
После перезаписи оставшихся 16 байтов звонок между 2 вацап клиентами начинает работать.
Таким образом эти 16 байт не работают совсем!
Шифрование звонка
Существует функция srtp_aes_icm_encrypt, которая является частью libsrtp библиотеки.
Эта функция шифрует SRTP потоки от вацап клиентов с помощью AES-128-ICM.
Она также была обнаружена по строковой константе “block index: %d” в отладчике.
Вот так выглядит одиночный SRTP пакет зашифрованный srtp_aes_icm_encrypt:
Значение первых 12 байт, выделенных красным, объяснено выше.
Синие байты представляют собой SRTP payload.
Последние 4 байта (выделенные зелеными) – это authentication tag.
Так как всего 6 потоков, SRTP payload будет разным в каждом случае.
Call Integrity
Это часть описывает как обеспечивается целостность SRTP пакетов.
В библиотеке libsrtp есть функция srtp_hmac_compute.
Эта функция вычисляет authentication tag для SRTP пакетов которыми обмениваются вацап клиенты.
srtp_hmac_compute модет быть локализована и оттрассирована с помощью Frida, поиском
референса “intermediate state: %s” в строковых константах.
Заголовок srtp_hmac_compute виден вот так:
static srtp_err_status_t srtp_hmac_compute(void *statev,
const uint8_t *message,
int msg_octets,
int tag_len,
uint8_t *result)
srtp_hmac_compute применяет HMAC-SHA1 для вычисления аутенификационного тага.
При трассировке Frida я обнаружил входящее сообщение и исходящий результат, а также значение tag_len для каждого SRTP пакета.
Следующий лог показывает значения tag_len и message srtp_hmac_compute во время звонка:
Attaching…
search srtp_hmac_compute in memory from: 0x1016380ac
found srtp_hmac_compute at: 0x10163b5f4
tag_len: 10
message: 81 ca 00 07 fe 67 2e 32 56 14 89 75 c5 c0 39 4a d3 a0 cd 48 8c 4b 61 8a 78 32 a7 89 1e b7 71 26 80 00 00 01tag_len: 4
message: 00 00 00 00tag_len: 10
message: 81 d0 00 02 fe 67 2e 32 b5 6f 93 8e 80 00 00 02tag_len: 4
message: 00 00 00 00tag_len: 4
message: 00 00 00 00tag_len: 4
message: 00 00 00 00tag_len: 4
message: 00 00 00 00tag_len: 10
message: 81 ca 00 07 83 42 f3 44 81 78 9f f5 39 b1 23 50 48 19 e0 f1 61 5b b5 32 dc b3 10 08 e7 47 a8 4b 80 00 00 01tag_len: 10
message: 81 d0 00 02 83 42 f3 44 94 60 21 fe 80 00 00 02tag_len: 4
message: 00 00 00 00tag_len: 4
message: 00 00 00 00tag_len: 10
message: 81 c8 00 12 fe 67 2e 32 87 b7 69 f8 5a 27 4c 76 b4 29 f6 5d 59 26 de af bd e9 4c 8b f3 ff 48 e3 a9 7e 62 cf db 9c 8a 3d 34 50 48 f8 fc 0e 88 7a 17 eb 17 94 9f 3d 91 27 89 d5 cc bd 21 ea 01 39 27 e1 05 07 66 69 1f 68 08 53 1a 18 02 9e bc 50 ed 8e 40 3e 8a 7b d3 b6 19 e8 54 6f 6b 58 ac 4e e3 25 f5 c2 e8 1c 97 bb 46 f9 38 45 80 00 00 03...
Я обнаружил 2 вещи:
1. SRTP пакеты с длинной тага в 4 байта аутенефицируются некорректно.
Message не содержит актуальный SRTP пакет.
Вместо этого значения 4 нулевых байтов используются для вычисления authentication tag.
Однако, при манипуляциях с тагами этих пакетов, звонок обрывался через несколько секунд.
Возможно что я делал что-то неправильно.
2. Потоки с аутенификационными тагами длинной 10 байт по всей видимости были аутенефицированы правильно.
Несмотря на это, целостность аутенификационных тагов не проверяется во время VOIP звонка.
Следующий спиппет показывает что я отключил аутенификационные таги SRTP пакетов, имеющие таги длиной 10 байт:
const scanStart = new ApiResolver(«objc»).enumerateMatches(
"+[NSURL URLWithUnicodeString:]"
)[0].address;
console.log(«search srtp_hmac_compute in memory from: » + scanStart);
const size = 100000;
const matches = Memory.scanSync(
ptr(scanStart),
size,
// first bytes of the hexadecimal representation of srtp_hmac_compute
«E0 03 16 AA 4C 00 00 94 D5 02 01 91»
);
const targetPtr = ptr(matches[0].address);
console.log(«found srtp_hmac_compute at: » + matches[0].address);
const targetFunction = new NativeFunction(targetPtr, «int», [
«pointer»,
«pointer»,
«int»,
«int»,
«pointer»
]);
const MANIPULATABLE_TAG_SIZE = 10;
const manipulatedTag = Memory.alloc(MANIPULATABLE_TAG_SIZE);
manipulatedTag.writeByteArray([0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]);
Interceptor.attach(ptr(targetFunction), {
onEnter: function(args) {
/*
static srtp_err_status_t srtp_hmac_compute(void *statev,
const uint8_t *message,
int msg_octets,
int tag_len,
uint8_t *result)
*/
console.log(«srtp_hmac_compute tag (» + args[3].toInt32() + "):");
const tag_len = args[3].toInt32();
if (tag_len === MANIPULATABLE_TAG_SIZE) {
console.log(
hexdump(args[1], {
length: args[2].toInt32()
})
);
args[3] = 0;
args[4].writePointer(manipulatedTag);
}
}
});
Когда выполнялся скрипт от Frida, VOIP звонки работали.
Следовательно, целостность SRTP пакетов была нарушена.
Почему так происходит, непонятно, для выснения этих причин нужно более углубиться в разбор функционированя этого случая.
Заключение
Статья приоткрывает фундаментальные части WhatsApp VoIP протокола…
Я продемонстрировал как можно проанализировать сетевой трафик клиентов, как анализировать бинарники и как анализировать приложение во время работы.
Основные результаты этий аналитики следующие:
— Вацап использует опенсурсные библиотеки libsignal-protocol-c, libsrtp, PJSIP и mbed TLS для имплементации своего VOIP протокола.
— Так называемый “master secret” используется для инициализации 2 SRTP потоков, зашифрованных AES-128-ICM.
Мастер секрет извпользуется как входящий параметр для key derivation function (HKDF), которая в свою очередь содержит ключи, соли и nonces в качестве параметров для SRTP.
— Noise Pipes Protocol, Signal Protocol и XMPP участвуют в передаче мастер секрета другой стороне для совершения звонка.
Мастер секрет зашифрован Signal Protocol, затем упакован в XMPP сообщение, которая затем шифроуется Noise Pipes Protocol и затем отправляется на вацап сервер.
После этого сервер передает зашифрованный мастер секрет вызываемой стороне для сигнализации входящего звонка.
— Защита целостности VOIP звонков в вацапе имеет изьяны – потому что некоторые SRTP потоки не имеют проверку на целостность.
Более того, эти потоки можно сформировать с невалидными аутенификационными тагами с нулевыми байтами, вместо актуального SRTP пакета.
— SRTP пакеты не имеют чувствительных данных, за исключением прожолжительности VOIP сессии.
— Хакеры могут мнипулировать с уведомлением о звонке.
Это позволяет атакующему скомпрометировать вацап клиента, потому что принимающий звонок пользователь видит недостоверную информацию на своем девайсе.
Далее с помощью социальной инженерии можно развить эту атаку.
Литература:
github.com/schirrmacher/files/blob/master/WhatsApp%20VoIP%20Protocol.pdf
github.com/schirrmacher/files/blob/master/WhatsApp
github.com/schirrmacher/files/blob/master/WhatsAppCore
В заключении – эта статья показывает, как тяжело разработчикам скрыть имплементацию мобильного приложения, которая сделана с помощью открытых библитек.
Тулы типа Frida позволяют исследователяи и атакующим собрать необходимую информацию об имплементации за очень короткий промежуток времени.
Разработчикам подобных приложений следует иметь ввиду что криптографические ключи могут быть также легко обнаружены с помощью таких инструментов.
Для того чтобы такое не происходило следует тщательно удалять символичекие имена из бинарных файлов.
Более того, разработчикам следуют удалять также строковые константы, которые содержат критическую информацию или могут быть полезны для идентификации функционала.
member0
Автор, спасибо за работу. Ложка дегтя. Немного или даже много сбилось оформление. В середине читать стало сложно.