πŸ“œ ⬆️ ⬇️

Potential Android Telegram Vulnerability

Disclaimer: The potential vulnerability described below has now been fixed: the version on Google Play was updated on December 18, 2014, on January 3, 2015, the GitHub public code was edited .

It so happened that I needed to study the source codes of the encryption mechanism, the transmission and decryption of messages in Telegram for iOS and Android mobile platforms. That is, we are talking about client applications, it is their source code ( iOS , Android ) that are freely available.

Since I specialize more in iOS, first of all I began to study the version for this platform. After spending about a day reading the source code and working with the debugger, I realized what was happening and started the Android version. It is easy to guess that the mechanisms and principles of operation should be identical due to the compatibility of all platforms among themselves. But to my surprise, I found several differences in the message decryption algorithm in the Android version, which gave rise to the vulnerability, so to speak. The general essence of the vulnerability lies in the fact that in the client application there is no comparison of the hash of the decrypted message with the original hash transmitted along with the encrypted message. In fact, there is no verification of the message signature. The absence of such a check may allow third parties with access to the server to create random activity from persons participating in a secret chat. At the same time, access to a shared secret key is not required, and it remains invulnerable to third parties.

To understand the essence, let's first consider the principle of messaging. It consists of three main stages:
  1. Generate a shared secret key;
  2. Encrypt outgoing message;
  3. Decrypt incoming message.

Note: I deliberately omitted the client-server interaction stages (setting up a connection, sending / receiving messages), since they are exactly the same 3 stages. That is, the same security principle is used to encrypt / decrypt a single message and to transfer data between the client and the server.
')
The principle of generating a shared secret key is based on the Diffie-Hellman protocol .

Encryption:
  1. We form an object representing the original message;
  2. In spec. field we write an array from 1 to 16 random bytes;
  3. The source object is serializable to the byte array;
  4. From the zero position of the array, select 4 bytes and write the data length in the array;
  5. Calculate the hash (sha1) of the resulting data array;
  6. Calculate the key message (the last 16 bytes of the hash);
  7. Based on the shared secret key and the message key, we calculate the parameters for AES-256 encryption;
  8. We add random data to the initial data array until the resulting array is a multiple of 16 (AES requires 128-bit data blocks);
  9. The resulting array is encrypted using AES-256;
  10. Calculate the hash (sha1) of the shared secret key;
  11. Calculate the shared secret key identifier (the last 8 bytes of the hash);
  12. We form a final data array consisting of a shared secret key identifier (8 bytes), a message key (16 bytes) and an encrypted data array (size as it will).

Decryption:
  1. Calculate the hash (sha1) of the shared secret key, which is stored locally;
  2. Calculate the shared secret key identifier (the last 8 bytes of the hash);
  3. Read the shared secret key identifier from the received data array (the first 8 bytes);
  4. We compare with the locally calculated identifier. In case of equality, go to the next item, otherwise ignore the message;
  5. Read the message key from the received data array (the next 16 bytes);
  6. Based on the shared secret key and the message key, we calculate the parameters for AES-256 decryption;
  7. Read the remaining bytes from the received data array and decrypt them using AES-256;
  8. Read the length of the message from the decrypted data array (first 4 bytes);
  9. Check the length of the message: the value must be greater than zero and less than the length of the remaining decrypted data array. If the length is valid, then go to the next item, otherwise ignore the message;
  10. In the decrypted array, we leave only useful data (delete the first 4 bytes and bytes at the end, if the length of the array exceeds the length of the message);
  11. Calculate the hash (sha1) of the decrypted data array;
  12. Calculate the key message (the last 16 bytes of the hash);
  13. Compare the calculated message key with the key read from the received data array. In case of equality, go to the next item, otherwise ignore the message;
  14. We deserialize the decrypted data array into an object representing the received message.

