做移动端网络联调的时候,HTTP层面的抓包几乎算不上难题:给手机挂一个代理,打开Fiddler,所有请求响应的结构、参数、状态码都能看得一清二楚。可一旦游戏切换到了WebSocket长连接,很多人的工具链就当场失灵。原因其实不复杂——WebSocket不是一问一答的离散请求,而是握完手之后就一直保持的长连接,服务端随时能主动往客户端推消息,消息体也可能是二进制帧,之前的HTTP分析思路根本套不进去。
这篇文章要聊的,正是怎么用Fiddler Script去拦截和修改手游里的WebSocket封包。不管你是游戏客户端开发、服务端联调,还是想搞懂自家游戏协议的底层设计,这套方法都能让你在不改游戏代码的前提下,在代理层直接操纵封包内容。我会从环境准备、脚本钩子选择,一直讲到底层字节和长度校验的处理,尽量把每一步能踩的坑提前替你踩完。
1. 为什么HTTP抓包够了,WebSocket却让人头疼?
1.1 HTTP和WebSocket在抓包视角下的本质差异
HTTP是典型的请求-响应模型:客户端发起一次请求,服务端给出一次响应,然后这一次交互就结束了。Fiddler的会话列表正是为这种模型设计的,每个HTTP会话都能独立展示请求头、响应体、耗时,以及证书信息。
WebSocket完全是另一种玩法。握手阶段借用HTTP协议,发起一个带Upgrade: websocket的请求,服务端返回101 Switching Protocols之后,握手结束,后面连接就升级成一种全双工的长连接。客户端能发消息,服务端也能主动推消息,双方在同一条TCP连接里持续收发帧。Fiddler的会话列表里,这条连接最终会稳定地显示为一个特殊会话,但不体现具体每一帧的内容。
对调试者来说最大的坎在于:用HTTP那套思路去看WebSocket,只能看到握手成功那一刻,后续的所有帧在普通HTTP视图里几乎不可见。手游的网关通信基本都在WebSocket上,协议设计又往往不是简单的明文JSON,而是自研的二进制封包——可能是“前 4 字节长度 + 2 字节命令字 + payload”的结构,也可能直接套了protobuf或压缩数据。你不光要看到帧,还得具备在代理层把它们拆开、改动、再封回去的能力。
1.2 市面抓包方案选型对比
先横向比较一下几个常见方案,方便理解为什么最终应该选Fiddler Script。
Wireshark能看到底层所有TCP帧,但抓到的是TLS加密后的密文,想看到WebSocket明文,得先把主密钥喂给Wireshark,配置链路比较长。改包能力更是基本为零,它定位是网络分析,不是篡改工具。
Charles是很多人日常调试HTTP的首选,界面友好,断点功能可以临时修改请求和响应。但对于WebSocket消息级别的自动化修改,Charles的脚本能力偏向于简单的重写规则,没法像Fiddler Script那样在代理内部对每一帧做编程式处理。
Fiddler加Fiddler Script的组合,等于在一个成熟的HTTPS代理中间,塞进一个JScript.NET执行环境。你能复用Fiddler的证书解密、会话管理、过滤器,然后在WebSocket消息经过代理的瞬间,写代码去读取、改写、甚至丢弃或重放数据。这套组合对大规模批量改包、自动化联调测试非常适用。
工具 | 能看到WebSocket明文 | 能改WebSocket消息 | 脚本编程能力 | 上手成本 Wireshark | 需要额外配置密钥 | 基本不可用 | 无 | 高 Charles | 可以 | 弱,偏重放和断点 | 有限 | 中 Fiddler + Script | 可以 | 强,可编程修改 | 强 | 中
1.3 Fiddler Script在WebSocket调试中的定位
Fiddler Script本质上不是独立的抓包工具,而是Fiddler内置的一款脚本引擎。它挂在Fiddler的事件体系里,可以理解为代理服务在处理请求的不同阶段,会调用你写的自定义函数。只要流量经过这个代理,你就有机会插手。
在WebSocket场景下,关键事件有三个:TCP层面的OnBeforeSocketConnect,连接成功时的OnWebSocketConnect,以及每个消息帧被代理转发时的OnWebSocketMessage。OnWebSocketMessage是后面重头戏所在,因为它让代理有能力在客户端帧转发给服务端之前、或者服务端数据返回客户端之前,把数据拦下来改掉。
有个前提必须说清楚:Fiddler能处理WebSocket明文,依赖两个条件,一是客户端确实通过Fiddler建立的代理隧道访问网关,二是HTTPS解密或明文WebSocket流量能到达Fiddler。如果游戏客户端用了强制不走系统代理的网络库,或者私有加密通道,那么这一套方案需要先解决流量让Fiddler可见的问题,后面第4章还会讲到。
2. 抓包前的环境准备,一条都不能少
2.1 手机与PC联动:代理、端口与防火墙
先把基础环境搭起来。PC上打开Fiddler,默认监听端口是8888,这个端口不能随便改,后面手机代理要与之完全一致。手机和PC必须处于同一个局域网,最好连同一个WiFi,然后查看PC局域网IP,在手机WiFi设置里选择“手动代理”,把服务器地址填为PC的IP,端口填8888。
这一步常见的问题是Windows防火墙默认没有放行Fiddler的流量,手机一连代理就超时。处理方式是打开Windows防火墙的“允许应用通过防火墙”,找到Fiddler对应的程序,勾选专用网络,或者临时放行TCP 8888端口。放行后用手机浏览器访问http://PC的IP:8888,如果能弹出Fiddler的证书下载页,说明基础链路是通的。
2.2 HTTPS证书安装与安卓高版本信任问题
WebSocket的安全版本wss://跑的是TLS加密通道,Fiddler想看到里面的明文,必须在手机上安装并信任Fiddler的根证书。在Tools菜单里开启HTTPS Decrypt后,手机上打开刚才那个下载页,下载FiddlerRoot certificate并安装。
Android 7.0及以上有个很容易踩的坑:普通App默认只信任系统证书库,不信任用户手动安装的证书。就算你证书装好了,很多App在握手时仍然会报证书验证失败,Fiddler里表现为一串Certificate error或TLS handshake failed。
解决方案分情况:如果是自己的调试App,在AndroidManifest里配置networkSecurityConfig,允许信任用户证书即可;如果是第三方App,就需要把Fiddler证书迁移到系统证书库,这要求设备已解锁或使用模拟器。在实际工作中,我一般优先确保目标游戏是自家项目的调试包,否则证书环节会让你花掉大量时间。
2.3 从堆积如山的会话里识别WebSocket连接
当所有HTTPS流量都经过Fiddler后,如何找到WebSocket那条会话?最直接的标志是状态栏里的101,这是HTTP协议升华为WebSocket的切换响应。会话列表里找到状态为101的那一条,点击后在底部Inspectors区域可以看到协议信息,通常标着WebSocket。
更省力的办法是使用Fiddler的过滤器,在Filters页签下勾选只显示WebSocket流量,或者先用一条测试脚本把所有WebSocket会话的主机地址打出来,方便快速定位目标网关。实际调试大型手游时流量非常杂,图片、日志、业务接口混在一起,不提前做过滤,找一个WebSocket连接就像在沙子里找针。
3. Fiddler Script拦截与修改WebSocket封包的核心实现
3.1 打开Fiddler Script编辑器,先跑通第一个日志脚本
Fiddler Script编辑器入口在Rules菜单下,选Customize Rules,Fiddler会用默认编辑器打开一个名为CustomRules.js的脚本文件。这个文件中已经预置了OnBeforeRequest、OnBeforeResponse等常用函数,直接在这个文件里追加或修改函数即可。
先别急着写复杂逻辑,按惯例先跑一个最小验证脚本。往文件末尾添加如下代码,保存后立刻关闭再重开一个WebSocket连接:
static function OnWebSocketMessage(oWSMessage: WebSocketMessage): void { FiddlerObject.Log("[WS] 消息类型: " + oWSMessage.Type + " 方向: " + (oWSMessage.IsOutbound ? "客户端->服务端" : "服务端->客户端")); }保存后回到Fiddler主界面,左下角如果显示Script Editor: Ready,说明脚本语法正常。此时触发游戏里的任意一个需要走WebSocket的操作,就能在Log面板里看到消息记录。这一步的价值是验证OnWebSocketMessage事件真的被框架调用了,排除后续问题出在脚本没加载或者签名不对。
3.2 实战:修改一条JSON文本帧
假设目标游戏发送的WebSocket消息是文本帧,内容是一段JSON,原始数据大致如下:
{"cmd":"buy","itemId":100,"count":1}现在要把count从1改成99,然后发给服务端。注意OnWebSocketMessage中IsOutbound为true表示消息从客户端发往服务端,false表示服务端推给客户端,方向搞反会导致修改完全不生效。
脚本实现如下:
static function OnWebSocketMessage(oWSMessage: WebSocketMessage): void { if (!oWSMessage.IsOutbound) { return; } var s: String = oWSMessage.DataAsString; FiddlerObject.Log("[WS Out] " + s); if (s.indexOf("\"cmd\":\"buy\"") >= 0) { s = s.Replace("\"count\":1", "\"count\":99"); oWSMessage.DataAsString = s; FiddlerObject.Log("[WS Modified] " + s); } }DataAsString是Fiddler Script提供的字符串化读写属性。赋值后框架会用原始帧的编码方式重新编码数据,对文本帧来说通常没有问题。但有一点必须留意,如果游戏内部用的不是UTF-8而是GBK,字符串替换后可能会出现乱码,需要在脚本里明确处理编码转换。
这组修改逻辑本身不复杂,真正的坑在于:很多游戏并不可信JSON文本帧,甚至文本帧只是外壳,正文另有二进制内容。这时候DataAsString阅读起来已经困难,单纯字符串替换就更不靠谱,你必须走到二进制层面。
3.3 二进制封包:处理好长度字段和校验值
手游里最常见的做法是自定义二进制协议,封包格式大致是长度(4字节) + 命令字(2字节) + payload。修改payload之后,长度字段必须同步更新,否则服务端按错误的长度边界解析,大概率直接断开连接。这里以IAsyncResult风格的字节数组操作演示一下:
static function OnWebSocketMessage(oWSMessage: WebSocketMessage): void { if (!oWSMessage.IsOutbound) { return; } var bytes: System.Byte[] = oWSMessage.Data; // 假设前4字节是小端序长度字段 var origLen: Int32 = (bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24)); FiddlerObject.Log("[WS Binary] 原始长度=" + origLen + " 实际数据长度=" + bytes.Length); // 解析命令字,这里假设是第5、6字节,小端序 var cmd: Int32 = (bytes[4] | (bytes[5] << 8)); if (cmd === 0x0101) { // 假设0x0101是购买命令 // 假设payload第7、8字节是数量,小端序 var countVal: Int32 = (bytes[6] | (bytes[7] << 8)); FiddlerObject.Log("[WS Binary] 当前数量=" + countVal); // 修改为99 bytes[6] = 99 & 0xFF; bytes[7] = (99 >> 8) & 0xFF; // 如果修改前后payload长度不变,长度字段可以不动 // 如果payload长度变了,必须重算长度 var newLen: Int32 = bytes.Length - 4; // 假设长度字段只包含命令字+payload bytes[0] = newLen & 0xFF; bytes[1] = (newLen >> 8) & 0xFF; bytes[2] = (newLen >> 16) & 0xFF; bytes[3] = (newLen >> 24) & 0xFF; oWSMessage.Data = bytes; FiddlerObject.Log("[WS Binary] 已修改,新长度=" + newLen); } }这段代码是典型的“只改记录,不改schema”的做法,适用于绝大部分游戏内常见的数值型改动。但需要注意几个细节。
第一,长度字段的字节序必须严格按协议来。小端序在手游协议中非常常见,但也不排除有大端序的。一旦字节序搞错,连接会立刻被打断。
第二,如果协议里除了长度字段还有CRC32、MD5等校验字段,你只改数值不重算校验,服务端校验时照样会拒收。这种时候需要先搞清楚校验字段的位置和算法。有的游戏在封包尾部放了8字节hash,网上能搜到的通用解法是先用同样的哈希算法根据修改后的payload重新计算,再回写到对应的字段。如果算法不清楚,脚本就只能停留在“能抓不能改”的阶段,这也是二进制协议调试里最难啃的部分。
第三,不要假设客户端和服务端接收超长payload。某些早期游戏的服务端用固定的字节数组接收消息,本来1字节的数量字段被改成4字节,对其他字段的偏移全是错的,远比你预想的更容易搞挂连接。
3.4 在握手阶段动手:修改请求头与连接参数
有些时候你要改的不是具体某一条消息,而是WebSocket握手时携带的参数。比如客户端默认的Sec-WebSocket-Protocol子协议与测试环境不匹配,或者Origin头域被服务端校验。这种情况下,可以在握手请求被代理转发前动手。
Fiddler会把握手请求当成一个普通HTTP会话来处理,OnBeforeRequest就能拿到它。典型的写法是:
static function OnBeforeRequest(oSession: Session): void { if (oSession.HTTPMethodIs("CONNECT")) { return; } if (oSession.PathAndQuery.indexOf("ws") >= 0 || oSession.PathAndQuery === "/gateway") { oSession.RequestHeaders["Origin"] = "https://debug.example.com"; oSession.RequestHeaders["Sec-WebSocket-Protocol"] = "proto-v2"; } }需要说明的是,并非所有WebSocket握手都需要改头部,具体取决于服务端的校验范围。在做接口兼容性测试时,这个技巧非常管用,因为你不需要重新打包客户端,直接在代理层就能模拟不同来源的连接。
3.5 一条完整的工作流:从只读日志到批量改写
根据我的实操经验,Fiddler Script改包最忌讳的是“拿到脚本就动手改数据”。完整的推进顺序应该是下面这样。
第一步,先只打日志。把目标流的消息类型、方向、长度、文本内容全部输出到Log面板,运行至少10分钟,确认你确实看到了目标封包,并且对消息格式有了初步判断。不要跳过这一步直接尝试修改,否则一旦改了内容导致服务端异常,你甚至无法判断是原始消息本来就有问题,还是你的脚本改出了问题。
第二步,在日志基础上确定协议格式。如果是JSON文本,字段含义比较好懂;如果是二进制,先根据已知消息对照工具抓包,把命令字、长度、字段偏移都估算出来。务必把你推断的协议格式记录在脚本顶部注释里,方便回看。
第三步,实现转换函数并开启修改。修改逻辑尽量独立成一个函数,不直接堆在OnWebSocketMessage里,这样出了错容易定位。改完之后立刻看Log面板里有没有异常,同时观察游戏客户端是否出现掉线、重连或逻辑错误。
第四步,把自定义规则文件备份一份。Fiddler升级或重装时会覆盖CustomRules.js,不备份等于白干。
4. 实测排查与踩坑记录,写完即用
4.1 抓不到WebSocket会话的几类原因
我见过不少同学卡在“Fiddler已经开了,HTTP都正常,但WebSocket会话一条都见不到”。原因多数出在下面这几类。
现象可能原因解决方案 连接没有101响应目标服务端是wss,但HTTPS解密未开启,Fiddler只看到隧道看不清内容开启Decrypt HTTPS traffic,并确认证书已安装在设备上 手机代理配置正常但完全抓不到包游戏客户端底层的网络库未走系统代理,例如某些自研跨平台网络库直接发Socket,不理会系统代理设置改用虚拟WiFi或旁路代理,把流量透明导入Fiddler;或使用支持强制代理的模拟器 看到握手但后续没有消息Fiddler与客户端的WebSocket协商版本不兼容,或客户端发起的是TLS 1.3,Fiddler解密受限升级Fiddler版本,关闭客户端的TLS 1.3强制回退,或在脚本里检查握手响应状态 过滤条件误伤Filters里设置只显示WebSocket,导致WebSocket会话被隐藏,但HTTP会话正常检查Fiddler的Filters页签,确认没有勾选只显示特定协议或域名
4.2 修改封包后连接立刻被断开的排查思路
如果你成功修改了消息,但紧接着连接就被关闭,最常见的原因就是前面反复强调的长度或校验问题。连接断开前一般会收到一个1006或1002错误码,1006代表异常关闭,1002代表协议错误。
第一步,立刻注释掉修改逻辑,只保留日志输出,复测一次。如果只读日志不会导致断开,基本可以排除网络问题,问题一定出在你改动的内容上。
第二步,检查长度字段。确认你改完数据后重新计算并回写了长度字段,同时确认长度字段的字节序和边界定义。注意一个容易忽略的细节:有些协议的长度字段只统计payload,不包括自身4字节长度字段,计算时要把这个偏移算清楚。
第三步,检查校验字段。看看封包里有没有CRC32、Adler32、MD5之类的字段,有的话老老实实重算。不要以为服务端不做完整性校验,现在手游服务端普遍有校验,改了数值不重算就等于自杀。
第四步,如果长度和校验都对,考虑一下你的修改是不是在结构上破坏了协议。比如把TCP流当成WebSocket帧处理,导致WebSocketMessage.Data并不包含完整业务封包,而只是流式分段,这种情况修改逻辑需要改成基于流的缓冲拼接,不能再直接操作Data字段。
4.3 性能与稳定性经验:别把脚本写成死循环
Fiddler Script跑在JScript.NET上,性能跟本地编译的C#有很大差距。如果你在OnWebSocketMessage里做大量字符串拼接、正则表达式匹配、甚至循环遍历整个字节数组,遇到高频心跳包的时候,代理会明显卡顿,游戏轻则掉线,重则把整个代理拖到假死。
个人建议,凡是能用简单indexOf判断的,不要用正则。凡是能提前算好的长度偏移,不要在消息回调里重复计算。日志输出在调试完成后尽量注释掉,因为每一行FiddlerObject.Log都会在UI上刷新一次,大量日志输出带来的性能消耗比脚本本身更大。
还有一点要注意脚本中静态变量的生命周期。Fiddler Script中的静态变量在代理进程生命周期内是常驻的,如果跨消息积累状态,比如上一次修改的上下文,一定要在合适的地方清掉,否则消息顺序一变,状态就可能串掉。
4.4 我的边界意识与使用建议
最后多说一句,Fiddler Script的能力边界在于“你能看懂多深的协议”。它能帮你改看得懂的明文或二进制封包,但改不了你看不懂的加密协议。如果目标游戏的消息体经过了AES、XXTEA这类对称加密,光在Fiddler Script里做字符串替换是无效的,你必须先定位密钥和算法,在脚本中引入System.Security.Cryptography去解密、修改、再加密,整个工作量和难度是另一个数量级。
在实操中我更倾向于把这套方法用在两类场景:一类是自家开发的游戏,联调时模拟异常数据包、验证服务端边界;另一类是纯学习协议设计,用Fiddler Script去理解消息结构和字段含义。对于没有授权或非学习目的的流量,建议还是保持克制,不要越过合法边界。
最后再分享一个实际踩过的坑
有一次我改一个MMO手游的二进制封包,目标是把某个任务奖励的数量从1改成10。脚本改完长度字段后,连接没有断,服务端也回了正常响应,但奖励数值始终没变。排查了一个下午,最后发现封包尾部还有一段8字节的哈希,它既不是CRC也不是MD5,而是服务端下发的一个动态盐值与数据拼接后算出来的签名。我改完payload,没重算这个哈希,服务端直接把数据当成了非法数据丢弃,只是没有断开连接而已。
那次之后我养成一个习惯:拿到一个新的二进制封包,先别急着写改包脚本,先把原始帧完整导出来,把长度、命令字、payload、尾部校验逐段标出来,彻底弄明白哪些字段是服务端校验的,再动手写转换逻辑。Fiddler Script只能改你能看懂的协议,改之前把协议格式梳理清楚,比什么高级脚本都管用。这个习惯,也是我能给准备上手WebSocket改包的同学最实在的建议。