1 背景前提

没有的就是想看看别人怎么做的,让自己在实际开发也有好的方案可以参考参考 “本文档仅为个人技术学习与研究目的而撰写,不涉及任何微信业务层用户数据。所有分析均在合法授权的个人测试环境中进行。严禁用于任何非法用途。”

本文以x64dbg,ida为原本结合Golang代码

2 研究思路

2.1 基本架构和TLS“外壳”

Fig.1 总体架构

从总体架构中可以知道,mmtls作为**“中间层”** ,沟通了业务层和网络连接层,其功能是为业务层提供了安全保障 Fig.2 mmtls协议内部

分为这四种通讯类型,每一种都有对应的代码.

为了方便这里hook了ws2_32.dll的send和recv函数

这里用Golang列出大致的(命名不一定准哈)

1
2
3
4
5
6
const(
	magicAlert     = uint8(0x15)  
	magicHandshake = uint8(0x16)  
	magicRecord    = uint8(0x17)  
	magicSystem    = uint8(0x19)
)

既然是结合Tls TLS 1.3 随便用wireshark

都是Opaque Type(1 byte) + Version (2 bytes) + Length (2 bytes) + Encrypted Data(Length bytes),那当然MMtls也不例外的,回头看一下这2张图

Opaque Type Version Length Encrypted Data
16 F1 04 01 6E ……….
17 F1 04 00 20 ……….
表格是16进制
嗯非常好Omg

[!quote] 基于TLS1.3的微信安全通信协议mmtls介绍 Alert协议用于通知对端发生错误,希望对端关闭连接,目前mmtls为了避免server存在过多TCP Time-Wait状态,Alert消息只会server发送给client,由client主动关闭连接。

这个是Opaque Type=0x15的情况,发生错误了

3 HandShake流程

因为之前不了解椭圆曲线,去Bilibili了解了一下公钥加密技术ECC椭圆曲线加密算法原理_哔哩哔哩_bilibili

根据官方开源的Tencent/mars: Mars is a cross-platform network component developed by WeChat.,然后通过longlink一路跟过来,到了req2buf,再就找到了发现很多核心算法都有mmcrypto这个字符串hh

4. 密码学

4.1 ECDHE

4.1.1 原理

公钥加密技术ECC椭圆曲线加密算法原理_哔哩哔哩_bilibili,这里讲得很好就是那个乘法分配律好家伙

4.1.2 流程

客户端在启动的时候会调用GenEcdhKeyPair生成公钥和私钥,接着发送Client Hello发送自己的公钥,之后服务端返回Server Hello返回服务端的公钥 这里Go代码参考anonymous5l/mmtls: wechat mmtls protocol handshake implement 进行密钥交换后利用自己的私钥和服务端公钥计算出共享密钥

1
2
3
4
5
m.connectKey = m.computeEphemeralSecret(  
    m.serverPublicKey.X,  
    m.serverPublicKey.Y,  
    m.privateKey.D,  
)

4.2 Hkdf拓展

  • expanded secret
  • application data key expansion
  • client finished
  • handshake key expansion
  • PSK_ACCESS
  • PSK_REFRESH

3.3 AES-Gcm加密

3.3.1 mmcrypto::OpenSslCryptoUtil::AesGcmEncrypt

这个算法肯定是借来用的我们翻一下看看大概是怎么个流程,根据这篇文章对称加密算法AES之GCM模式简介及在OpenSSL中使用举例-CSDN博客

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
namespace {
 
static const unsigned char gcm_key[] = { // 32 bytes, Key
	0xee, 0xbc, 0x1f, 0x57, 0x48, 0x7f, 0x51, 0x92, 0x1c, 0x04, 0x65, 0x66,
	0x5f, 0x8a, 0xe6, 0xd1, 0x65, 0x8b, 0xb2, 0x6d, 0xe6, 0xf8, 0xa0, 0x69,
	0xa3, 0x52, 0x02, 0x93, 0xa5, 0x72, 0x07, 0x8f
};
 
static const unsigned char gcm_iv[] = { // 12 bytes, IV(Initialisation Vector)
	0x99, 0xaa, 0x3e, 0x68, 0xed, 0x81, 0x73, 0xa0, 0xee, 0xd0, 0x66, 0x84
};
 
// Additional Authenticated Data(AAD): it is not encrypted, and is typically passed to the recipient in plaintext along with the ciphertext
static const unsigned char gcm_aad[] = { // 16 bytes
	0x4d, 0x23, 0xc3, 0xce, 0xc3, 0x34, 0xb4, 0x9b, 0xdb, 0x37, 0x0c, 0x43,
	0x7f, 0xec, 0x78, 0xde
};
 
std::unique_ptr<unsigned char[]> aes_gcm_encrypt(const char* plaintext, int& length, unsigned char* tag)
{
	EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
	// Set cipher type and mode
	EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
	// Set IV length if default 96 bits is not appropriate
		EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, sizeof(gcm_iv), nullptr);
	// Initialise key and IV
	EVP_EncryptInit_ex(ctx, nullptr, nullptr, gcm_key, gcm_iv);
	// Zero or more calls to specify any AAD
	int outlen;
	EVP_EncryptUpdate(ctx, nullptr, &outlen, gcm_aad, sizeof(gcm_aad));
	unsigned char outbuf[1024];
	// Encrypt plaintext
	EVP_EncryptUpdate(ctx, outbuf, &outlen, (const unsigned char*)plaintext, strlen(plaintext));
	length = outlen;
	std::unique_ptr<unsigned char[]> ciphertext(new unsigned char[length]);
	memcpy(ciphertext.get(), outbuf, length);
	// Finalise: note get no output for GCM
	EVP_EncryptFinal_ex(ctx, outbuf, &outlen);
	// Get tag
	EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, outbuf);
	memcpy(tag, outbuf, 16);
	// Clean up
	EVP_CIPHER_CTX_free(ctx);
	return ciphertext;
}
 
