本文翻译自:
https://research.checkpoint.com/fakesapp-a-vulnerability-in-whatsapp/
作者:Dikla Barda, Roman Zaikin,Oded Vanunu
WhatsApp拥有用户15亿,有超过10亿个群组,每天发送消息超过650亿(2018年初数据)。大量的用户和消息规模下,出现垃圾邮件、谣言、虚假消息的概率也很大。
Check Point研究人员近期发现WhatsApp中存在漏洞,攻击者利用漏洞可以拦截和伪造个人聊天和群组聊天会话消息,这样攻击者就可以传播垃圾邮件、谣言、虚假消息了。
研究人员发现了三种利用该漏洞的攻击方法,这三种方法都是用社会工程技巧来欺骗终端用户。攻击者可以:
用群组聊天中的引用(quote)特征来改变发送者的身份,即使发送者不是群成员;
修改其他人的回复消息(以发送者的口吻);
伪装成公开消息,发送私聊消息给另一个群组成员,当目标个人回复后,会话中所有人都会看到该消息。
https://www.youtube.com/embed/rtSFaHPA0C4
WhatsApp会加密发送的消息、图片、语言通话、视频通话和所有形式的内容,这样只有接收者能看到。但不止WhatsApp可以看到这些消息。
图1: WhatsApp加密的聊天
研究人员决定分析加密过程,对算法进行逆向来解密这些数据。解密了WhatsApp的通信后,研究人员发现WhatsApp使用的是protobuf2协议。
把protobuf2数据转变成json数据就可以看到发送的真实参数,然后研究人员伪造了参数数据来验证WhatsApp的安全性。
研究人员利用Burp Suit Extension and 3 Manipulation方法对其进行研究。
在伪造之前,研究人员先获取了session的公钥和私钥,并填入burpsuit扩展中。
在QR码生成之前,可以从WhatsApp web端的密钥生成阶段获取密钥:
图2: 通信用的公钥和私钥
想要获取密钥,就要获取用户扫描QR码后手机发给WhatsApp web端的秘密参数:
图3: WebSocket中的秘密密钥
扩展给出的结果:
图4: WhatsApp Decoder Burp Extension
点击连接(connect)后,扩展会连接到扩展的本地服务器,服务器会执行扩展所需的所有任务。
解密了WhatsApp的通信后,就可以看到手机端WhatsApp和web端之间发送的所有参数。然后就可以伪造消息了,并以此检查WhatsApp的安全性。
三种攻击场景描述如下:
在这种攻击者,可以伪造回复消息来模仿另一个群组人员,即使该群组成员并不存在,比如Mickey Mouse。
为模仿群组中的人,攻击者需要抓取这样的加密流量:
图5: 加密的WhatsApp通信
一旦获取流量后,就可以发送给扩展,扩展会解密流量:
图6: 解密的WhatsApp消息
使用扩展时应注意以下参数:
了解了这些参数之后就可以伪造会话消息了。比如,群成员发送的“great”内容可以修改为“I’m going to die, in a hospital right now”,参与者的参数也可以修改为其他人:
图7: 伪造的Reply消息
Id也有修改,因为数据库中已经存在该id了。
为了让每个人都看到伪造的信息,攻击者需要回复他伪造的消息,引用并修改原始消息(将great修改为其他),然后发送给群里的其他人。
如下图所示,研究人员创建了一个没有消息记录的新群组,然后使用上面的方法创建了假的回复:
图8: 原始会话
参数participant
可以是文本或不在群中的某人的手机号,这会让群人员认为这真的是该成员发送的消息。比如:
图9: 修改消息内容
使用调试工具,结果就是:
图10: 回复来自群外人员的消息
在攻击2中,攻击者能以其他人的口吻发送消息以达到修改聊天的目的。这样,就可以模仿他人或完成欺骗交易。
为了伪造消息,必须修改消息的fromMe参数,表示在个人会话中发送消息。
从web端发送的消息在发送到Burp suite之前,我们对其进行分析。可以在aesCbcEncrypt
函数上设置一个断点,从a
参数出获取获取。
图11: OutGoing消息修改
然后复制数据到Burp扩展中,选择outgoing direction
,然后解密数据:
图12: 解密Outgoing Message
在将其改为false
,然后加密后,得到下面的结果:
图13: Outgoing Message加密
然后要修改浏览器的a
参数,结果是含有内容的推送通知。这样甚至可以欺骗整个会话。
图14: 发送消息给自己
如果是其他人的话,整个会话应该是这样的:
图15: 发送消息给自己,别人看到的结果
在这种攻击下,可以修改群组中的特定成员,在群聊中发送私聊信息,当接收者回复给消息时,整个群成员都可以看到回复的内容。
研究人员通过逆向安卓APP发现了攻击向量。在该实例中,研究人员发现如果攻击者在群中修改了一个消息,那么就会在数据库/data/data/com.whatsapp/databases/msgstore.db
中看到该消息。
图16: 在群聊中发送私聊消息保存在/data/data/com.whatsapp/databases/msgstore.db数据库中
可以使用sqlite3客户端使用下面的命令打开会话:
SELECT * FROM messages;
可以看到下面的数据:
图17: 修改后的数据库
为了在群中发送消息,但限制消息只能某个特定群成员才能看到,因此要设定remote_resource
参数。
这里的使用的方法就是将key_from_me
参数从0
修改为1
。
完成这些动作后,运行下面的命令,更新key_from_me
和数据:
update messages set key_from_me=1,data=”We, all know what have you done!” where _id=2493;
攻击者需要重启WhatsApp客户端来强制引用发送新消息。之后的结果就是:
只有特定的受害者接收到了消息。
如果受害者写消息回应(writes something as a response),那么群组内的所有人都可以看到;但如果受害者直接回复(reply to)消息的话,只有他自己可以看到回复的内容,但其他人就可以看到原始消息。
源码:https://github.com/romanzaikin/BurpExtension-WhatsApp-Decryption-CheckPoint
WhatsApp Web端在生成QR码之前,会生成一对公约和私钥用于加密和解密。
图23: 会话用的公钥和私钥
以下称私钥为priv_key_list
,称公钥为pub_key_list
。
密钥是用随机的32字节用curve25519_donna生成的。
图24: Curve25519加密过程
为了解密数据,需要创建解密码。这就需要从WhatsApp Web端提取私钥,因为需要私钥才可以解密数据:
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]))
然后,QR码就创建了,在用手机扫描QR码之后,就可以通过websocket
发送信息给Whatsapp Web端了:
图25: 来自WebSocket的秘密密钥
最重要的参数是加密的,之后会传递给setSharedSecret
。这会将密钥分成三个部分,并且配置所有解密WhatsApp流量所需的加密函数。
首先,是从字符串e
到数组的翻译,有些部分会把密钥分成前32字节的n和第64字节到结尾t
的a
两部分。
图26: 获取SharedSecret
深入分析函数E.SharedSecret
,发现它使用前32字节和生成QR码的私钥作为两个参数:
图27: 获取SharedSecret
然后可以在python脚本中加入下面的代码:
self.conn_data[“shared_secret”] = self.conn_data[“private_key”].get_shared_key(curve25519.Public(self.conn_data[“secret”][:32]), lambda key: key)
然后是扩展的80字节:
图28: 扩展SharedSecret
分析发现该函数使用HKDF函数,所以看到了函数pyhkdf
,还被用于扩展key:
shared_expended = self.conn_data[“shared_secret_ex”] = HKDF(self.conn_data[“shared_secret”], 80)
然后,hmac验证函数会将扩展的数据看作参数e
,然后分成三部分:
还有一个参数s
,用来将参数n
和a
连接在一起。
图29: HmacSha256
然后用参数r
调用HmacSha256
函数,函数会用参数s
对数据进行签名,之后就收到hmac
验证,并于r
进行比较。
r
是t
的32字节到64字节,t
是数组格式的加密数据。
图30: 检查消息的有效性
Python代码如下:
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”)
最后与加密相关的函数是aesCbcDecrypt
,它用参数s
将64字节之后的扩展数据、扩展数据的前32字节(参数i
)和secret 64字节之后的数据连接在一起。
图31: 获取AES key和MAC key
解密密钥随后会使用,然后对代码进行翻译:
keysDecrypted = AESDecrypt(shared_expended[:32], shared_expended[64:] + self.conn_data[“secret”][64:])
解密后,就得到t即前32字节数据,也就是加密密钥,之后的32字节数据就是mac密钥:
self.conn_data[“key”][“aes_key”] = keysDecrypted[:32]
self.conn_data[“key”][“mac_key”] = keysDecrypted[32:64]
整体代码如下:
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]
有了生成QR码的所有加密参数,就可以加入解密过程了。
首先,拦截(获取)消息:
图32: 收到的加密后的消息
可以看到,消息是分成两部分的:tag和数据。可以用下面的函数解密消息:
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
从中可以看出,为了方便复制Unicode数据,接收的数据是base64编码的。在burp中,可以用ctrl+b对数据进行base64编码,然后传递给函数decrypt_incomping_message
。函数会把tag与内容分割开,然后通过比较hmac_sha256(self.conn_data[“mac_key“], content[32:])
和content[:32]
来检查密钥是否可以解密数据。
如果都匹配的话,那么继续进入AES解密步骤,需要使用AES Key和32字节的内容。
内容中含有IV
,也就是aes区块的大小,然后是真实数据:
self.decrypted_content = AESDecrypt(self.conn_data[“aes_key”], content[32:])
函数的输出是protobuf
(是google 的一种数据交换的格式,它独立于语言,独立于平台):
图33: Protobuf格式的加密数据
然后用whatsapp_read
函数将其翻译为json
格式。
为了解密收到的消息,首先要了解WhatsApp协议的工作原理,所以要调试函数e.decrypt
:
图34: ReadNode函数
ReadNode
函数会触发readNode
:
图35: ReadNode函数
把所有代码翻译为python来表示相同的功能:
代码首先从数据流中读取一字节的内容,然后将其移动到char_data
,然后用函数read_list_size
读取入数据流的列表大小。
然后调用token_byte
获取另一个字节,token_byte
会被传递给read_string
:
图36: ReadString函数
代码使用了getToken
,并把参数传递到token数组的一个位置上:
图37: getToken函数
这是通信中WhatsApp发送的第一项,然后翻译readString
函数中的所有函数,并继续调试:
然后就可以看到readNode
函数中的readAttributes
函数:
图38: readAttribues函数
readAttributes
函数会继续从数据流中读取字节,并通过相同的token列表进行语法分析:
WhatsApp发送的第二个参数是消息的真实动作,WhatsApp发送{add:”replay”}
表示新消息到达。
继续查看readNode
函数代码,看到发送的消息的三个部分:
图39: 解密的数组
接下来要处理的是第三个参数protobuf
,然后解密。
为了了解Whatsapp使用的protobuf
方案,将其复制到空的.proto
文件中:
图40: protobuf
索引也可以从Whatsapp protobuf方案中复制,并编译为python protobuf文件:
然后用python函数将protobuf翻译为json。
图41: 解密的数据
在扩展中应用之后就可以解密通信了:
图42: 使用扩展来解密数据
加密的过程与解密过程相似,就是顺序不同,这里要逆向的是writeNode函数:
图43: writeNode 函数
图44: writeNode函数
有了token和token属性之后,那么需要做的与readNode
中一样:
首先,检查节点长度是不是3;然后给token属性数乘2,并传递给writeListStart
,writeListStart
会写类别字符的开始和列表大小,与readNode
一样:
然后进入writeString
,可以看到翻译为X
的action和token index
中action的位置:
图45: writeToken函数
翻译代码和所有函数:
writeAttributes
会翻译属性,之后由writeChildren
翻译真实数据。
图46: writeChildren函数
翻译函数:
解密和解密消息如下:
为了简化加密的过程,研究人员修改了真实的writeChildren
函数,然后添加了另一个实例来让加密过程更简单:
结果就是加密和解密的收到的消息。
解密发送的数据请查看github代码:
https://github.com/romanzaikin/BurpExtension-WhatsApp-Decryption-CheckPoint