Telegram session劫持探索

渗透技巧 2年前 (2022) admin
5,115 0 0

 

Telegram session

劫持探索

 

零鉴科技

Telegram session劫持探索
×

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

文件结构

目录结构

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

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

.├── 3241E7577682E411s├── D877F783D5D3EF8C│   ├── 5DFE1B025533CC34s│   ├── 80EB1923C860B053s│   ├── 83481FA7DFF31E68s│   ├── 9710392255F6037Es│   ├── 9DD9F15BEB962075s│   ├── configs│   └── maps├── D877F783D5D3EF8Cs├── ED5F06835F0E7386s├── countries├── dumps├── emoji│   ├── ...├── key_datas├── prefix├── settingss├── shortcuts-custom.json├── shortcuts-default.json├── user_data│   ├── cache│   │   └── 0│   │       ├── 00│   │       │   ├── 1868D6DC9582│   │       │   └── EFB0CD60440D│   │       ├── ...│   │       └── binlog│   └── media_cache│       ├── 0│       │   ├── 00│       │   │   └── 1A7B91AEEBD8│       │   ├── 01│       │   ├── ...│       │   └── binlog│       └── version└── usertag

可以看到,大部分文件可以通过文件名来间接的了解文件内部数据所代表的的含义,但仍有一部分文件名由数字+字母(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 session劫持探索

 

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

Telegram/SourceFiles/storage/details/storage_file_utilities.cpp:ReadFile() {   // ...  // check signature  HashMd5 md5;  md5.feed(bytes.constData(), dataSize);  md5.feed(&dataSize, sizeof(dataSize));  md5.feed(&version, sizeof(version));  md5.feed(magic, TdfMagicLen);  if (memcmp(md5.result(), bytes.constData() + dataSize, 16)) {    DEBUG_LOG(("App Info: bad file '%1', signature did not match"              ).arg(name));    continue;  }  // ...}

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

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

 

FileKey GenerateKey(const QString &basePath) {  FileKey result;  QString path;  path.reserve(basePath.size() + 0x11);  path += basePath;  do {    result = base::RandomValue<FileKey>(); // using FileKey = quint64;    path.resize(basePath.size());    path += ToFilePart(result);  } while (!result || KeyAlreadyUsed(path));  return result;}

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

 

QString ToFilePart(FileKey val) {  QString result;  result.reserve(0x10);  for (int32 i = 0; i < 0x10; ++i) {    uchar v = (val & 0x0F);    result.push_back((v < 0x0A) ? ('0' + v) : ('A' + (v - 0x0A)));    val >>= 4;  }  return result;}

B.数据解密

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

 

调用prepareAES_oldmtp()函数,使用localKey和messageKey生成aesKey ->  调用aesIgeDecryptRaw()函数,使用aesKey来解密数据 ->    使用SHA1算法校验数据的完整性

python的伪代码如下:

 

decrypted = decryptLocal(mapEncrypted, localKey)
def decryptLocal(encrypted, key):    encryptedKey = encrypted[:16]    decrypted = aesDecryptLocal(encrypted[16:], key, encryptedKey)
    digest = sha1(decrypted)[:16]    if digest != encryptedKey:        raise ValueError('App Info: bad decrypt key, data not decrypted')  def aesDecryptLocal(src, authkey, key128):    aesKey, aesIV = prepareAES_oldmtp(authkey, key128)
    dst = bytearray(len(src))    buffer = ffi.from_buffer(dst)    ffi_openssl.aesIgeDecryptRaw(src, buffer, len(src), aesKey, aesIV)

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

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

 

调用ReadFile()函数获取加密数据 ->   调用DecryptLocal()函数,使用localkey解密加密的数据 ->    解析明文数据,进行序列化等操作

缓存文件

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

PlaceFromId()的代码如下:

 

// https://github.com/desktop-app/lib_storage/blob/master/storage/cache/storage_cache_database_object.cppQString PlaceFromId(PlaceId place) {  auto result = QString();  result.reserve(15);  const auto pushDigit = [&](uint8 digit) {    const auto hex = (digit < 0x0A)      ? char('0' + digit)      : char('A' + (digit - 0x0A));    result.push_back(hex);  };  const auto push = [&](uint8 value) {    pushDigit(value & 0x0F);    pushDigit(value >> 4);  };  for (auto i = 0; i != place.size(); ++i) {    push(place[i]);    if (!i) {      result.push_back('/');    }  }  return result;}
// 示例:// std::array<uint8_t, 7> place = {1, 2, 3, 4, 5, 6, 7};// PlaceFromId(place) = "11/223344556677"

A.文件结构

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

Telegram session劫持探索

 

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

 

constexpr auto kSaltSize = size_type(64);constexpr auto kSha256Size = size_type(32);
struct BasicHeader {  bytes::array<kSaltSize> salt = { { bytes::type() } };  uint32 format : 8; // 位域  uint32 reserved1 : 24;  uint32 reserved2 = 0;  uint64 applicationVersion = 0;  bytes::array<openssl::kSha256Size> checksum = { { bytes::type() } };};

B.数据解密

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

 

CTR摸式是一种通过将逐次累加的计数器进行加密来生成密钥流的流密码。CTR模式中,每个分组对应一个逐次累加的计数器,并通过对计数器进行加密来生成密钥流。也就是说,最终的密文分组是通过将计数器加密得到的比特序列,与明文分组进行XOR而得到的。

CTR模式中可以以任意顺序对分组进行加密和解密,因此在加密和解密时需要用到的“计数器”的值可以由nonce和分组序号直接计算出来,因此CTR模式能够以任意顺序处理分组,就意味着能够实现并行计算。

 

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

 

with open(path, 'rb') as f:  if f.read(4) != b'TDEF':    raise ValueError('wrong file type')
    salt = f.read(64)    encrypted = f.read(16 + 32)
    real_key = sha256(key[:len(key) // 2] + salt[:32])    iv = sha256(key[len(key) // 2:] + salt[32:])[:16]    d = CtrState(real_key, iv)
    data = d.encrypt(encrypted)    checksum = data[16:]
    if sha256(key + salt + data[:16]) != checksum:      raise ValueError('wrong key')
    return d.encrypt(f.read())

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

Telegram session劫持探索

 

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文件进行了读取,解密以及数据解析等操作,主要步骤如下:

 

调用ReadFile()函数获取加密数据(keyData) ->   解析keyData,读取salt, keyEncrypted, infoEncrypted ->    调用createLocalKey()函数生成passcodeKey ->       调用DecryptLocal()函数和密钥导出函数,使用导出密钥解密keyEncrypted,得到localKey ->        调用DecryptLocal()函数,使用localkey解密infoEncrypted,得到info->          解析用户数据,初始化账户设置

这里需要关注三个点:

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

 

MTP::AuthKeyPtr CreateLocalKey(const QByteArray &passcode, const QByteArray &salt) {  const auto s = bytes::make_span(salt);  const auto hash = openssl::Sha512(s, bytes::make_span(passcode), s);   const auto iterationsCount = passcode.isEmpty()    ? 1 // Don't slow down for no password.    : kStrongIterationsCount;
  auto key = MTP::AuthKey::Data{ { gsl::byte{} } };  PKCS5_PBKDF2_HMAC(     reinterpret_cast<const char*>(hash.data()),    hash.size(),    reinterpret_cast<const unsigned char*>(s.data()),    s.size(),    iterationsCount,    EVP_sha512(),    key.size(),    reinterpret_cast<unsigned char*>(key.data()));  return std::make_shared<MTP::AuthKey>(key);}

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

 

_passcodeKey = CreateLocalKey(passcode, salt);
EncryptedDescriptor keyInnerData, info;if (!DecryptLocal(keyInnerData, keyEncrypted, _passcodeKey)) {  LOG(("App Info: could not decrypt pass-protected key from info file, "       "maybe bad password..."));  return StartModernResult::IncorrectPasscode;}auto key = Serialize::read<MTP::AuthKey::Data>(keyInnerData.stream);_localKey = std::make_shared<MTP::AuthKey>(key);

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

 

Domain::StartModernResult Domain::startModern(const QByteArray &passcode) {  auto key = Serialize::read<MTP::AuthKey::Data>(keyInnerData.stream);  // ...  _localKey = std::make_shared<MTP::AuthKey>(key);  // ...  auto config = account->prepareToStart(_localKey);}
std::unique_ptr<MTP::Config> Account::prepareToStart(std::shared_ptr<MTP::AuthKey> localKey) {  return _local->start(std::move(localKey));}
std::unique_ptr<MTP::Config> Account::start(MTP::AuthKeyPtr localKey) {  Expects(localKey != nullptr);
  _localKey = std::move(localKey);  readMapWith(_localKey);  clearLegacyFiles();  return readMtpConfig();

下图为解密后的key_datas文件数据:Telegram session劫持探索

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

# settings

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

Telegram session劫持探索

# D877F783D5D3EF8Cs

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

Telegram session劫持探索

 

# D877F783D5D3EF8C/maps

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

Telegram session劫持探索

# D877F783D5D3EF8C/configs

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

Telegram session劫持探索

# 缓存文件

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

Telegram session劫持探索

认证过程

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

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

  • telegram所使用的mproto通信协议是telegram开发者自己实现的一套完整的安全通信协议,不依靠于其他公认的安全通信协议。

  • telegram选择的是AES-IGE模式对文件和消息进行加密,这是一个十分古老的aes加密模式,而且相比于现在常用的加密模式,他并没有什么优势,因此基本上没有人使用AES-IGE的加密模式。

    这个加密模式在认证上还缺失一定的安全性,比如无法抵御blockwise-adaptive CPA攻击。telegram还专门写了一大章节的文字来说明在mproto协议下使用这个模式是安全的(没有选择更换加密模式的原因,可能是为了前后兼容性吧)。

    在搜索资料的过程中,发现许多网友都质疑telegram使用这种加密模式,并在论坛上给官方提了很多“意见”。

    传送门 => https://news.ycombinator.com/item?id=6915741https://github.com/telegramdesktop/tdesktop/issues

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))的结果,这也从侧面证明了这个文件存储了关键数据。

 

In[163]: import hashlibIn[164]: md5 = hashlib.md5('data'.encode("utf-8")).digest() In[165]: md5Out[165]: b'x8dwx7f8]=xfexc8x81] xf7I`&xdc'In[166]: int.from_bytes(md5, 'little')Out[166]: 292629419324765554216674928803425777549In[167]: ToFilePart(292629419324765554216674928803425777549)Out[167]: 'D877F783D5D3EF8C'  Out[168]: 'data    : D877F783D5D3EF8C -> 8d777f385d3dfec8 (substr (md5_hex ("data"),    0, 16))'Out[169]: 'data#2  : A7FDF864FBC10B77 -> 7adf8f46bf1cb077 (substr (md5_hex ("data#2"),  0, 16))'Out[170]: 'data#3  : F8806DD0C461824F -> 8f08d60d4c1628f4 (substr (md5_hex ("data#3"),  0, 16))'Out[171]: 'data#4  : C2B05980D9127787 -> 2c0b95089d217778 (substr (md5_hex ("data#4"),  0, 16))'Out[172]: 'data#5  : 0CA814316818D8F6 -> c08a411386818d6f (substr (md5_hex ("data#5"),  0, 16))'

注意事项

#1

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

Telegram session劫持探索

 

 

#2

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

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

 

// read app versionqint32 version;if (f.read((char*)&version, sizeof(version)) != sizeof(version)) {  DEBUG_LOG(("App Info: failed to read version from '%1'").arg(name));  continue;}if (version > AppVersion) {  DEBUG_LOG(("App Info: version too big %1 for '%2', my version %3").arg(version).arg(name).arg(AppVersion));  continue;}

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

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

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

Telegram session劫持探索

 

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

Telegram session劫持探索

 

 

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

Telegram session劫持探索

 

 

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

 

def decryptLocal(encrypted, key) -> bytes:    encryptedKey = encrypted[:16]    decrypted = aesDecryptLocal(encrypted[16:], key, encryptedKey)
    digest = sha1(decrypted)[:16]    if digest != encryptedKey:        raise ValueError('App Info: bad decrypt key, data not decrypted - maybe has passcode)
    dataLen = int.from_bytes(decrypted[:4], 'little')    return decrypted[4:dataLen]
def createLocalKey(passcode, salt) -> bytes:    iterCount = 100_000 if passcode else 1
    hash = hashlib.sha512(salt + passcode + salt).digest()    key = hashlib.pbkdf2_hmac("sha512", hash, salt, iterCount, 256)    return key
qstream = readFile('key_data')
salt = qstream.readBytes()keyEncrypted = qstream.readBytes()infoEncrypted = qstream.readBytes()
_passcodeKey = createLocalKey(passcode, salt)_localKey = decryptLocal(keyEncrypted, _passcodeKey)

可以看到,本质上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进行爆破。Telegram session劫持探索

 

在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 session劫持探索

 

 

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

B.session 劫持

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

Telegram session劫持探索

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

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

  • 目标的MAC地址存储在session文件内。

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

Telegram session劫持探索

 

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

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

 

Telegram session劫持探索
关于我们

长沙零鉴科技有限公司是一家以前沿安全技术为尖刀,高水平安全人才为支柱,在信息安全日益严峻的形势下专业从事网络信息安全技术的自主创新型企业,零鉴科技致力于打击网络犯罪领域的技术研究与产品研发。

截至目前,零鉴科技已为不同省市的多家执法机关提供高效精确的反网络犯罪情报分析服务和优质的安全解决方案。

也欢迎志同道合的小伙伴们与我们共同进步~

简历投递邮箱:[email protected]

END

Telegram session劫持探索
点击上方蓝字 · 关注我们
CLICK THE BLUE WORD TO FOLLOW US

 

原文始发于微信公众号(零鉴科技):Telegram session劫持探索

版权声明:admin 发表于 2022年1月20日 上午7:48。
转载请注明:Telegram session劫持探索 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...