Пользователь
0,0
рейтинг
19 февраля 2015 в 12:26

Разработка → Потенциальная уязвимость в Telegram Android

Дисклеймер: Описанная ниже потенциальная уязвимость на данный момент исправлена: 18 декабря 2014 была обновлена версия на Google Play, 3 января 2015 были внесены правки в публичный код на GitHub.

Так сложилось, что мне необходимо было изучить исходные коды механизма шифрования, передачи и дешифрования сообщений в Telegram для мобильных платформ iOS и Android. То есть речь идет о клиентских приложениях, именно их исходники (iOS, Android) находятся в свободном доступе.

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

Чтобы разобраться в сути, давайте для начала рассмотрим принцип обмена сообщениями. Он состоит из трех основных этапов:
  1. Генерация общего секретного ключа;
  2. Шифрование исходящего сообщения;
  3. Дешифрование входящего сообщения.

Замечание: Я здесь намеренно опустил этапы клиент-серверного взаимодействия (установка соединения, передача/прием сообщений), так как они представляют собой точно такие же 3 этапа. То есть для шифрования/дешифрования отдельного сообщения и для передачи данных между клиентом и сервером используется один и тот же принцип защиты.

Принцип генерации общего секретного ключа построен на протоколе Диффи-Хеллмана.

Шифрование:
  1. Формируем объект, представляющий исходное сообщение;
  2. В спец. поле записываем массив от 1 до 16 рандомных байт;
  3. Исходный объект сериализуем в массив байт;
  4. С нулевой позиции массива выделяем 4 байта и записываем длину данных в массиве;
  5. Рассчитываем хеш (sha1) получившегося массива данных;
  6. Рассчитываем ключ сообщения (последние 16 байт хеша);
  7. На основе общего секретного ключа и ключа сообщения рассчитываем параметры для AES-256 шифрования;
  8. В исходный массив данных дописываем рандомные данные до тех пор, пока длина получившегося массива не будут кратна 16 (AES требует блоки данных размером 128 бит);
  9. Получившийся массив шифруем с помощью AES-256;
  10. Рассчитываем хеш (sha1) общего секретного ключа;
  11. Рассчитываем идентификатор общего секретного ключа (последние 8 байт хеша);
  12. Формируем конечный массив данных состоящий из идентификатора общего секретного ключа (8 байт), ключа сообщения (16 байт) и зашифрованного массива данных (размер как получится).

Дешифрование:
  1. Рассчитываем хеш (sha1) общего секретного ключа, который хранится локально;
  2. Рассчитываем идентификатор общего секретного ключа (последние 8 байт хеша);
  3. Считываем идентификатор общего секретного ключа из полученного массива данных (первые 8 байт);
  4. Сравниваем с локально рассчитанным идентификатором. В случае равенства переходим к следующему пункту, иначе игнорируем сообщение;
  5. Считываем ключ сообщения из полученного массива данных (следующие 16 байт);
  6. На основе общего секретного ключа и ключа сообщения рассчитываем параметры для AES-256 дешифрования;
  7. Считываем оставшиеся байты из полученного массива данных и дешифруем их с помощью AES-256;
  8. Считываем длину сообщения из дешифрованного массива данных (первые 4 байта);
  9. Проверяем длину сообщения: значение должно быть больше нуля и меньше длины оставшегося дешифрованного массива данных. Если длина валидна, то переходим к следующему пункту, иначе игнорируем сообщение;
  10. В дешифрованном массиве оставляем только полезные данные (удаляем первые 4 байта и байты в конце, если длина массива превышает длину сообщения);
  11. Рассчитываем хеш (sha1) дешифрованного массива данных;
  12. Рассчитываем ключ сообщения (последние 16 байт хеша);
  13. Сравниваем рассчитанный ключ сообщения с ключом, считанным из полученного массива данных. В случае равенства переходим к следующему пункту, иначе игнорируем сообщение;
  14. Десериализуем дешифрованный массив данных в объект, представляющий полученное сообщение.

С теорией разобрались. Пришло время перейти к практике.
Рассмотрим код дешифрования сообщения для обеих платформ (в коде генерации общего секретного ключа и шифрования сообщения отличий либо ошибок найдено не было, поэтому мы его опустим). Код соответствует последней ревизии ветки master. Принципиально важные проверки пронумерованы в комментариях (1, 2 ,3).
Telegram iOS: TGUpdateStateRequestBuilder.mm

