# 不启动微信，也能收发消息：一次独立通信核心的还原笔记

当我们把微信聊天记录搬到工具里时，很自然会遇到一个问题：工具能读消息、能解密图片，为什么发送时还要依赖电脑上的微信？能不能像调用一个网络客户端那样，自己登录、自己发请求、自己接收结果？

这次研究就是从这里开始的。我们把目标缩小到一个可以验证的范围：**不启动桌面微信，独立完成扫码登录、初始化、文本发送和接收，并把状态保存下来。最后移除工具专用微信，再做一次双向收发。**

现在，这条链路已经在自己的账号和文件传输助手上实测通过。手机看到了独立程序发来的完整文字，独立程序也收到了手机回复，正文逐字节一致。下面按实际学习顺序，把原理和还原过程重新讲一遍。

这是一份针对特定版本、特定协议路径的研究记录。代码在私有仓库单独迭代，不随 WeChat Use 发布。文中不包含真实账号凭据、登录二维码或聊天原文。

## 一、目标：我们究竟要从应用里拿走什么？

先想象在微信里发一句“你好”。窗口负责让你选联系人、输入文字和点击发送；但窗口背后，还要有人处理账号身份、序列化、加密、网络连接、服务器响应以及本地状态。

我们要独立实现的，就是后面这一部分。

这里的“核心”指通信协议与状态机，不是操作系统内核。它也不等于把微信的全部功能重写一遍。一个最小的独立文本客户端，可以先只接受明确的接收方标识，不必先把完整通讯录、群成员、朋友圈和媒体系统都做出来。

这让验收目标变得具体：

1. 新会话必须由手机扫码确认建立，不能借用桌面微信当前进程里的登录态。
2. 发出的业务请求必须由我们自己的程序编码、加密和传输。
3. 接收必须推进自己的同步状态，写入自己的消息库。
4. 程序退出后，新进程能读回状态，而不是靠一个一直没退出的进程维持假象。
5. 桌面微信不运行，工具专用副本移除后，仍能双向收发。

