Research By: Dikla Barda, Roman Zaikin and Oded Vanunu
As of early 2018, the Facebook-owned messaging application, WhatsApp, has over 1.5 billion users with over one billion groups and 65 billion messages sent every day. With so much chatter, the potential for online scams, rumours and fake news is huge. It doesn’t help then, if threat actors have an additional weapon in their arsenal to leverage the platform for their malicious intentions.
Check Point Research, however, recently unveiled new vulnerabilities in the popular messaging application that could allow threat actors to intercept and manipulate messages sent in both private and group conversations, giving attackers immense power to create and spread misinformation from what appear to be trusted sources.
Our team observed three possible methods of attack exploiting this vulnerability – all of which involve social engineering tactics to fool end-users. A threat actor can:
Following the process of Responsible Disclosure, Check Point Research informed WhatsApp of their findings. From Check Point Research’s view, we believe these vulnerabilities to be of the utmost importance and require attention.
Please read below for our full technical analysis.
Demonstration Video of the Attacks in Action
Technical Analysis
As is well known, WhatsApp encrypts every message, picture, call, video or any other type of content you send so that only the recipient can see it. What’s more, not even WhatsApp has the ability to view those messages.
Figure 1: WhatsApp Encrypted Chat
These encryption processes caught our attention and we decided to try to reverse WhatsApp’s algorithm to decrypt the data. Indeed, after decrypting the WhatsApp communication we found that WhatsApp is using the “protobuf2 protocol” to do so.
By converting this protobuf2 data to Json we were able to see the actual parameters that are sent and manipulate them in order to check WhatsApp’s security.
The outcome of our research is a Burp Suit Extension and 3 Manipulation methods.
To start the manipulation, though, we first have to get the private and public key of our session and fill it in our burpsuit extension.
If you are interested in a detailed explanation about how the encryption actually works behind the scenes, please read the encryption paragraph at the end of this blog post.
Accessing the Keys
The keys can be obtained from the key generation phase from WhatsApp Web before the QR code is generated:
Figure 2: Public and Private Key of the Communication
After we take these keys we need to take the “secret” parameter which is sent by the mobile phone to WhatsApp Web while the user scans the QR code:
Figure 3: The Secret Key from the WebSocket
As a result of this, our extension will look like the below:
Figure 4: WhatsApp Decoder Burp Extension
After clicking on “Connect”, the extension connects to the extension’s local server, which will perform all the tasks required for the extension.
Manipulating WhatsApp
By decrypting the WhatsApp communication, we were able to see all the parameters that are actually sent between the mobile version of WhatsApp and the Web version. This allowed us to then be able to manipulate them and start looking for security issues.
This resulted in us being able to carry out a variety of attack types, which are described below.
Attack 1: Change the Identity of a Sender in a Group Chat, Even If They Are Not a Member of the Group
In this attack, it is possible to spoof a reply message to impersonate another group member and even a non-existing group member, for example, ‘Mickey Mouse’.
To impersonate someone from the group, all the attacker need do is catch the encrypted traffic:
Figure 5: Encrypted WhatsApp Communication
Once the traffic is captured, he can simply send it to an extension which will then decrypt the traffic:
Figure 6: Decrypting the WhatsApp Message
By Using Our Extension
The interesting parameters to note here are:
And this is the point where interesting things begin to happen…
For example, we can change the conversation to something else. The message with the content “Great!” sent by a member of a group, for instance, could be changed to something else like: “I’m going to die, in a hospital right now” and the participant parameter could also be changed to someone else from the group:
Figure 7: A Spoofed Reply Message
Note that we have to change the id to something else because it is already sent and appears in the database.
In order to make everyone see the new spoofed message the attacker needs to reply to the message he spoofed, quoting and changing that message (“Great”) in order for it be sent to everyone in the group.
As you can see in the below screenshot, we created a new group where no previous messages were sent, and by using the method from above we were able to create a fake reply.
Figure 8: The Original Conversation
The ‘participant’ parameter can also be a text or a phone number of someone that is not in the group, which would cause everyone in the group to believe that it actually is sent from this participant.
For example:
Figure 9: Changing The Content Of The Message
By Using Our Debugging Tool
…and the result will look like this:
This would again be sent to everyone in the group as before.
Figure 10: Reply To a Message That Sent From
Someone Outside of the Group
Attack 2: Changing a Correspondent’s Reply To Put Words in Their Mouth
In this attack, the attacker is able to manipulate the chat by sending a message back to himself on behalf of the other person, as if it had come from them. By doing so, it would be possible to incriminate a person, or close a fraudulent deal, for example.
In order to spoof the messages, we have to manipulate the ‘fromMe’ parameter in the message, which indicates who sent the message in the personal chat.
This time we will capture the outgoing message from WhatsApp Web before it is even sent to our Burp Suite. In order to do that we can put a break point on the aesCbcEncrypt function and take the data from the ‘a’ parameter:
Figure 11: OutGoing Message Manipulation
We will then copy this data to our Burp extension and select the outgoing direction. By pressing on “Decrypt”, our extension will decrypt the data:
Figure 12: Decryption of Outgoing Message
After changing it to false and encrypting it back we then get the below result:
Figure 13: Encryption of Outgoing Message
We have to then modify the ‘a’ parameter in our browser, and the result will be a push notification with the content. In this way it is even possible to spoof the entire chat.
Figure 14: Sending Messages To Myself
on Behalf of Someone Else.
The whole conversation will then look like this:
Figure 15: Sending Messages To Myself
on Behalf of Someone Else
Attack 3: Send a Private Message in a Chat Group But When The Recipient Replies, The Whole Group Sees It.
In this attack, it is possible to send a message in a group chat that only a specific person will see, though if he replies to this message, the entire group will see his reply.
In this way it is possible to manipulate a certain member of the group and ‘trip them up’ in order to have them reveal information to the group that they may otherwise not want them to know.
We found this attack vector while we reversed the Android mobile app. In this instance, we found that if the attacker manipulates a simple message in the group, such as “We are the team”, we will actually find this message in ‘/data/data/com.whatsapp/databases/msgstore.db’ database – as seen below.
Figure 16: Sending a Private Message in the Group Chat
We will find this message in ‘/data/data/com.whatsapp/databases/msgstore.db’ database
Then, if we open the conversation on a mobile phone by using the sqlite3 client and issue the following command:
SELECT * FROM messages;
We will see the following data:
Figure 17: Manipulation of the Database
In order to send a message to the group, but restrict it to only a specific group member, we have to set his number under the ‘remote_resource’ parameter.
The trick here is to simply change the ‘key_from_me’ parameter from 0 to 1
Having done this, we will then run the following command and update the key_from_me and the data:
update messages set key_from_me=1,data=”We, all know what have you done!” where _id=2493;
The attacker needs to then close and reopen his WhatsApp to force the application to send the new message. After doing so, the result will be as below:
Notice that only the victim received the message?
If the victim writes something as a response, everybody in the group will get his response, but if he will reply to the message only he will see the replied content and all the others will see the original message…!!
WhatsApp Encryption Explained
Source code: https://github.com/romanzaikin/BurpExtension-WhatsApp-Decryption-CheckPoint
Let’s start with WhatsApp Web. Before generating the QR code, WhatsApp Web generates a Public and Private Key that is used for encryption and decryption.
Figure 23: Private and Public Key of the Conversation
Let’s call our private Key ‘priv_key_list’ and our public Key ‘pub_key_list’.
These keys were created by using curve25519_donna by using random 32 bytes.
Figure 24: Encryption Process Curve25519
To decrypt the data we will start to create a decryption code. This will take the private key from WhatsApp Web instead of the random bytes because we need to have the same keys in order to decrypt the data:
self.conn_data[“private_key”] = curve25519.Private(“”.join([chr(x) for x in priv_key_list]))
self.conn_data[“public_key”] = self.conn_data[“private_key”].get_public()
assert (self.conn_data[“public_key”].serialize() == “”.join([chr(x) for x in pub_key_list]))
Then, after the QR code is created, after scanning it with a phone we can send the following information to Whatsapp Web over a websocket:
Figure 25: The Secret Key From WebSocket
The most important parameter here is secret which then passes to setSharedSecret. This will divide the secret into multiply parts and configure all the cryptographic functions we need in order to decrypt the WhatsApp traffic.
First, we can see that there is a translation from String ‘e’ into Array and some slices which divide the secret into two parts: ‘n’, which is the first 32 bytes and ‘a’, which is the characters from 64th byte to the end of the ‘t’.
Figure 26: Getting the SharedSecret
And if we dive into the function’ ‘E.SharedSecret’, we can see that it uses two parameters which were the first 32 bytes and the private key from the QR generation:
Figure 27: Getting the SharedSecret
Following this, we can then update our python code and add the following line:
self.conn_data[“shared_secret”] = self.conn_data[“private_key”].get_shared_key(curve25519.Public(self.conn_data[“secret”][:32]), lambda key: key)
Next we have the expended which is 80 bytes:
Figure 28: Extending the SharedSecret
By diving in we can see that the function uses the HKDF function. So we found function ‘pyhkdf’ and use it in our code to expend the key in the same way that WhatsApp did:
shared_expended = self.conn_data[“shared_secret_ex”] = HKDF(self.conn_data[“shared_secret”], 80)
We next have the hmac validation function which takes the expended data as parameter ‘e’ and divides it into 3 parameters:
There is also the parameter, ‘s’, which is a concatenation of the parameter ‘n’ and ‘a’, from the function before which forms part of our secret.
Figure 29: HmacSha256
Then the function HmacSha256 will be called with the parameter ‘r’ and it will sign the data with the parameter ‘s’, after that ‘n’ we will receive the hmac verifier which will be compared to ‘r’, which is a slice of ‘t’ from 32byte to 64 bytes, and ‘t’ is our secret in the array format, as seen previously.
Figure 30: Checking the validity of the messages
In python it will look like this:
check_hmac = HmacSha256(shared_expended[32:64], self.conn_data[“secret”][:32] + self.conn_data[“secret”][64:]) if check_hmac != self.conn_data[“secret”][32:64]:
raise ValueError(“Error hmac mismatch”)
The last encryption related function in this block is ‘aesCbcDecrypt’ which uses the parameter ‘s’ which is a concatenation between the data from byte 64 to the end of expended shared and the data from byte 64 of the secret, and ‘i’ which is the first 32bytes of expended shared.
Figure 31: Getting the AES key and the MAC key
The result is the decrypted key which we will use later. So, if we translate the code it will look like this:
keysDecrypted = AESDecrypt(shared_expended[:32], shared_expended[64:] + self.conn_data[“secret”][64:]) After the decryption, we will have new ‘t’ which is the first 32 bytes, which is the encryption key, and the next 32 bytes, which is the mac key:
self.conn_data[“key”][“aes_key”] = keysDecrypted[:32]
self.conn_data[“key”][“mac_key”] = keysDecrypted[32:64]
The whole code will then look like this:
self.conn_data[“private_key”] = curve25519.Private(“”.join([chr(x) for x in priv_key_list]))
self.conn_data[“public_key”] = self.conn_data[“private_key”].get_public()
assert (self.conn_data[“public_key”].serialize() == “”.join([chr(x) for x in pub_key_list]))
self.conn_data[“secret”] = base64.b64decode(ref_dict[“secret”])
self.conn_data[“shared_secret”] = self.conn_data[“private_key”].get_shared_key(curve25519.Public(self.conn_data[“secret”][:32]), lambda key: key)
shared_expended = self.conn_data[“shared_secret_ex”] = HKDF(self.conn_data[“shared_secret”], 80)
check_hmac = HmacSha256(shared_expended[32:64], self.conn_data[“secret”][:32] + self.conn_data[“secret”][64:])
if check_hmac != self.conn_data[“secret”][32:64]:
raise ValueError(“Error hmac mismatch”)
keysDecrypted = AESDecrypt(shared_expended[:32], shared_expended[64:] + self.conn_data[“secret”][64:])
self.conn_data[“key”][“aes_key”] = keysDecrypted[:32]
self.conn_data[“key”][“mac_key”] = keysDecrypted[32:64]
So, after we have the code that can regenerate all the encryption parameters needed we can continue to the decryption process.
To do that we start with capturing a message:
Figure 32: The Encrypted Incoming Message
As you can see, the message is split into two parts: the tag and the data. We will use the following function to decrypt the message:
def decrypt_incoming_message(self, message):
message = base64.b64decode(message)
message_parts = message.split(“,”, 1)
self.message_tag = message_parts[0]
content = message_parts[1]
check_hmac = hmac_sha256(self.conn_data[“mac_key”], content[32:])
if check_hmac != content[:32]:
raise ValueError(“Error hmac mismatch”)
self.decrypted_content = AESDecrypt(self.conn_data[“aes_key”], content[32:])
self.decrypted_seralized_content = whastsapp_read(self.decrypted_content, True)
return self.decrypted_seralized_content
As you can see, we receive the data in base64 format in order to copy the Unicode data easily, In Burp we can encode the data to base64 by simply pressing ctrl+b and pass it to the function decrypt_incomping_message. This function splits the tag from the content and checks if our key can decrypt the data by comparing the hmac_sha256(self.conn_data[“mac_key“], content[32:]) with content[:32].
If everything fits we can continue to the AES decryption step which uses our aes key and the content from the 32byte.
This content contains first the IV ,which is in size of aes block size, and then the actual data:
self.decrypted_content = AESDecrypt(self.conn_data[“aes_key”], content[32:])
The output of this function will be a protobuf, which looks like this:
Figure 33: The Decrypted Message with Protobuf
In order to translate it to json we will use the ‘whatsapp_read’ function.
WhatsApp Encryption Explained (Decrypt Incoming Message):
In order to decrypt a message, we first have to understand how the WhatsApp protocol works so we started by debugging the function e.decrypt:
Figure 34: ReadNode Function
This function will trigger readNode which has the following code:
Figure 35: ReadNode Function
We translated all the code to python to represent the same function which looks like this:
This code first reads a byte from the stream and moves it to char_data. It then tries to read the list size of the incoming stream by using the function read_list_size.
Then we got another byte which we will call token_byte that will be passed to read_string and looks like this:
Figure 36: ReadString Function
This code uses getToken and passes our parameter as a position in the token array:
Figure 37: getToken Function
This is the first item whatsapp sends in the communication, then we translated all the functions in the function readString and continued with the debugging:
Next you can see the function ‘readAttributes’ in the function readNode:
Figure 38: readAttribues function
This function just continues to read more bytes from the stream and parse them via the same token list we saw before when we parsed the “action” token, which will look like this:
So the second parameter that WhatsApp sends is the actual action to the messenger where we can see that WhatsApp sent {add:”replay”} which means a new message arrived.
Basically we will continue with the code until we get to the end of readNode which will give us the three parts of the message that was sent:
So, until this point we got the first and the second part easily by rewriting all the functions to python, which is very straight forward.
Figure 39: Decrypted Array
Next we have to deal with the third parameter which is the protobuf and decrypt it.
To get the protobuf we can look at the protobuf scheme implemented by Whatsapp and just copy it into a clean .proto file which can be obtained from here:
Figure 40: protobuf
The indexes can be also copied from Whatsapp protobuf schema and compiled to python protobuf file by using:
Then we can translate the protobuf to json easily by using the python functions generated by the protobuf…
…and the result will look like this:
Figure 41: Decrypted Data
After implementing that inside our extensions we were able to decrypt the communication:
Figure 42: Using Our Extension to Decrypt the Data
WhatsApp Encryption Explained (Encrypt Incoming Message)
The encryption process is pretty much the same as the encryption but in the opposite order, so this time we will reverse the writeNode function:
Figure 43: writeNode function
Which is implemented like this:
Figure 44: writeNode function
As you can see this time we already have the token and the token attributes which we have to translate to its position in the token lists, and then just reimplement all the function in the same way as we did in the readNode:
The code is very straight forward; first we check if the node we got is in length of three. Then we multiply the number of token attributes by two and pass it to writeListStart which will write the start of the list character and then the list size (the same thing that we saw in readNode):
After we have the start list we will get into writeString which performs the same thing as readString, as you can see “action” translated to ten which is “action” position in the tokens index and so on:
Figure 45: writeToken function
We translated the code and all the functions, which look like the below:
then the code go into writeAttributes which will translate the attributes and from then to writeChildren which will translate the actual data.
Figure 46: writeChildren function
We translated this function which looks like the below:
This way we build the data back, so our code that decrypts and encrypts the messages will look like this:
To simplify the encryption process we changed the actual writeChildren function and added another instance type to make the encryption simpler:
The result is the encryption and decryption of incoming data.
To decrypt outgoing data please see code our github:
https://github.com/romanzaikin/BurpExtension-WhatsApp-Decryption-CheckPoint