
简介这是一份面向C#桌面开发与网络协议分析学习者的WinForm实战项目源码聚焦雷速比赛场景下的MQTT通信逆向与WebSocket数据解析适合具备一定C#基础、希望深入理解客户端协议抓取与消息订阅机制的开发者参考。压缩包共收录897个文件整体约431.38MB其中373个cs源文件构成核心业务逻辑168个pak与114个h、48个cpp文件涉及底层依赖与原生模块另有58个dll、20个pdb及若干xml、json、config配置项配合7个exe与6个nupkg可还原较完整的工程结构。项目内含雷速比赛MQTT分析与WebSocket分析两套csproj工程并保留v8快照、缓存与资源文件便于对照调试。目前已有641人学习下载读者可从中获取MQTT连接建立、主题订阅、消息编解码以及WebSocket数据流处理的实现思路同时借助逆向JS相关逻辑理解前后端交互细节适合作为协议分析类桌面工具的参考范例。1. 从一条 MQTT 消息说起这套 C# WinForm 逆向程序到底能干什么如果你抓过移动端比赛类 App 的包大概率见过这样的场景界面上的比分、赔率、赛程每隔几秒就跳一次但抓包工具里翻来覆去只有一堆二进制帧既不是明文 JSON也不是标准 HTTP 接口。这类数据十有八九走的是 MQTT 长连接服务端主动推、客户端被动收传统「抓请求—改参数—重放」的思路在这里直接失效。这套 C# WinForm 雷速比赛 MQTT 逆向程序解决的就是这个环节把 JS 侧混淆过的连接参数、主题订阅规则、消息解码逻辑还原出来再用 C# 在桌面端复刻一条能稳定收数的订阅链路。它适合做数据采集、盘口监控、赛事提醒的开发者也适合想拿一个真实 MQTT 逆向案例练手的 C# 上位机方向从业者。前提是你得接受一件事——逆向出来的东西不是一劳永逸的协议一变就得重新跟。2. 逆向 JS 里的 MQTT 连接参数从混淆代码到可复用配置2.1 先搞清楚 MQTT 在这类比赛 App 里怎么跑MQTT 本身是发布/订阅模型客户端连上 Broker 之后订阅若干 Topic服务端往这些 Topic 推消息。放到比赛类 App 里典型结构是这样App 启动后先走一次 HTTP 拿一个临时凭证或者签名然后拿这个凭证去连 MQTT Broker连接时带 ClientID、Username、Password连上后订阅形如match/{id}/odds、score/live/{id}这类主题。真正麻烦的地方在于这些连接参数往往不是写死的而是 JS 运行时算出来的——时间戳、设备指纹、随机数、AES 或 RSA 加密后的签名混在一起。所以逆向的第一步不是去读 MQTT 协议而是把 JS 里「参数是怎么算出来的」这段逻辑扒出来。常见做法是先用抓包工具定位到那条建立连接的请求看它带了哪些字段再回到 JS 源码里搜这些字段名。字段名通常被混淆成a、b、_0x1a2b这种但字符串常量、加密函数的调用特征、时间戳的位数是藏不住的。2.2 定位加密入口的实操路径我一般按这个顺序走比一上来就硬啃混淆代码高效得多// 在浏览器或 Node 环境里先 hook 住常见的加密库调用 // 以 CryptoJS 为例很多 App 的 JS 侧都用它做 AES (function () { var _aes CryptoJS.AES.encrypt; CryptoJS.AES.encrypt function (msg, key, cfg) { console.log([AES encrypt] msg, msg.toString()); console.log([AES encrypt] key, key.toString()); console.log([AES encrypt] cfg, JSON.stringify(cfg)); return _aes.apply(this, arguments); }; })();这段 hook 的作用是把加密的输入、密钥、配置全部打出来。逻辑说明不改动原函数行为只在调用前后插一层日志apply(this, arguments)保证原逻辑照常执行。参数说明msg是待加密明文key是密钥可能是字符串也可能是 WordArraycfg里通常能看到 modeCBC/ECB和 paddingPkcs7 等。跑一遍之后你就能拿到「明文长什么样、密钥是什么、用的什么模式」这三样凑齐C# 侧就能复刻同样的加密。如果 hook 不到 CryptoJS说明它用的是自己实现的加密或者 WebAssembly。这时候退一步直接在 Sources 面板里对可疑函数下断点看调用栈。调用栈里往往能看到一个「组装参数」的函数那个函数就是你要找的入口。2.3 把 JS 参数翻译成 C# 配置拿到算法之后落到 C# 里就是一段标准的加密 拼装。下面是一个 AES-CBC 的复刻示例对应上面 hook 到的场景using System; using System.Security.Cryptography; using System.Text; public static class MqttSign { // key 和 iv 来自 JS 侧 hook 结果注意长度必须是 16/24/32 字节 private static readonly string Key 从JS里扒出来的key; private static readonly string Iv 从JS里扒出来的iv; public static string BuildPassword(long timestamp, string deviceId) { // 明文拼接顺序必须和 JS 完全一致差一个分隔符就对不上 string plain ${deviceId}|{timestamp}|mqtt; using (var aes Aes.Create()) { aes.Key Encoding.UTF8.GetBytes(Key); aes.IV Encoding.UTF8.GetBytes(Iv); aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; var enc aes.CreateEncryptor(); byte[] input Encoding.UTF8.GetBytes(plain); byte[] output enc.TransformFinalBlock(input, 0, input.Length); return Convert.ToBase64String(output); } } }逻辑说明BuildPassword复刻 JS 侧的加密流程输入是时间戳和设备号输出是 Base64 字符串直接作为 MQTT 连接的 Password 字段。参数说明Key、Iv必须和 JS 完全一致长度不对会直接抛异常plain的拼接顺序、分隔符、大小写都是敏感点JS 里是deviceId | timestamp还是timestamp deviceId必须逐字符核对。CipherMode和PaddingMode也要对上JS 用 ECB 你写 CBC结果就是一堆乱码。提示时间戳单位要对齐。JS 的Date.now()是毫秒C# 的DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()也是毫秒但有些服务端要的是秒差三个数量级连上去就被踢。3. WinForm 端搭 MQTT 订阅链路连接、订阅、收数三件事3.1 选库与连接参数C# 侧连 MQTT常见选择是 MQTTnet纯托管、跨平台、API 清晰WinForm 里用完全够。NuGet 装MQTTnet即可不需要额外原生依赖。连接参数里几个关键点ClientID 要唯一重复会被 Broker 顶掉Username/Password 用上一章算出来的值KeepAlive 别设太大比赛数据推送频繁设 30 秒左右比较稳CleanSession 一般设 true避免残留会话干扰。using MQTTnet; using MQTTnet.Client; using System.Threading.Tasks; public class MqttClientWrapper { private IMqttClient _client; public async Task ConnectAsync(string host, int port, string clientId, string user, string pass) { var factory new MqttFactory(); _client factory.CreateMqttClient(); var options new MqttClientOptionsBuilder() .WithTcpServer(host, port) .WithClientId(clientId) .WithCredentials(user, pass) .WithKeepAlivePeriod(TimeSpan.FromSeconds(30)) .WithCleanSession(true) .Build(); // 断线重连比赛数据不能断断了要能自己回来 _client.DisconnectedAsync async e { await Task.Delay(3000); await _client.ConnectAsync(options); }; await _client.ConnectAsync(options); } }逻辑说明ConnectAsync负责建立连接并挂上断线重连。参数说明host/port是 Broker 地址逆向时从 JS 里拿clientId建议用设备号加随机后缀避免多开冲突WithKeepAlivePeriod控制心跳间隔WithCleanSession(true)表示不保留会话。DisconnectedAsync里延迟 3 秒重连是为了避开 Broker 的短时拒绝直接死循环重连容易被限流。3.2 订阅主题与消息分发连上之后就是订阅。Topic 通常带通配符比如match//odds表示所有比赛的赔率。订阅时 QoS 选 0 还是 1 要看场景QoS 0 最快但可能丢QoS 1 保证至少一次但可能重复。比赛数据一般 QoS 0 就够丢了下一帧马上补上。public async Task SubscribeAsync(string topic) { _client.ApplicationMessageReceivedAsync e { string payload System.Text.Encoding.UTF8.GetString( e.ApplicationMessage.PayloadSegment); string topicName e.ApplicationMessage.Topic; // 分发到 UI 线程WinForm 控件不能跨线程直接改 Form1.Instance.BeginInvoke(new Action(() { Form1.Instance.AppendLog(${topicName} {payload}); })); return Task.CompletedTask; }; await _client.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() .WithTopicFilter(f f.WithTopic(topic).WithQualityOfServiceLevel( MQTTnet.Protocol.MqttQualityOfServiceLevel.AtMostOnce)) .Build()); }逻辑说明ApplicationMessageReceivedAsync是收数回调把 payload 转成字符串后通过BeginInvoke丢回 UI 线程。参数说明PayloadSegment是消息体注意有些推送是二进制直接 UTF8 解码会乱得先判断是不是 Protobuf 或自定义二进制格式WithQualityOfServiceLevel里AtMostOnce对应 QoS 0AtLeastOnce对应 QoS 1。WinForm 里跨线程更新控件是血泪经验不BeginInvoke直接改程序跑一会儿就崩。3.3 消息解码从二进制到可读数据如果 payload 是明文 JSONJsonConvert.DeserializeObject就完事。但比赛类推送很多是二进制常见的是 Protobuf 或者自定义的紧凑格式。判断方法很简单看前几个字节是不是可打印字符不是就大概率是二进制。Protobuf 的话需要拿到.proto文件或者从 JS 里还原字段编号这一步比 MQTT 连接本身还费时间。// 假设已确认是 Protobuf用生成的类反序列化 public MatchOdds DecodeOdds(byte[] payload) { using (var stream new System.IO.MemoryStream(payload)) { // MatchOdds 是从 .proto 生成的 C# 类 return MatchOdds.Parser.ParseFrom(stream); } }逻辑说明ParseFrom把二进制流还原成对象。参数说明payload是原始字节不要先转字符串再转回来那样会破坏二进制。如果拿不到.proto常见做法是用工具从二进制样本里猜字段类型或者回到 JS 里找decode函数看它按什么顺序读字节。4. 避坑与排查那些让程序跑不过夜的问题4.1 连上就断日志显示 5 秒内被踢现象MQTT 连接建立成功但几秒后DisconnectedAsync触发反复重连。原因ClientID 重复或者 Username/Password 算错。Broker 对重复 ClientID 的处理是顶掉旧连接两个客户端互相顶就成了死循环。解决ClientID 加 GUID 后缀把算出来的 Password 和 JS 侧抓到的做逐字符比对重点看 Base64 里的、/、有没有被 URL 编码。4.2 订阅成功但收不到任何消息现象SubscribeAsync返回成功回调一次都不触发。原因Topic 写错或者 QoS 不匹配或者 Broker 要求先发一条「上线」消息才推数据。解决先用 MQTT 客户端工具手动订阅同样的 Topic 验证检查 Topic 大小写和通配符层级match//odds和match/*/odds不是一回事有些服务端要求客户端先往某个 Topic 发一条心跳不发就不推。4.3 消息乱码UTF8 解码出来是问号现象payload 转字符串后全是????或方块。原因消息是二进制格式不是文本。解决先打印前 16 个字节的十六进制看有没有 Protobuf 的特征字段号 类型确认是二进制后回到 JS 里找解码函数或者用 Protobuf 工具反推 schema。别硬用 UTF8 解解出来也没法用。4.4 程序跑几小时后内存暴涨现象任务管理器里内存持续上升最后卡死。原因ApplicationMessageReceivedAsync里每次都new对象、拼字符串GC 跟不上推送速度或者事件重复订阅一个消息触发多次回调。解决收数回调里尽量复用缓冲区日志做限流比如每秒最多打 10 条订阅事件只挂一次重连后不要重复。4.5 时间戳对不上导致签名失效现象本地测试通过部署到另一台机器就连不上。原因机器时区或系统时间偏差导致算出来的时间戳和 JS 侧不一致。解决统一用 UTC 时间DateTimeOffset.UtcNow部署前先w32tm /resync同步系统时间如果服务端校验时间窗口很窄比如 ±30 秒本地时间偏差超过窗口就直接失败。5. 进阶把逆向结果做成可维护的采集端逆向出来的东西有个特点——它是活的。服务端改一次协议你这边就得跟一次。所以真正有价值的不是「这一次连上了」而是「下次改了我能快速定位到改哪」。我的习惯是把整个链路拆成三层参数层签名、ClientID、Topic 规则、传输层MQTT 连接、重连、QoS、解码层payload 解析。每层单独一个类层与层之间只传原始数据不互相依赖。这样协议一变你只需要改对应那一层不用满项目找。参数层建议做成可配置。把 Key、Iv、Topic 前缀、Broker 地址这些抽到appsettings.json或者一个独立的config.json里改的时候不用重新编译。下面是一个配置读取的示例using System.Text.Json; public class MqttConfig { public string Host { get; set; } public int Port { get; set; } public string TopicPrefix { get; set; } public string AesKey { get; set; } public string AesIv { get; set; } public static MqttConfig Load(string path) { string json System.IO.File.ReadAllText(path); return JsonSerializer.DeserializeMqttConfig(json); } }逻辑说明Load从 JSON 文件读配置并反序列化。参数说明path是配置文件路径建议放在程序目录下方便现场改AesKey/AesIv这类敏感值不要硬编码进源码放配置文件里至少改起来快。注意System.Text.Json默认区分大小写JSON 里的字段名要和类属性名一致或者加PropertyNameCaseInsensitive true。验证方法上我一般会做两件事。一是拿 JS 侧抓到的原始消息和 C# 解码结果做对比字段值对不上就说明解码层有问题二是让程序连续跑 24 小时看断线重连次数和内存曲线重连次数突然变多往往意味着服务端在调整策略。这两步走完基本能判断这套采集端能不能上生产。从那以后我每次做完一个逆向采集端都会强制走一遍「配置外置 分层解耦 24 小时压测」这三步少一步后面都要还债。希望帮到你。本文还有配套的精品资源点击获取