Telegram session劫持探索

本文主要以telegram desktop源码作为起点,对其用户文件和认证过程进行学习和探索,从而更好的理解session劫持内部的机理。

文件结构

目录结构

Telegram用户数据一般存放在与telegram.exe同目录下的tdata文件夹内。

一个有效的Telegram登录用户的目录结构如下:

可以看到,大部分文件可以通过文件名来间接的了解文件内部数据所代表的的含义,但仍有一部分文件名由数字+字母(A~F)构成,比如:D877F783D5D3EF8C,并且telergram的大部分文件的内容为不可读的二进制数据。因此在这里通过阅读源码的方式来探索文件名的含义,从而进一步了解telegram文件的数据构成和登录session的存储位置。

数据文件

A.文件结构

Telegram/SourceFiles/storage/details/storage_file_utilities.cpp:ReadFile()函数中可以很清楚的看到telergram读取文件的逻辑。数据文件主要分为四个部分,依次为魔数,版本号,加密数据和签名,其中魔数占据文件的前四个字节,固定为TDF$(telegram desktop file),版本号占据后四个字节(当前为0x002dd278 = 3003000 = v3.3.0),接下来是加密数据,最后文件末尾为16个字节的签名。

在每次读取文件时,Telegram会通过计算“加密数据 + 版本号 + 魔数”的md5值来验证数据的完整性。若计算的结果与文件末尾的签名不一致,则跳过对当前文件的读取。

telegram对部分文件有独特的命名方法,具体在Telegram/SourceFiles/storage/details/storage_file_utilities.cpp:GenerateKey()函数中进行定义。

GenerateKey()生成了一个类型为quint64的随机数,接着调用ToFilePart()函数对随机数进行转换。

ToFilePart()函数使用& 0x0f操作来获取val的最后一个字节,并转换为16进制的字符形式,通过循环16次来得到长度为16,由数字+字母(A~F)构成的字符串。

B.数据解密

DecryptLocal()函数是telegram用来进行数据加密的主要函数,使用的是1978年由Campbell提出的AES-IGE(Infinite Garble Extension)加密模式对数据进行加密,主要步骤如下:

python的伪代码如下:

这里就不花费篇幅贴源代码了,感兴趣的同学可以关注storage_file_utilities.cpp, mtproto_auth_key.h, mtproto_auth_key.cpp 这三个文件,加密的逻辑都在这里了(最终调用的是openssl的加解密接口)。

最后,telegram对于加密文件的解析(storage_file_utilities.cpp: ReadEncryptedFile())主要进行了以下三步操作:

缓存文件

telegram的缓存文件存储在tdata/user_data/目录下,其中体积较小的文件放在cache目录,体积较大的文件则放在media_cache目录下。缓存文件使用PlaceFromId()函数生成随机的文件名,这个函数与GenerateKey()函数的处理逻辑基本一致,区别在于生成的文件名长度为14,并且前两个字节作为上层的文件名,以此来对缓存文件进行分类。

PlaceFromId()的代码如下:

A.文件结构

Telegram缓存文件的文件结构如图:

缓存文件分为三个部分,依次为魔数,基础头信息(Basic Header)和加密数据。其中魔数占据文件的头4个字节,固定为TDEF(Telegram Desktop Encrypted File的缩写),Basic Header是一个结构体,定义在storage/cache/storage_encrpted_file.cpp中,占据64 + 16 + 32个字节。

B.数据解密

telegram 使用AES-CTR128模式对缓存文件进行加密,localKey是加密所使用的的密钥。这里并没有使用额外的checksum来校验校验后序加密数据的完整性,Basic Header中的checksum仅仅用来校验Basic Header结构体内成员数据的完整性。

解密缓存文件的python代码如下:

解密后可以很清楚的看到,图片,表情,gif等文件主要都缓存在cache目录中,音频和视频主要缓存在media_cache中。

telegram使用了webp的技术来存储一部分的头像和表情,这么做能够使得体积大幅减少,图片质量也得到保障。

除了webp之外,telegram还有自己的tgs文件 (Telegram Animated stickers),主要使用的是Lottie的技术来存储更高质量的动画表情,然后用gzip压缩,进一步减小动画表情缓存文件的大小。这个github项目可以很方便地将tgs文件转换为gif文件。

文件解析

# key_datas

在对文件的结构有初步的了解后,我们的第一个目标来到key_datas文件,这个文件虽然仅存储了较少的用户数据,但它是所有文件中最关键的文件,因为它保存了解密其他文件的主密钥localKey

经过一番寻找,发现在Telegram/SourceFiles/storage/storage_domain.cpp:Domain::startModern()函数中对key_datas文件进行了读取,解密以及数据解析等操作,主要步骤如下:

这里需要关注三个点:

