针对 Telegram Passport 的填充 Oracle 攻击

作者:TheZero是KeePassXC的维护者,以前是F-Droid的合作者。

自Telegram Passport问世以来,我试图分析其协议,以了解用户数据的加密是否强大且正确实施。

该规范描述了一种集中式加密存储,其中“用户将文档上传一次,然后可以立即与需要实际ID的服务(金融和ICO等)共享数据。”如果你想阅读完整的规范,可以查看Telegram的说明文档(https://core.telegram.org/passport)。

本文将着重介绍由Telegram设计的与AES-CBC一起使用的自定义填充(padding),以及它如何可能被滥用。Telegram客户端与第三方服务共享用户文档时用到该流程。

注意:本文不会描述针对Telegram服务的直接漏洞。

TelegramPassport的工作流程

该规范涉及3个实体:

1. 最终用户

2. Telegram Passport服务器,用户的文档存储在这里

3. 将接收文档的第三方服务(比如Telegram机器人)

如上所述,我将注意力放在了第3个实体以及第三方服务为了能从Telegram获取用户的数据需要实现的解密机制上。规范中定义该交互的部分如下:

用户文档(规范中名为“登录信息数据”)用AES256-CBC来加密。该操作模式有一定的延展性:在解密过程中,改变密文的一位就会导致相应的明文块完全损坏,但它倒置后面明文块中相应的位,而其余块原封不动。

该属性使实现的CBC模式容易受到填充oracle攻击(paddingoracle attack)。更具体地说,攻击者想要攻破CBC加密机制,只要知道某个特定的密文是否生成拥有有效填充的明文。如果你可以输入密文,并设法查明它们是否解密成有效填充的明文,你就可以解密任何特定的密文。

如果你想了解填充oracle攻击的更多信息,请阅读这篇很棒的文章(https://robertheaton.com/2013/07/29/padding-oracle-attack/)。

就个人而言,我对攻击者针对Telegram实施填充攻击很感兴趣也很惊讶。它不是像PKCS#5或PKCS#7那样的标准填充,而是自定义填充:原始数据填充了任意数量的随机字节,在32个和255个之间,使有效载荷的大小是16(AES块大小)的倍数;填充的第一个字节包含用于填充的字节数,包括它本身。

具体场景

规范不是很清楚,但我们从客户的源代码可以看到发生了什么。下面用Python表明了填充的工作机理:

EncryptedCredential是一个加密的JSON对象,它含有3个字段:data、hash和secret。我们并不关心有多少个填充字节。重要的是含有填充长度的那个“魔法字节”(magic byte)。

data包含用base64编码的、经过加密的JSON序列化数据

hash包含用base64编码的数据哈希值,用于数据验证

secret包含用base64编码的私密内容,以机器人的公共RSA密钥来加密。

示例:

请注意,JSON对象始终以结束。继续介绍下文之前,请注意Telegram要求开发人员检查:解密的填充数据的SHA-256哈希值与JSON有效载荷中提供的哈希值匹配,实际上使填充oracle攻击无法得逞。然而,如果第三方开发人员未执行这项检查,这种攻击仍然有效。

在我们的场景中,将接收该加密JSON对象的第三方服务(比如Telegram机器人)将执行下列步骤:

1. 使用机器人的私钥解密secret有效载荷

2. 像上述规范那样,计算key和iv

3. 解密data有效载荷

4. 逆填充data明文:

1)搜索明文中的第一个}字符

2)将下一个字节读取为“填充字节”

3)检查填充长度是否与“填充字节”的值匹配

4)如果没有}字符或填充长度不匹配,报告填充错误

5. 读取JSON data。如果JSON无效,报告编码错误,否则报告成功。

执行攻击

针对PKCS#5的标准填充oracle攻击先蛮力攻击密文的最右边字节,直到填充正确,这意味着解密的明文以0x01结束。

在本例中,我们无法以这种方式执行攻击。最后一个N字节是随机字节,所以采用蛮力攻击不会获得任何有用的结果。此外,由于我们采用了倒退法,迟早我们会修改含有“填充字节”的密文块;由于CBC的延展性作用于下一个块,我们会破坏所有块。

我改而使用的方法是从左到右对密文逐个字节地进行位翻转(与255进行异或运算)。

CBC位翻转

从密文最左边的字节开始,对其进行位翻转,那样第一个明文块就会被破坏,但第二个明文块中的第一个字节也会被位翻转。这样我们会破坏当前块中的实际的JSON daba,但是当我们遇到下一个块中的}字符时,服务将报告填充错误。

这种方法容易出现误报。比如说,考虑这种情况:损坏的数据返回新的}字符,无意中后面跟着确切的填充长度;服务报告填充错误时,我们将对同一字节执行另一个测试,改而用127与它进行异或运算。如果服务再次返回填充错误,我们基本上可以确信找到了“填充字节”。

借助这种机制,我们可以了解关于填充长度和有效载荷长度的信息,并且知道针对实际的填充oracle攻击从何处入手。

CBC位翻转

假设明文以下面这种方式组成:

}字符在第20位

填充长度字节在第21位

想了解第19位的明文值,我们需要得到其中间值,然后用该位置的密文值与它进行异或运算。

第一个块包含空字节(\x00),除了第3个字节和第4个字节,这是我们想要蛮力攻击的两个字节。我们可以蛮力攻击明文中的第19个字节和第20个字节,只要蛮力攻击密文中的第3个字节和第4个字节(19-16= 3),并为填充oracle提供2个块:

第二个块包含我们想要解密的加密字节

一旦我们了解了第19个位置的值,就可以针对第18个字节和第19个字节重复同样的过程,依此类推获得所有明文*。

如上所述,这种方法也容易出现误报,所以一旦我们找到了有效值v,就通过把第一个块中的空字节换成\ xff来执行验证步骤。

*遗憾的是,在我们的场景中,我们无法解密所有明文,因为第一个块用我们不了解的IV进行了异或运算。然而,考虑到第一个块以{"data":" + base64_of_image_header开头,我们可能会蛮力攻击它。

“给你看看代码!”

我在撰写本文时,编写了一个实现我们定义的场景的简单服务器以及执行填充oracle攻击的客户端,以演示我们的发现。

代码很糟糕,远远谈不上是稳定版本,不过你可以在这里下载(https://github.com/PequalsNP-team/pequalsnp-team.github.io/tree/master/assets/TelegramPassport)。

使用python2 tpassport.py运行服务器,然后使用python3passport_oracle.py运行客户端。你可能需要解决某些依赖项才能运行脚本。

结论

你将哪种类型的填充与CBC模式一起使用并不重要,总有一种方法可以用在填充oracle场景中以解密密文。

如果你计划使用CBC,始终在加密之前以一种先加密后MAC的方式执行完整性检查;如果可能的话,使用HMAC对数据进行验证。如果可能的话,根本不要使用CBC,而是青睐像GCM这样的AE/AEAD模式。

最后但并非最不重要的是:别使用你自己的加密方法!

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180805A13PRH00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券