std::unique_ptr<unsigned char[]> aes_gcm_decrypt(
const unsigned char* ciphertext, 
int& length, 
const unsigned char* tag)
{
	EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
	// Select cipher
	EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
	// Set IV length, omit for 96 bits
	EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, sizeof(gcm_iv), nullptr);
	// Specify key and IV
	EVP_DecryptInit_ex(ctx, nullptr, nullptr, gcm_key, gcm_iv);
	int outlen;
	// Zero or more calls to specify any AAD
	EVP_DecryptUpdate(ctx, nullptr, &outlen, gcm_aad, sizeof(gcm_aad));
	unsigned char outbuf[1024];
	// Decrypt plaintext
	EVP_DecryptUpdate(ctx, outbuf, &outlen, ciphertext, length);
	// Output decrypted block
	length = outlen;
	std::unique_ptr<unsigned char[]> plaintext(new unsigned char[length]);
	memcpy(plaintext.get(), outbuf, length);
	// Set expected tag value
	EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, (void*)tag);
	// Finalise: note get no output for GCM
	int rv = EVP_DecryptFinal_ex(ctx, outbuf, &outlen);
	// Print out return value. If this is not successful authentication failed and plaintext is not trustworthy.
	fprintf(stdout, "Tag Verify %s\n", rv > 0 ? "Successful!" : "Failed!");
	EVP_CIPHER_CTX_free(ctx);
	return plaintext;
}

EVP_EncryptInit_ex

这个跑x64可以知道是24

翻一下正版openssl看看是怎么个情况

  1. 21.5 对称加解密函数_OpenSSL 中文手册
  2. Openssl 对称加解密函数 - EVP_Cipher、EVP_Encrypt、EVP_Decryp 系列-CSDN博客
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
加密inl缓冲区中的inl字节in并将加密版本写入out。可以多次调用此函数来加密连续的数据块。
写入的数据量取决于加密数据的块对齐。对于大多数密码和模式,写入的数据量可以是从零字节到 (inl + cipher_block_size - 1) 字节的任何内容。对于包装密码模式,写入的数据量可以是从零字节到 (inl + cipher_block_size) 字节的任何内容。
对于流密码,写入的数据量可以是从零字节到 inl 字节的任何内容。因此,out应该为正在执行的操作包含足够的空间。实际写入的字节数放在outl中
**/
int EVP_EncryptUpdate(
	EVP_CIPHER_CTX *ctx, 
	unsigned char *out,
	int *outl, 
	const unsigned char *in, 
	int inl
);

全都有了不分析了。

3.3.2 mmcrypto::OpenSslCryptoUtil::AesGcmDecrypt

模拟

主要看一下nonce的生成方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// aesGcmDecrypt mmtls的Aes-GCM模式解密  
func aesGcmDecrypt(recordType uint8, keys *trafficKeyPair, serverSeqNum uint32, data []byte, length uint16) []byte {  
    c, err := aes.NewCipher(keys.serverKey)  
    if err != nil {  
       return nil  
    }  
    aead, err := cipher.NewGCM(c)  
    if err != nil {  
       return nil  
    }  
    nonce := make([]byte, 12)  
    copy(nonce, keys.serverNonce)  
    xorNonce(nonce, serverSeqNum)  
    aad := make([]byte, 13)  
    binary.BigEndian.PutUint64(aad, uint64(serverSeqNum))  
    aad[8] = recordType  
    binary.BigEndian.PutUint16(aad[9:], longlink.MmtlsVer)  
    binary.BigEndian.PutUint16(aad[11:], length)  
    dst, err := aead.Open(nil, nonce, data, aad)  
    if err != nil {  
       fmt.Printf("AESGCM解密失败: %v, AAD: %X, Nonce: %X\n", err, aad, nonce)  
       return nil  
    }  
    return dst  
}