createLocalKey()首先以sha512(salt + passcode + salt)的形式生成hash值。接下来再调用PKCS5_PBKDF2_HMAC_sha512密钥导出函数,将hashsalt作为输入参数,进行重复计算后得到最终的导出密钥passcodeKey

passcodeKey会调用DecryptLocal()函数来解密keyEncrypted,得到localKey

解密得到的localkey不只是用来解密infoEncrypted,同时它还是其他加密文件的解密密钥(可以理解为它是主密钥)。程序在这里使用了std::move()操作,将localKey的资源移动给_localKey_localKey则是后序解密文件所使用的对称密钥。

下图为解密后的key_datas文件数据:

在拥有了加解密文件的密钥_localKey后,剩下的加密文件就可以轻而易举的解开了。

# settings

settingss文件主要存储了用户页面相关的配置,包括背景图片,颜色,语言包等配置信息。

# D877F783D5D3EF8Cs

D877F783D5D3EF8Cs文件主要存储了用户的userId,以及与telegram云端进行数据通信时所使用到的加密密钥。

# D877F783D5D3EF8C/maps

maps文件中lskSelfSerialized字段存储了用户的基本信息,包括用户id,头像,姓名,注册电话,上次在线时间等信息。而其他字段主要存储的是一些配置或者资源文件的文件名,并且与文章开头列举的D877F783D5D3EF8C文件夹下的文件一一对应。

# D877F783D5D3EF8C/configs

configs文件主要存储了用户聊天,与telegram云端进行通信时的一些基础配置,包括telegram云端的ip和端口,撤回消息的时长限制等配置信息。

# 缓存文件

user_data目录下的缓存文件(图片,语音,视频)大部分都可以通过解密和解析数据得到原始的文件,在这里就不赘述了。

认证过程

telegram官网很详细地介绍了Cloud ChatSecret Chat的两种通信方式所使用的加密协议(mproto),密钥交换,身份认证等过程,有兴趣的同学可以配合源码来阅读文档,在这里也不赘述了。

这里讨论两个比较有意思的点:

  • telegram所使用的mproto通信协议是telegram开发者自己实现的一套完整的安全通信协议,不依靠于其他公认的安全通信协议。
  • telegram选择的是AES-IGE模式对文件和消息进行加密,这是一个十分古老的aes加密模式,而且相比于现在常用的加密模式,他并没有什么优势,因此基本上没有人使用AES-IGE的加密模式。这个加密模式在认证上还缺失一定的安全性,比如无法抵御blockwise-adaptive CPA攻击。telegram还专门写了一大章节的文字来说明在mproto协议下使用这个模式是安全的(没有选择更换加密模式的原因,可能是为了前后兼容性吧)。

session劫持

Telegram 不像 Whatsapp,默认是支持多端登录的,这也是Telegram能通过迁移tdata的方法来保持session的原因。这种特性对于攻击者来说,就变成了劫持session的最完美的前置条件。

现在流传在网上的session劫持方法都是通过复制整个tdata文件夹进行session的劫持,但对于一个使用了较长时间的telegram来说,tdata的体积会变得十分庞大(MB,甚至GB的量级),这在实战的过程中会有很大的局限性。

现在我们已经有解读大部分tdata文件的能力,因此可以选取最关键(保存session)的文件,减小拖取文件的体积,从而更方便于session的劫持。

通过前面对文件的解密,以及一系列的尝试后,得出能够成功进行session劫持的关键文件有:

  • tdata/key_datas
  • tdata/D877F783D5D3EF8Cs
  • tdata/D877F783D5D3EF8C/map

原因如下:

key_datas保存了解密文件的密钥localKeyD877F783D5D3EF8Cs保存了与云端通信的主密钥,D877F783D5D3EF8C/map保存了用户的基本信息。

比较有意思的一点是,D877F783D5D3EF8Cs这个文件是ToFilePart(substr(md5("data"), 0, 16))的结果,这也从侧面证明了这个文件存储了关键数据。

注意事项

#1

有的情况下,一个telegram客户端可能登录着多个用户(不超过3个),所有用户的session文件依旧保存在tdata下。telegram客户端会根据登录的次序进行编号,分别为datadata#2data#3,在session劫持的时候按照文件名进行迁移即可。

#2

在进行session劫持的过程中,如果一直都无法恢复session,并且能够确认session文件没有过期,那么有可能是本地的Telegram.exe版本与目标机器的Telegram.exe不匹配造成的。

telegram在key_datasmapsD877F783D5D3EF8Cs等文件中都将当前的版本号存储在文件的第4至第8个字节,并且在调用ReadFlie()函数时,telegram客户端会判断文件中的版本号是否大于客户端版本。若大于,则会直接停止读取文件,终止session的恢复。

因此,比较取巧的一个办法是,使用当前最新版本的客户端,这样就不会遇到版本不匹配的问题了。

passcode模块

