* Небольшой экскурс в WeChat;
* О платформе, версии приложения, используемых утилитах и расшифровке исполняемого файла;
* О двух протоколах (старом и новым);
* О сериализации объектов;
* Используемая криптография и обмен ключами;
* О заголовках и хэш-функциях;
* О найденных уявзимостях.
Мессенджер WeChat от китайской компании Tencent
WeChat — второй по популярности мессенджер в мире. Официальных данных о количестве пользователей найти очень затруднительно, но сделать приблизительную оценку можно.
Речь идет примерно о 800 миллионах пользователей по всему миру, 90% которых приходится на Китай.
В Китае практически каждый владелец смартфона использует WeChat (китайское самоназвание -Weixin), так как это не только мессенджер в классическом понимании, а целая система, включающая мобильный кошелек, встроенный браузер, интернет-магазин и пр. В нем представлены все государственные учреждения Китая. Оплатить квитанцию или записаться на прием к врачу можно через мессенджер.
Актуальной задачей являлась интеграция CRM систем заказчиков, активно работающих в Китае, с WeChat. Этому способствовала широкая распространенность WeChat в Китае, а также отсутствие официального API. СМС-информирование в Китае стоит дорого и, что самое главное, работает нестабильно, к тому же здесь отсутствует статус «прочитано». Заказчик, используя API, сможет через свою через CRM систему уведомлять пользователей WeChat (подписанных на получение информации от номера заказчика, об этом расскажем ниже) о доставке товаров, новых заказах и прочей сервисной информации.
Исследование протокола
Было принято решение изучить мессенджер «изнутри», разобраться в коде 32-битной версии мессенджера для iOS. Имеется у нас старенький, видавший виды, iPhone 4S с версией iOS 7.2.1.
В качестве MITM использовуем Burp Suite Free Edition.
Скачиваем приложение и при помощи замечательной утилиты dumpdecrypted расшифровываем исполняемый файл.
На момент начала реверс-инжиниринга WeChat была актуальна версия 6.3.13.
Теперь осталось скопировать файл с устройства, дисассемблировать в IDA, и можно начинать.
Рассмотрим алгоритм обмена ключами на примере регистрации.
Запускаем приложение и видим предложение ввести номер телефона для регистрации.
Вводим номер телефона и видим в MITM HTTP-запрос на адрес
http:// hkshort.weixin.qq.com/bindopmobileforreg.
Тело запроса состоит из:
• Заголовка (желтый цвет);
• Шифрованных данных (синий цвет).
После весьма продолжительного статического анализа кода удалось выяснить, что клиент общается с сервером посредством сериализованных объектов. Объекты сериализуются при помощи библиотеки Protocol Buffers.
Первым сообщением передаются следующие данные:
• Номер телефона;
• Язык системы телефона;
• ID устройства;
• Версия клиента;
• Ключ для расшифровывания ответа (случайные 16 байт);
• Прочие данные (не очень интересующие нас).
Сообщение сериализуется и шифруется публичным ключом сервера при помощи алгоритма RSA. Ответ сервера расшифровывается AES-ключом, переданным в запросе. В ответе сообщается, что либо все в порядке, либо указывается ошибка.
Получаем СМС и вводим код. Формируется тот же запрос, только уже с кодом из СМС, а в ответе получаем так называемый ticket. Теперь, имея ticket, можно отправлять запрос на регистрацию. Вводим имя и нажимаем Ок.
Обмен ключами происходит по алгоритму Диффи-Хеллмана с использованием эллиптической кривой над конечным полем «secp224r1». Генерируются закрытый и открытый ключ, и отправляется запрос на адрес hkshort.weixin.qq.com/newreg. Сервер генерирует свои ключи, а также выдает нам так называемые CryptUin и ServerID, о которых расскажем позже. В ответ сервер присылает нам свой публичный ключ и сессионный ключ.
Теперь у нас есть публичный ключ сервера, и мы вычисляем общий ключ, с помощью которого расшифровываем сессионный ключ. С этого момента общение клиент-сервер осуществляется с помощью симметричного алгоритма AES с длиной ключа 128 бит.
Вообще, в алгоритме установки соединения и обмена ключами нет ничего сверхъестественного. Правильно зашифровать данные и обменяться ключам — это половина задачи, необходимо также для каждого сообщения правильно составить заголовок. Даже если правильно сериализовать данные и зашифровать, то при неправильном заголовке сервер пришлет ответ с ошибкой. Заголовок выглядит так:
bfa65f16050520252cb6b770021001754cc8fd5e57e085457800fb0242420001aeb890f40b010a0080.
Теперь расскажем подробнее о каждом поле:
1. Идентификатор протокола. Каждый пакет начинается с этого байта.
2. Флаги. Хранит информацию о длине SrvID, длине самого заголовка и сжатии исходного сообщения.
Флаг сжатия выставляется равным 0b10, если сообщение не сжималось, в противном случае ставится равным 0b01.
3. Версия приложения. Без комментариев.
4. CryptUin. После прохождения регистрации каждому аккаунту присваивается уникальный идентификатор из четырех байт.
5. SrvID. ID текущей сессии. Меняется при каждом новом подключении.
6. uiCgi. Код команды. У каждой команды есть свой uiCgi и url. Например для команды bindopmobileforreg, uiCgi равен 0x91, а для newreg – 0x7e. Большинство чисел упаковываются, используя следующий алгоритм:
private static void Write7BitEncodedInt(BinaryWriter store, int value)
{
Debug.Assert(store != null);
// Write out an int 7 bits at a time. The high bit of the byte,
// when on, tells reader to continue reading more bytes.
uint v = (uint)value; // support negative numbers
while (v >= 0x80)
{
store.Write((byte)(v | 0x80));
v >>= 7;
}
store.Write((byte)v);
}
В данном примере uiCgi равен 0x17b, а в упакованном виде – fb02.
7. Длина исходного сообщения. Длина сериализованных данных. Число так же упаковывается, но поскольку оно меньше 0x80, осталось без изменений.
8. Длина сжатого сообщения. Сжатия не производилось, поэтому не отличается от предыдущего.
9. Флаг.
10. Хеш.
Вычисляется следующим образом:
hash1 = md5(cryptUin.shareKey);
hash2 = md5( strlen(data).shareKey.hash1.data)
resultHash = adler32(hash2)
sharedKey –это общий ключ, полученный при хендшейке.
Хеш также упаковывается.
11.Флаги. Значение этих флагов осталось загадкой, но они статичны, поэтому отдельно их изучать не было смысла.
Сейчас уже используется другой протокол, о котором напишем в последующих публикациях. По сути он является оберткой над вышеописанным. Старый протокол до сих пор поддерживается — для этого в отладчике необходимо сбросить флаг MmtlsCtrlFlag.
Защита от спама.
Чтобы защититься от спама, пользователь может включить опцию «подтверждение дружбы». В этом случае написать ему сообщение можно только после того как он подтвердит, что вы друзья. Запрос на подтверждение дружбы может содержать приветственное сообщение.
Отправить много приветственных сообщений не получится. После отправки пятнадцати запросов все остальные перестают отправляться и встают в очередь. Шестнадцатый запрос отправится только тогда, когда кто-то из предыдущих пятнадцати добавит вас в друзья. Но пользователь этого не знает, и интерфейс приложения тоже никак об этом не сообщает. Выяснить это удалось при помощи анализа траффика и экспериментов.
В процессе работы была так же обнаружена интересная уязвимость. В приложении есть возможность найти пользователя по номеру телефона. Сервер либо отвечает что такого пользователя нет, либо возвращает информацию о нем(имя, пол, город, фото итд). Но если отправлять эти запросы слишком часто, то сервер отвечает сообщением «Too many attempts. Try again later ». Этим можно воспользоваться, так как, если пользователя не существует, сервер всегда сообщит об этом, а если приходит ответ «Too many attempts. Try again later „- это значит, что пользователь существует. Используя это, можно собирать базу пользователей. Кстати обнаружились очень “интересные» пользователи, которые используют WeChat, но обычным способом их не обнаружить, и даже невозможно отправить им приглашение, скорее всего это «особенные» люди Китая. Даже если запросить регистрацию на «интересный» номер, то сервер сообщит, что данный пользователь уже зарегистрирован и предлагает восстановить аккаунт, но уже не по СМС.
Используя 20 000 потоков возможно с одного аккаунта за сутки собрать всю базу WeChat по Китаю, блокировки аккаунта не происходит.
Также отдельно хотелось бы сообщить, что End-to-End шифрования между пользователями не происходит. Сообщения шифруются только симметричным ключом, сервер их расшифровывает и заново зашифровывает симметричным ключом получателя и отправляет их получателю.
Данная статья является вводной, при наличии интереса со стороны сообщества Хабра возможно появление следующих публикаций по WeChat, так как любое событие в WeChat (например, сериализация объектов) достойны отдельных статей.
Автор: Максим Суворов