With the theory sorted out. It's time to move on to practice.
Consider the message decryption code for both platforms (no differences or errors were found in the code for generating the shared secret key and encrypting the message, so we omit it). The code corresponds to the latest revision of the master branch. The fundamentally important checks are numbered in the comments (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β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” 

As can be seen from the code, the following checks are performed in the iOS version:
  1. Compare the identifier (hash) of the shared secret from the body of the incoming message with the identifier (hash) of the local shared secret;
  2. Compare the transmitted length of the decrypted message with the minimum and maximum allowable length;
  3. Compare the key (hash) of the received decrypted message with the key (hash) of the original message that was sent by the sender.

Android versions of verification 2 and 3 are missing.

Consider a situation in which the absence of these checks may affect the secret chat:
For a constructive dialogue, let's call Alice and Bob.
And so, the characters:
  1. Bob - companion number 1. Uses Telegram Android for messaging;
  2. Alice - companion number 2. For messaging using any Telegram client;
  3. An attacker is a developer or other person who has physical access to the Telegram server.

Scenario:
  1. Bob initiates a secret chat with Alice to generate a Diffie-Hellman shared secret key (requests p and g from the server; performs checks; generates a and ga; transmits ga to Alice);
  2. Alice receives a secret chat with Bob (requests p and g from the server, performs checks, generates b, gb; generates a shared secret key based on b, ga and p; transmits the identifier (hash) of the shared secret key and gb to Bob);
  3. Bob confirms the secret chat with Alice (generates a shared secret key based on a, gb and p; compares the identifier (hash) of his key with the identifier (hash) of the key received from Alice);
  4. Alice sends an encrypted message to Bob;
  5. Bob receives the message and successfully decrypts it;
  6. The attacker sees Alice's encrypted message sent to Bob. The attacker can not decrypt the message because it does not have access to a shared secret key;
  7. The attacker extracts the following data from the intercepted encrypted message: the shared secret key identifier (hash) (first 8 bytes), the decrypted message key (hash) (next 16 bytes);
  8. The attacker forms a new message on behalf of Alice as follows:
    • The first 8 bytes are equal to the identifier (hash) of the shared secret key from the intercepted message;
    • Next, an array of random data with a length of at least 32 bytes is written (16 bytes is the message key (hash), 4 bytes is the message length, 4 bytes is the class identifier (it will become clear below), 8 bytes is additional data to form a block correct in terms of AES-256 length).

  9. The attacker sends a new message to Bob on behalf of Alice;
  10. Bob receives a new message from Alice, sent by the attacker, and tries to decrypt it:
    • Reads the identifier (hash) of the shared secret key (the first 8 bytes) and successfully compares it with the identifier calculated locally;
    • Reads the key (hash) of the decrypted message (next 16 bytes);
    • Calculates AES-256 symmetric encryption parameters using a shared secret key and the received message key (hash). The obtained parameters are random byte sets and do not correspond to the original encryption parameters;
    • The received parameters are used to decrypt the message (remaining bytes). The message received at the output is a random set of bytes and does not correspond to the original message. Since at this stage there is no verification of the length and key (hash) of the resulting message, the data is transmitted for further processing, despite their deliberate falsity;
    • The first 4 bytes are cut from the resulting message (in the original message, this data is the length of the original message). Further in the code, these 4 bytes are not used anywhere;
    • The rest of the message is passed to the deserializer: TLObject object = TLClassStore.Instance (). TLdeserialize (is, is.readInt32 ());
    • The first 4 bytes of the remaining message are interpreted as a class identifier (the second parameter in the TLdeserialize method). The TLClassStore class contains a dictionary in which the values ​​are classes of different message types, and the keys are class identifiers (constants 4 bytes long). The full contents of the dictionary are presented in the TLClassStore.java class.
      TLClassStore tries to find the class corresponding to the 4 random bytes transferred. If a match is found, a new object of the corresponding class is returned, otherwise null is returned and the incoming message is completely ignored (that is, Bob will not notice this). If successful, the remainder of the message is used to initialize the parameters of the created object. Next, the resulting object is used for its intended purpose. For Bob, this will look like random activity on the part of Alice (for example, a new text message with random content).


The probability of successful creation of an object is approximately equal to 382/2 ^ 32 ≃ 8.9 * 10 ^ -8, where
382 - the number of classes contained in the dictionary;
32 - class identifier length in bits.
The probability is low, of course, but since unsuccessful cases pass unnoticed by the user, an attacker can continuously send messages, limited only by the width of the client’s connection channel to the server. In this case, the attack can be quite feasible. If we assume that the minimum traffic per message can be about 100 bytes, then you will need about 1 GB of traffic to guarantee the creation of an object.

Let us try to estimate the probability of a successful attack in case of at least one of the missed checks:
If there is a message length check: (2 ^ 10/2 ^ 32) * (382/2 ^ 32) ≃ 2.1 * 10 ^ -18, where
2 ^ 10 = 1024 - the maximum valid length of the message, approximately as much memory is occupied by a regular message;
32 = 4 bytes, so much memory is the length of the message.
If there is a verification key (hash) of the message: (1/2 ^ 128) * (382/2 ^ 32)) 2.6 * 10 ^ -46, where
128 - the length of the key (hash) messages.

It should be noted that verification of the message signature is present at other levels of protection. For example, when installing a client-server connection (using the same principle as when exchanging messages): 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β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” 

Although it looks a bit strange, but I still do not think that in the absence of verification of the signature some malicious intent is hidden, since the vulnerability is not critical. On the other hand, there may be other vulnerabilities, which together with this give a greater profit.

However, at the moment, the developers have made the necessary edits to the Dev branch and updated the build on Google Play. I would also like to note the fact that for the defects I found, the developers paid a reward of $ 5000. As they say "not a trifle and nice."

Source: https://habr.com/ru/post/247409/


All Articles