passcode功能类似于个人电脑的锁屏功能,用户通过设置passcode和休眠时长,来保证telegram的信息不被泄露。

在进行session劫持的过程中,有概率会遇到含有passcode的情况。在这种情况下,迁移过来的telegram会呈现下面这种状态,导致攻击者无法查看telegram的信息。

打开DebugLogs/log_xx_xx.txt就可以看到因未提供passcode,而无法解密所产生的错误日志信息:

根据日志输出的信息,可以定位到storage_domain.cpp:startModern()(读取key_data文件)函数,更细一点来说,是在storage_file_utilities.cpp:DecryptLocal()函数中校验checksum的部分。

跟一下代码前后的处理逻辑,用python写出对应的处理流程:

可以看到,本质上telegram是通过比对解密后数据的checksum和原始明文的checksum来确认passcode是否正确,若两者的checksum相等,则说明解密所得到的localKey是正确的。

因此,将上述解密localKey的处理流程逆一下,就可以得到加密localKey的流程,但因为在有passcode的情况下,密钥导出函数的迭代次数为100000次,这极大的增加了爆破所需的时长。

这里我使用的是john-the-ripper爆破工具,bleeding-jumbo分支已经集成了telegram passcode爆破模块,支持旧版本和新版本telegram的passcode爆破(在telegram小于v2.1.14以前的迭代次数为4000,并且使用的是PKCS5_PBKDF2_HMAC_SHA1的密钥导出函数),但比较可惜的一点的是,新版本的passcode爆破没有GPU版,只能使用CPU进行爆破。

在passcode解密研究的过程中,同样发现两个比较有意思的点:

  • passcode仅仅影响了key_data单个文件,其他文件的内容并不会发生变化。并且在设置passcode前和设置passcode后,key_data文件内的localKey始终保持不变。
  • passcode(local passcode)将他的字面意思贯彻到了极致,也就是说passcode仅针对于当前的session,而不会上传到云端,更不会影响到其他session的使用。而在session劫持的过程中,因为攻击者是通过复制目标的session文件来达到劫持的目的,这样意味着攻击者和目标使用的是同一个session,而此时session文件已经被passcode进行了二次加密,所以迁移过来的session也需要输入同样的passcode才能进入程序的主页面。

telegram-like程序

potato是一款基于telegram进行二次开发的闭源即时通信软件,官网地址:https://www.potato.im。

A.目录结构

从目录结构可以看出,pdata的目录结构类似于较低版本的telegram目录结构。

在较低版本的telegram中,map*文件不仅仅存储了登录用户的信息和各种配置信息,同时还存储了解密文件的主密钥localKey。在后续高版本的telegram中,才将这两部分数据分别存储在maps文件和key_datas文件。

B.session 劫持

尝试将telegram session劫持的思路运用在potato上,会发现无法成功。将日志与比对成功的session劫持的日志进行比对,发现在恢复session的过程中,调用了类似于getMacAddress函数来获取本机的MAC地址。

因为potato是闭源软件,所以只能通过比对telegram的源码和potato产生的log日志来猜测potato二开的代码逻辑。但是在仔细比对了多个telegram版本的源代码后,并经过了漫长的断网和联网测试,发现telegram客户端中并没有获取本机MAC地址的操作,因此可以有如下的推测:

  • potato在session恢复中加入了MAC地址的校验,若本机MAC地址和session文件中存储的MAC地址不一致则无法恢复session。
  • 目标的MAC地址存储在session文件内。

先来验证第一点,这里我使用的是虚拟机作为劫持端,因为虚拟机可以很轻松的修改MAC地址,并且不会出现断网的情况。迁移方案与低版本的telegram迁移方案一致,复制D877F783D5D3EF8C*D877F783D5D3EF8C/map*两个文件。从下图可以看到,迁移成功!

接下来验证第二点,这里可以借用解密telegram文件的方法来解密potato文件,但因为potato使用的是低版本的telegram进行二次开发,所以需要更改部分的解密时所使用的加密算法(比如密钥导出函数)和代码逻辑,并且potato增加了一些telegram中没有的字段和数据结构,这给解析数据造成了很大的困扰。

最后根据一番猜测和努力,能成功解密一部分的数据,其中MAC地址就存储在D877F783D5D3EF8C*文件中。

添加客服微信咨询:

出海跨境王

我还没有学会写个人说明!

相关推荐

除了WhatsApp以外,还有哪些即时聊天软件?

如果你的客户是阿塞拜疆,白俄罗斯,柬埔寨,埃及,哈萨克斯坦,吉尔吉斯斯坦,立陶宛,摩尔多瓦,委内瑞拉,那你一定会用到Telegram这个App,这款App在以上的国家是排名第一的即时聊天工具。它是一款加密的通讯App,月活跃量已超过5亿,它的主要使用市场在俄罗斯,伊朗,意大利,西班牙等。