上一篇[微信图片如何加密，我们怎样一步步还原](https://blog.leeguoo.com/posts/wechat-419-image-encryption-and-decryption/)研究的是图片对象与缓存的恢复。这次向前走了一层：消息尚未变成本地聊天记录时，它如何从网络进入客户端？

## 二、先分清三件事：传输、业务、存储

说“微信用了 AES”远远不够。因为同一段内容，在不同位置可能处于不同的封装里，使用的密钥和状态也不同。

我们这次把它拆成三层：

| 层次 | 解决的问题 | 本次研究中的位置 |
| --- | --- | --- |
| 传输保护 | 这次连接如何建立可信的加密通道，记录有没有被修改 | MMTLS 握手、记录认证、短连接恢复 |
| 业务封装 | 账号如何证明身份，一次发送或同步请求如何编码 | 登录协议、业务加密、请求头、消息体 |
| 本地持久化 | 收到的消息和下一次同步位置如何可靠保存 | 自己的收件库、事务、游标、操作记录 |

图片 DAT 的解密不等于解开通信会话；传输握手成功也不等于账号登录成功；账号登录成功之后，还要初始化同步状态，才能正确收取增量消息。

可以先把完整链路记成这样：

```mermaid
flowchart TD
    A[自己的客户端] --> B[建立受认证的传输通道]
    B --> C[获取登录二维码]
    C --> D[手机扫码并确认]
    D --> E[完成账号认证]
    E --> F[首次初始化与同步]
    F --> G[保存自己的账号状态和消息库]
    G --> H[发送文本请求]
    G --> I[增量同步消息]
    H --> J[服务器响应与手机核对]
    I --> K[消息与新游标同一事务提交]
```

这张图也解释了为什么“用 curl 发一下”没有看起来那么简单。HTTP 请求只是外壳之一。真正要独立的是：**请求之前准备什么状态，包里的字节怎样生成，返回之后又允许改变什么。**

## 三、过程：先建立通道，再谈账号

### 1. 加密连接的关键是证明对方是谁

我们的传输实现从新的椭圆曲线临时密钥开始，验证服务器签名，并检查握手完成阶段的校验值。随后才建立发送和接收各自的密钥与序号。

这里有两个概念值得分开。

密钥交换让双方能够导出共同的秘密；身份认证则让客户端确认，交换秘密的确实是预期的服务器。如果只做前者，能解开数据并不自动说明连接可信。

记录序号同样重要。它参与一条连接的加密状态管理，发送方向和接收方向不能混用，失败的上下文也不能拿来继续凑下一包。我们的实现对序号溢出直接拒绝，对失败上下文停止复用。

这条路径使用 P-256 等密码学组件，但 MMTLS 有自己的报文与状态约定。不能把它直接叫作“标准 TLS 1.3”，然后假设通用 TLS 库已经替我们实现了全部协议。

短连接还涉及经过认证的会话票据和恢复密钥派生。它的意义是减少每次重新建立连接的成本；它不意味着把上一次临时私钥和整套加密上下文存盘，再原封不动地重复使用。

### 2. 二维码是一段登录流程的入口

有了通道，客户端可以请求二维码。但一张有效 PNG，只证明拿到了图像，不能证明账号已经登录。

我们还原出的登录流程可以概括为：

```text
Get QR → 显示二维码 → 手机扫码并确认
       → Check 同一二维码的状态
       → 最终账号认证 → 保存认证后的账号材料
```

在本次版本路径中，获取与检查分别对应 Get 502、Check 503，最终认证对应 Manual 252。这些编号是我们分析这一协议路径时使用的操作标识，不是对外稳定 API。

其中最容易忽视的是“同一二维码”。检查响应必须与原始请求身份、二维码来源相绑定。不能拿旧二维码的检查结果去完成新一轮认证，也不能因为用户说“扫了”，就跳过服务器状态确认。

手机的扫码确认是授权动作。程序能做的是完成协议流程，不能替代用户确认账号登录。

### 3. 登录内部还有一层密钥协商

最终认证内部并不只是“把扫码结果传回去”。我们在这条版本路径里还原了嵌套的 secp224r1 协商：未压缩公钥为 57 字节，得到的共享 X 坐标为 28 字节，随后按观察到的算法派生用于解包的密钥。

这里和前面的 P-256 不是同一个角色。P-256 出现在传输握手路径，secp224r1 出现在这条登录业务路径。把两处都简写成“ECDH 密钥”，很容易在实现时接错输入。

这条登录分支观察到用共享 X 的 MD5 结果参与 CBC 包装，且 key 与 IV 采用相同字节。我们为了兼容现有协议还原了它；这不是给新系统推荐的密码设计。

同样，不能把这里的 MD5 当成服务器身份认证。哈希、业务检查值、握手签名分别承担不同职责，缺一项时，不能用另一项“看起来也校验了”来替代。

## 四、登录之后，为什么不能马上开始普通收消息？

想象你拿到一张新的门禁卡。它说明你可以进入，但没有告诉你上次读到哪一条消息。

独立客户端第一次登录后，也需要拿到初始同步状态。本次路径先执行 NewInit 139，再用响应里经过绑定的初始化字段建立自己的同步起点，随后执行第一次 NewSync。

这里我们专门确认了一个细节：用的是 Init 响应的 f2 字段。即使某一次样本里 f2、f3 的内容碰巧相同，也不能因此把字段身份抹掉。一次偶然相等，不构成可互换的协议契约。

完成第一次同步之后，才提交自己的收件库并生成运行配置。这样，“初始化完成”指的是一组已经落地、可被新进程重新读取的状态，而不是某个函数返回了 0。

### 同步游标不是“最后一条消息 ID”

聊天软件的增量状态通常比时间戳复杂。我们解析到的 SyncKey 是一组带标识的状态值，需要按协议规则合并，不能随便拿最后一条消息 ID 代替。

一次响应也不只包含一条文本。它包含命令列表，不同命令表达不同的变化。我们为消息命令保留了嵌套字符串、原始字段和完整整数，不认识的命令也保留原始数据，避免默默丢弃后再假装同步完整。

消息 ID 还可能超过 JavaScript Number 的安全整数范围。存储和接口必须保留完整 64 位值；需要穿过 JSON 的地方，应使用准确的十进制字符串，而不是先变成浮点数再转回来。

### 为什么消息与游标必须一起提交？

假设收到一批消息 M，以及新的游标 K。

如果先保存 K，随后进程崩溃，M 没保存下来，下次服务器可能从 K 之后继续，造成漏消息。如果先保存 M，却没保存 K，下次又可能收到同一批消息。

我们的做法是保留原始响应与来源摘要，然后在自己的数据库事务里同时提交消息版本、批次信息与新游标：

```text
验证整批响应
    ↓
保存可追溯的原始响应
    ↓
BEGIN
    写消息版本
    写批次去重信息
    更新同步游标
COMMIT
```

任一验证失败，都不推进游标。这个设计并不要求读取或修改桌面微信的在线数据库，它管理的是独立客户端自己的数据。

## 五、一条文本到底怎样变成网络请求？

走到这里，我们已经有账号状态与同步起点。现在才轮到发送“你好”。

先把接收方、正文和相关业务字段编码成 protobuf 字节。protobuf 只负责结构化编码，不负责加密。随后，业务层依据所选模式处理压缩和加密，再加上业务头与检查值，最后放进传输层记录中发送。

```mermaid
flowchart LR
    A[接收方与文字] --> B[protobuf 业务体]
    B --> C[按模式压缩与加密]
    C --> D[业务头与检查值]
    D --> E[传输加密记录]
    E --> F[服务器]
```

### 压缩和加密的顺序不能反

本次静态还原包含 ordinary mode 5 与 mode 13 两条业务体编码分支。

mode 5 会判断 zlib 压缩是否有收益，再使用 AES-128-CBC。包头里的原始长度与被选中的加密前长度有各自含义，不能统一写成密文长度。

mode 13 则采用强制压缩与 AES-GCM，观察到的格式包含密文、12 字节 nonce 和 16 字节 tag。AEAD 中的 tag 用来验证密文与相应输入的完整性，nonce 的生成约束也属于协议正确性的一部分。[RFC 5116](https://www.rfc-editor.org/rfc/rfc5116)对这类认证加密接口和 nonce 要求有通用说明；它不是微信协议说明书。

这次真实账号收发使用的是已确认的普通 CBC5 路径。实现了另一条编码分支的离线测试，不代表每条实际收发都经过 GCM，也不代表所有未识别模式都能自动兼容。

### 最难直接搬走的，是协议自己的计算逻辑

标准 AES、椭圆曲线和 zlib 都有成熟实现。协议特有的检查值却不能靠“选一个常用算法”补出来。

我们遇到了一段固定模块里的计算逻辑。继续调用桌面微信中的函数，会让“独立客户端”仍然依赖应用本体。于是我们把它作为数据解析，解码实际使用的指令，并写了一个受限解释器。

解释器只访问自己映射的内存，不把来路不明的地址当宿主指针，不允许任意外部调用，并设置指令步数上限。当前覆盖实际使用的 55 类指令；模块字节也有固定摘要约束。

在这条路径里，输入从加密后的业务体生成，再交给模块计算，最终取得规定长度的结果。我们验证返回码、输出长度和实际写入，任何一项不满足就拒绝继续组包。

这里学到的重点是：**还原一个协议，不只是在找密钥；还要还原产生合法请求所需的计算与状态。**一个局部检查值算出来了，也只能证明局部函数有输出，最终仍要经过完整请求和真实响应验证。

## 六、接收是反向解包，但不是把发送代码倒着跑

收到服务器响应后，先验证传输记录，再解开对应的业务封装，解析命令和消息，最后进入同步事务。

这是一条方向相反、职责相近的路径，但不能机械地复用所有发送侧假设。例如，一个编码器能把自造数据加密再解密回来，只证明它内部自洽；若双方对字段含义都写错了，往返测试仍可能通过。

所以我们分开验证：固定样本的字段和边界、被修改数据的拒绝行为、真实服务器响应，以及手机端完整正文对应关系。只有这些证据接起来，才把某条接收路径算作跑通。

### 收消息一定要不断轮询吗？

这里要区分官方网络组件与我们当前实现。

腾讯公开的 [Mars](https://github.com/Tencent/mars)提供网络通信组件，STN 涉及长短连接、心跳等机制。它能帮助理解整体网络架构，但公开的网络组件并不等于完整的微信账号、登录与业务协议。

我们的独立工具目前接通了显式增量拉取，以及心跳提示后的同步路径。完整长连接推送尚未全部接线，因此不能把当前工具说成完整复现了官方推送，也不能反过来说微信官方只靠固定间隔轮询。

工具里设置的等待间隔和超时，是我们自己的运行策略。遇到协议中的一个等待数值，如果单位还没绑定清楚，就不能凭数值大小直接解释成若干秒。

## 七、怎样证明它真的脱离了桌面微信？

只关掉窗口还不够。应用可能在后台运行；我们自己的程序也可能暗中读取原应用里的资源，或者借用旧后台服务。

所以最终验收比“命令退出码为 0”多做了几步。

首先，使用独立扫码建立的新账号材料，完成初始化和自己的消息存储。随后，让新的进程在源码目录之外启动，在禁止网络的条件下读回收件库和游标。这证明状态确实保存了下来；它不证明离线的 status 命令重新向服务器登录了一次。

然后，在当前电脑上正常退出工具专用微信，把这个副本移入废纸篓。聊天数据没有清除，应用可以恢复。主微信保留，但不运行。

为了避免“虽然没运行，却还偷偷从应用里取东西”的疑问，协议运行时使用系统沙箱限制：对主微信和废纸篓里的专用副本都拒绝读取，相关原应用数据目录与服务访问也受限。实际探测读取返回权限拒绝。

最后，在这个条件下各执行一次发送和接收：

| 检查对象 | 实际结果 | 能说明什么 |
| --- | --- | --- |
| 文本发送 | 服务器接受，手机显示完整测试正文 | 独立发送链路到达了手机 |
| 文本接收 | 新消息正文与手机回复逐字节相同，服务器消息 ID 非零 | 独立同步拿到了对应消息 |
| 同步状态 | 序列由 2 推进到 3，累计保存 130 个消息版本 | 收件事务与状态持久化实际发生 |
| 运行环境 | 收发过程采样均未发现桌面微信进程 | 这两次操作未依赖运行中的桌面微信 |
| 应用依赖 | 专用副本原路径不存在；主应用与废纸篓副本读取被沙箱拒绝 | 运行时不能靠读取这两份应用完成操作 |
| 生命周期 | 原始子进程退出记录、结果与请求绑定均核对 | 成功不是来自另一轮请求或遗留进程 |

这里的 130 是保存的消息版本数，不是宣称收到了 130 条独立的新聊天。最后一次增量接收新增了两个消息版本，其中一个新增消息正文与手机测试回复完整匹配。

更准确的结论是：**工具专用微信移除、主微信未运行且对协议进程不可读的条件下，我们完成了自己的文本双向收发。**不是说电脑上所有微信文件都被删除了。

## 八、为什么操作记录也属于通信核心？

发送请求可能遇到一个麻烦情况：服务器已经处理，但客户端没拿到响应。如果此时自动再发一次，就可能重复投递。

因此我们把发送意图先持久化，再创建具体子进程执行。请求、参数摘要、进程身份、结果和退出记录都绑定到同一轮操作。结果未知时保留记录并停止自动重试，不能因为文件暂时没出现，就把它当成从未发送。

接收也有类似问题，不过可以借助批次去重和原子提交控制本地重复。发送端未知状态则不能只靠本地去重证明远端没有执行。

这也是为什么本次跨电脑迁移时，不能只复制登录材料。我们还要复制一致的收件库和历史操作记录，并禁止旧电脑继续使用同一份运行状态。否则两个客户端可能同时推进游标，或对未知操作做出互相矛盾的判断。

保存一个会话文件很容易；保存一个能解释“我做过什么、下一步还能做什么”的客户端状态，才是持久化的完整含义。

## 九、结论：做成了什么，还没有做成什么？

本次完成的是一个独立文本通信核心：手机扫码授权、账号认证、首次初始化、自己的消息库、增量同步、文本发送，以及新进程读回状态。工具专用桌面微信移除后，文本双向收发仍有手机和本地记录相互对应的实测证据。

它还不是完整微信客户端。完整通讯录和群成员映射、任意目标覆盖、媒体收发、完整长连接推送、长时间运行稳定性，都不能由这两次文本验收推导出来。之前图片解密的成果也不会自动变成这个新核心的媒体能力，还需要单独接线和验收。

这次研究里，我最想留下的学习方法是：把一个大应用拆成有输入、有输出、有状态的几个边界，再为每个边界找对应证据。

看到二维码，就继续问账号认证是否完成；解开响应，就继续问消息和游标是否一起提交；服务器返回成功，就继续去手机核对；新进程能启动，就继续检查它是否借用了原应用。

这样逐层还原之后，“脱离应用发请求”才从一个想法，变成可以独立运行、可以复查的通信程序。

## 复习时，沿着这七个问题走一遍

1. **窗口退出后，谁在通信？**区分应用 UI、协议客户端和服务器。
2. **为什么不只找一把 AES 密钥？**传输、业务、存储的密钥与状态用途不同。
3. **二维码出现是否就登录了？**还要手机确认、同源状态检查和最终账号认证。
4. **第一次收消息从哪里开始？**先初始化，再使用准确来源的同步状态。
5. **游标为什么不能提前保存？**消息与游标必须在同一个本地事务提交。
6. **发送成功看什么？**请求绑定的有效响应，加上接收端完整正文核对；未知结果不自动重试。
7. **怎么证明独立？**不运行原应用、限制读取依赖、保留原始进程证据，并做真实双向验收。

本文的微信特定字段与验收结果来自本次私有实现和保存的原始记录；外部链接只用于补充网络架构与密码学背景，不作为这些实测结论的替代证据。