//———————————————————————Cut———————————————————————
        int64_t keyId = 0;
        [encryptedMessage.bytes getBytes:&keyId range:NSMakeRange(0, 8)];
        NSData *messageKey = [encryptedMessage.bytes subdataWithRange:NSMakeRange(8, 16)];
        
        int64_t localKeyId = 0;
        NSData *key = nil;
        bool keyFound = false;
        
        if (cachedKeys != NULL)
        {
            auto it = cachedKeys->find(conversationId);
            if (it != cachedKeys->end())
            {
                keyFound = true;
                localKeyId = it->second.first;
                key = it->second.second;
            }
        }
        
        if (!keyFound)
        {
            key = [TGDatabaseInstance() encryptionKeyForConversationId:conversationId keyFingerprint:&localKeyId];
            
            if (cachedKeys != NULL)
                (*cachedKeys)[conversationId] = std::pair<int64_t, NSData *>(localKeyId, key);
        }
        
        if (key != nil && keyId == localKeyId) // 1)
        {
            MessageKeyData keyData = [TGConversationSendMessageActor generateMessageKeyData:messageKey incoming:false key:key];
            
            NSMutableData *messageData = [[encryptedMessage.bytes subdataWithRange:NSMakeRange(8 + 16, encryptedMessage.bytes.length - (8 + 16))] mutableCopy];
            encryptWithAESInplace(messageData, keyData.aesKey, keyData.aesIv, false);
            
            int32_t messageLength = 0;
            [messageData getBytes:&messageLength range:NSMakeRange(0, 4)];
            
            if (messageLength < 0 || messageLength > (int32_t)messageData.length - 4) // 2)
                TGLog(@"***** Ignoring message from conversation %lld with invalid message length", encryptedMessage.chat_id);
            else
            {
                NSData *localMessageKeyFull = computeSHA1ForSubdata(messageData, 0, messageLength + 4);
                NSData *localMessageKey = [[NSData alloc] initWithBytes:(((int8_t *)localMessageKeyFull.bytes) + localMessageKeyFull.length - 16) length:16];
                if (![localMessageKey isEqualToData:messageKey]) // 3)
                    TGLog(@"***** Ignoring message from conversation with message key mismatch %lld", encryptedMessage.chat_id);
                else
                {
                    NSInputStream *is = [[NSInputStream alloc] initWithData:messageData];
                    [is open];
                    [is readInt32];
                    
                    int32_t signature = [is readInt32];
                    id decryptedObject = TLMetaClassStore::constructObject(is, signature, nil, nil, nil);
//———————————————————————Cut———————————————————————

Telegram Android: SecretChatHelper.java

//———————————————————————Cut———————————————————————
ByteBufferDesc is = BuffersStorage.getInstance().getFreeBuffer(message.bytes.length);
is.writeRaw(message.bytes);
is.position(0);
long fingerprint = is.readInt64();
byte[] keyToDecrypt = null;
boolean new_key_used = false;
if (chat.key_fingerprint == fingerprint) { // 1)
    keyToDecrypt = chat.auth_key;
} else if (chat.future_key_fingerprint != 0 && chat.future_key_fingerprint == fingerprint) {
    keyToDecrypt = chat.future_auth_key;
    new_key_used = true;
}

if (keyToDecrypt != null) {
    byte[] messageKey = is.readData(16);
    MessageKeyData keyData = Utilities.generateMessageKeyData(keyToDecrypt, messageKey, false);

    Utilities.aesIgeEncryption(is.buffer, keyData.aesKey, keyData.aesIv, false, false, 24, is.limit() - 24);

    int len = is.readInt32();
    TLObject object = TLClassStore.Instance().TLdeserialize(is, is.readInt32());
//———————————————————————Cut———————————————————————

Как видно из кода, в iOS версии выполняются следующие проверки:
  1. Сравниваем идентификатор (хеш) общего секретного ключа из тела входящего сообщения с идентификатором (хешем) локального общего секретного ключа;
  2. Сравниваем переданную длину дешифрованного сообщения с минимальной и максимальной допустимой длиной;
  3. Сравниваем ключ (хеш) полученного дешифрованного сообщения с ключом (хешом) оригинального сообщения, который был передан отправителем.

В Android версии проверки 2 и 3 отсутствуют.

Рассмотрим ситуацию, в которой отсутствие этих проверок может повлиять на секретный чат:
Для конструктивного диалога позовем Алису и Боба.
И так, действующие лица:
  1. Боб — собеседник №1. Для обмена сообщениями использует Telegram Android;
  2. Алиса — собеседник №2. Для обмена сообщениями использует любой клиент Telegram;
  3. Злоумышленник — разработчик или иное лицо имеющее физический доступ к серверу Telegram.

Сценарий:
  1. Боб инициирует секретный чат с Алисой, чтобы сгенерировать общий секретный ключ по Диффи-Хеллману (запрашивает p и g с сервера; выполняет проверки; генерирует а и ga; передает ga Алисе);
  2. Алиса принимает секретный чат с Бобом (запрашивает p и g с сервера, выполняет проверки, генерирует b, gb; генерирует общий секретный ключ на основе b, ga и p; передает Бобу идентификатор (хеш) общего секретного ключа и gb);
  3. Боб подтверждает секретный чат с Алисой (генерирует общий секретный ключ на основе a, gb и p; сравнивает идентификатор (хеш) своего ключа с идентификатором (хешем) ключа, полученного от Алисы);
  4. Алиса отправляет зашифрованное сообщение Бобу;
  5. Боб получает сообщение и успешно его дешифрует;
  6. Злоумышленник видит зашифрованное сообщения Алисы, отправленное Бобу. Злоумышленник не может расшифровать сообщение, так как не имеет доступа к общему секретному ключу;
  7. Злоумышленник извлекает следующие данные из перехваченного зашифрованного сообщения: идентификатор (хеш) общего секретного ключа (первые 8 байт ), ключ (хеш) дешифрованного сообщения (следующие 16 байт);
  8. Злоумышленник формирует новое сообщение от лица Алисы следующим образом:
    • Первые 8 байт равны идентификатору (хешу) общего секретного ключа из перехваченного сообщения;
    • Далее записывается массив рандомных данных длиной не менее 32 байт (16 байт — ключ (хеш) сообщения, 4 байта — длина сообщения, 4 байта — идентификатор класса (ниже станет понятно, что это), 8 байт — дополнительные данные, чтобы сформировать блок, корректной с точки зрения АES-256 длины).

  9. Злоумышленник отправляет новое сообщение Бобу от лица Алисы;
  10. Боб получает новое сообщение от Алисы, отправленное злоумышленником, и пытается его дешифровать:
    • Считывает идентификатор (хеш) общего секретного ключа (первые 8 байт) и успешно сравнивает с идентификатором, рассчитанным локально;
    • Считывает ключ (хеш) дешифрованного сообщения (следующие 16 байт);
    • Рассчитывает параметры симметричного шифрования AES-256 с помощью общего секретного ключа и полученного ключа (хеша) сообщения. Полученные параметры представляет собой рандомные наборы байтов и не соответствует оригинальным параметрам шифрования;
    • Полученные параметры используются для дешифрования сообщения (оставшиеся байты). Полученное на выходе сообщение представляет собой рандомный набор байт и не соответствует оригинальному сообщению. Так как на этом этапе отсутствует проверка длины и ключа (хеша) получившегося сообщения, то данные передаются для дальнейшей обработки, несмотря на их заведомую ложность;
    • Из получившегося сообщения вырезаются первые 4 байта (в оригинальном сообщении эти данные представляют собой длину исходного сообщения). Далее в коде эти 4 байта нигде не используются;
    • Оставшаяся часть сообщения передается в десериализатор: TLObject object = TLClassStore.Instance().TLdeserialize(is, is.readInt32());
    • Первые 4 байта оставшегося сообщения интерпретируются как идентификатор класса (второй параметр в методе TLdeserialize). Класс TLClassStore содержит словарь, в котором значения представляют собой классы различных типов сообщений, а ключи — идентификаторы классов (константы длиной в 4 байта). Полное содержание словаря представлено в классе TLClassStore.java.
      TLClassStore пытается найти класс соответствующий переданным 4 рандомным байтам. Если соответствие найдено, то возвращается новый объект соответствующего класса, иначе возвращается null и входящее сообщение полностью игнорируется (то есть Боб этого не заметит). В случае успеха оставшаяся часть сообщения используется для инициализации параметров созданного объекта. Далее полученный объект используется по назначению. Для Боба это будет выглядеть как рандомная активность со стороны Алисы (например, новое текстовое сообщение с рандомным содержанием).


Вероятность успешного создания объекта примерно равна 382 / 2^32 ≃ 8.9 * 10^-8, где
382 — количество классов содержащихся в словаре;
32 — длина идентификатора класса в битах.
Вероятность, конечно, невысокая, но так как неуспешные случаи проходят незаметно для пользователя, то злоумышленник может непрерывно отправлять сообщения, ограничиваясь только шириной канала подключения клиента к серверу. В таком случае атака может быть вполне осуществимой. Если предположить, что минимальный трафик на одно сообщение может составлять около 100 байт, то потребуется около 1 ГБ трафика для гарантированного создания объекта.

Попробуем прикинуть вероятность успешной атаки в случае наличия хотя бы одной из пропущенных проверок:
При наличии проверки длины сообщения: (2^10 / 2^32) * (382 / 2^32) ≃ 2.1 * 10^-18, где
2^10 = 1024 — максимальная валидная длина сообщения, примерно столько памяти занимает обычное сообщение;
32 = 4 байта, столько памяти занимает длина сообщения.
При наличии проверки ключа (хеша) сообщения: (1 / 2^128) * (382 / 2^32) ≃ 2.6 * 10^-46, где
128 — длина ключа (хеша) сообщения.

Стоит отметить, что на других уровнях защиты проверка подписи сообщения присутствует. Например, при установке клиент-серверного соединения (используется тот же принцип, что и при обмене сообщениями): ConnectionsManager.java

//———————————————————————Cut———————————————————————
byte[] realMessageKeyFull = Utilities.computeSHA1(data.buffer, 24, Math.min(messageLength + 32 + 24, data.limit()));
if (realMessageKeyFull == null) {
    return;
}

if (!Utilities.arraysEquals(messageKey, 0, realMessageKeyFull, realMessageKeyFull.length - 16)) { // 3)
    FileLog.e("tmessages", "***** Error: invalid message key");
    connection.suspendConnection(true);
    connection.connect();
    return;
}
//———————————————————————Cut———————————————————————

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

Тем не менее, на данный момент разработчики внесли необходимые правки в Dev ветку и обновили сборку в Google Play. Также хочется отметить тот факт, что за найденные мной недочеты разработчики выплатили вознаграждение в размере 5000$. Как говорится «не мелочь и приятно».
Uladzimir Papko @visput
карма
26,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +12
    выплатили вознаграждение

    а вот это — респект Паше Дурову разработчикам
    • –4
      Имхо зря зачеркнули Дурова, у него и манифест был хороший и он продвигает криптографию в массы
      • 0
        Это Вы про это?

        Я Вам так скажу, когда я его знал лично (ещё до создания Вконтактика) — говорил он о гораздо более глубоких вещах и на ином уровне, нежели начал писать в опусах подобных указанному выше. Отдам должное — мастерством письменного эпатажа он овладел в совершенстве)

        ИМХО: в его письменных опусах не вижу границы между продвижением в массы и личными бизнес-интересами
      • +3
        Крипто в массы это Tox, ***коин и аналогичные проекты у которых нет владельца, серверов и возможности контроля.

        Когда есть какая то фирма и на её серверах что то хранится необходимое для работы — это бизнес.
        Только вопрос времени в том когда и как фирма начнёт зарабатывать на этой базе и когда к ним придут и попросят помочь в ОРМ без возможности отказатся.
        • 0
          Да, естествено Tox, биткоин и прочие Full P2P технологии это полноценная крипто в массы, но если сравнивать общение через skype/vk/facebook и telegram — то лучше уж последнее
          • 0
            Нет. При общении через skype/vk/facebook у вас хотя бы нет ложного чувства безопастности, вызванного обилием лапши на ушах от PR-отдела компании, делающей telegram.
            • 0
              почему же ложного? исходники открыты
              • 0
                Потому, что они изобрели свой кастомный велосипед, в результате чего постоянно находятся дыры, которые еще неизвестно, не закладки ли. И уж точно у них множество сомнительных решений, вместо доказанных математиками и проверенных десятилетиями алгоритмов.
                А уж их «защита» от mitm — смех да и только. Фактически, они легко могут читать всю вашу переписку.
                • +2
                  Еще раз подчеркиваю — что я не считаю телеграм лучше Tox и пр. я говорю о том, что телеграм позволяет популизировать идеи криптографии и безопастности, и в дальнейшем, будет меньше вопрос (типо «Мне нечего скрывать, зачем мне шифрование?») при переходе на Tox или аналоги.
                  • 0
                    Это интересная мысль, я не понял ее сразу.
              • 0
                Насколько я понимаю тему криптографии в массы, Вам нужно курить в сторону свободных XMPP/Jabber, а не проприетарных и прочих «пседвобесплатных» решений как Telegram. Было время когда Гугл не зарабатывал денег неся много идей в массы, а теперь даже Фэйсбук имеет 2,5 млрд долларов чистой прибыли в год.
          • +1
            Для голоса/видео скайп пока лучше.
            Всем остальным из списка я никогда не пользовался и не собираюсь начинать :)

            У Tox и прочих p2p есть одно приемущество которое другие никак не позволяют и не хотят делать: отсутствие регистрации.
            uTox — ты его скачал, запустил и всё уже само зарегалось и готово к работе, никаких дебильных регистраций на каком то долбаном сайтое, где тебя допрашивают обо всё и вымогают мыло и мобилу, пока разве что анализы мочи и кала не просят, но гугл уже работает над этим…
            • 0
              Качество кстати у FaceTime/Viber аналогичное Skype.
  • –31
    Интересно, а Дуров Вам заплатит за эту уязвимость?
    • +8
      Тем не менее, на данный момент разработчики внесли необходимые правки в Dev ветку и обновили сборку в Google Play. Также хочется отметить тот факт, что за найденные мной недочеты разработчики выплатили вознаграждение в размере 5000$. Как говорится «не мелочь и приятно».
    • +25
      пиши@не читай
  • –4
    [зануда мод]
    дешифрование это взлом шифра, а у вас — расшифровка или расшифрование
    [/зануда мод]
    • –5
      Взлом шифра — это криптоанализ. А термина «расшифровка» нет существует.
      • +1
        Давайте я за вас погуглю:
        дешифрование
        wiktionary.org: расшифровка
        • –5
          По Вашей же ссылке:
          Эта терминология абсурдна и, по-существу, единственный аргумент в ее защиту состоит в том, что она применяется уже очень давно. Аргументы же против демонстрируют ее несостоятельность.
          • +8
            Статья о шифровании на хабре. Что в комментах? Правильно — срач на тему правописания.
  • +2
    Меня вот что еще беспокоит…
    Из документации:
    An AES key and an initialization vector are computed ( key is the shared key obtained during Key Generation, x = 0 ):
    sha1_a = SHA1 (msg_key + substr (key, x, 32));
    sha1_b = SHA1 (substr (key, 32+x, 16) + msg_key + substr (key, 48+x, 16));
    sha1_с = SHA1 (substr (key, 64+x, 32) + msg_key);
    sha1_d = SHA1 (msg_key + substr (key, 96+x, 32));
    aes_key = substr (sha1_a, 0, 8) + substr (sha1_b, 8, 12) + substr (sha1_c, 4, 12);
    aes_iv = substr (sha1_a, 8, 12) + substr (sha1_b, 0, 8) + substr (sha1_c, 16, 4) + substr (sha1_d, 0, 8);
    

    Правильно ли я понимаю, что из 256 байт общего секретного ключа используются только первые 128?
    • 0
      Да, судя по всему так есть. Содержание кода (iOS, Android, см. метод generateMessageKeyData) соответствует документации.
      • 0
        Вот интересно зачем так сделано. То, что 128 байт все равно много — это понятно. Но ничего же не должно быть просто так.
        • 0
          Все равно разница между 128 и 256 байт колоссальная. Кажется, Вам стоит обратиться к разработчикам за разъяснениями.
          • 0
            Разница настолько колоссальная, что при нынешних технологиях безразличная, как ∞+1 и ∞+10001
  • 0
    Кстати вышло обновление которое теперь шифрует файл с историей переписки.
    • 0
      Чем шифрует?
      Или теперь пользователю нужно будет вводить какой-то пароль?
      Или имеется ввиду стандартное шифрование средствами iOS SDK, когда устройство залочено?
      • +1
        Предлагает выбрать пароль и в довесок активировать Touch ID что бы без пароля, конкретно чем шифрует — не могу понять тк не умею читать исходники для iOS
        • 0
          Да, предлагают шифровать базу, если указать длинный пароль.
          А чем шифруют видимо никто пока не узнает, так последней версии кода, видимо, нет в открытом доступе. Последнюю версию, которую нашел — 2.8. Текущая 2.9.x.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.