4 引用自己

  • [[WeChat:mmcryto的灵感]]
  • [[WeChat:SendWhenNoData]]
  • [[WeChat:req2buf and buf2Resp]]
  • [[WeChat:handshake]]

5 Hello Client

16 F1 04 01 6E //总长 00 00 01 6A 01 //Client Hello 04 F1 02 C0 2B //Cipher Suite:见[[#sub_40FA1D0]] 00 A8 //Cipher Suite:见[[#sub_40FA1D0]] 8A 65 5C F8 4E E3 E3 05 0A FD 48 C0 8F 77 C1 3D A0 48 E1 78 16 70 0A 2C CF 38 6D 37 B5 08 19 06 //random32 69 6D D5 7B //Timestramp:1768805755 00 00 01 3A 02 //Count:2 2个数据块 00 00 00 8B 00 0F 01 //PSK Identity 00 00 00 84 02 00 27 8D 00 00 00 00 00 00 48 00 0C 07 45 8F 94 24 F8 AD 1F E6 F2 20 B9 00 69 85 A7 28 F3 E6 43 8A 62 C9 3F 82 52 8E 04 6A AD 65 CA 16 D9 E3 E6 B0 3C 13 F6 E9 B0 92 7B A6 97 00 4D 11 54 8D F0 87 C6 FF 79 D1 70 73 0B 65 67 12 A3 A5 1C FC A5 7F 94 7F 70 AC 2D BC 58 20 25 E8 95 11 02 65 E1 18 15 ED 86 FD E9 14 E9 87 22 55 C2 6B AE 26 3F 8B 44 A0 D8 AE E0 FA 37 EC 85 72 4D A9 9F 55 38 D9 A3 EE 00 00 00 A6 00 10 02 00 00 00 47 00 00 00 05 //Type 00 41 04 5A AB 48 CE FB 8A 2D DE CB 3C 08 0E 58 83 25 99 12 5E 11 4D 67 80 4A 92 C5 89 A6 2B F4 7A 4C 21 EE 03 75 DA E3 43 20 87 1E D4 FA A2 FD C3 59 29 53 6D FE 96 EF 5F 12 F9 63 2D 1C 97 A7 3F B0 2C 00 00 00 47 00 00 00 06 00 41 04 72 31 9E 81 01 28 2A 25 41 98 3C FA 26 C8 4F F6 18 AA 2B FD 4D CE D6 CA 9F C0 39 80 62 76 A1 EC E2 5D 8B F3 CE CE 60 EC CA 4D F3 C9 F0 CA 9E BE 1B 7F 4F 8A E8 EC 91 55 09 FD 35 FD B4 31 CA 29 00 00 00 00 02 //支持 1-RTT PSK 00 00 00 03 //支持1-RTT ECDH 00 00 00 04 //支持0-RTT PSK

微信协议入门——原理篇 - 万物归空 - 博客园

有时候长连接失败会用短链接

sub_40FA1D0

这是最关键的加密套件 (Cipher Suite) 标识符。

  • 定义:0xC02B 在 IANA 标准中对应的是TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  • 拆解其安全含义:
    • ECDHE:使用椭圆曲线临时密钥交换(具备前向安全性,即即便服务器私钥泄露,历史消息也无法被破解)。
    • ECDSA:使用椭圆曲线数字签名算法进行身份验证。
    • AES_128_GCM:使用 128 位密钥的 AES 算法,并采用 GCM 模式(同时提供加密和完整性校验)。
    • SHA256:用于消息摘要和 PRF(伪随机函数)的散列算法。

参考文献

  1. [原创]mmtls的分析研究与总结-软件逆向-看雪安全社区|专业技术交流与安全研究论坛
  2. 基于TLS1.3的微信安全通信协议mmtls介绍
  3. 公钥加密技术ECC椭圆曲线加密算法原理_哔哩哔哩_bilibili
  4. Tencent/mars: Mars is a cross-platform network component developed by WeChat.
  5. anonymous5l/mmtls: wechat mmtls protocol handshake implement
  6. 21.5 对称加解密函数_OpenSSL 中文手册
  7. Openssl 对称加解密函数 - EVP_Cipher、EVP_Encrypt、EVP_Decryp 系列-CSDN博客
  8. 对称加密算法AES之GCM模式简介及在OpenSSL中使用举例-CSDN博客
  9. TLS/1.2和TLS/1.3的核心区别 | HTTPS有哪些不安全因素_哔哩哔哩_bilibili
使用 Hugo 构建
主题 StackTrial 设计