大厅的 SWF 分加密和不加密两种。最关键的那个反而没加密——这成了整条链路的突破口。
《生死狙击》是 4399 上的一款 Flash 页游。这篇记录的是我把它的大厅客户端拆开的过程:SWF 怎么解密、协议长什么样,以及摸清协议之后能做到哪一步。
Flash 本身已经在 2020 年底停止支持了,但这套路子没过时:先认清文件格式,找到解密入口,还原算法拿到钥匙,再顺着协议往上爬。换成今天任何一个客户端,路径都一样。
先说明:本文是技术分析记录,只讲原理和方法,不提供成品工具。
一、先认清 SWF 有哪些形态
动手之前得先知道自己面对的是什么文件。SWF 的格式和保护形态大致分这么几类。
标准 SWF:直接就能反编译
这类文件老老实实遵守 Adobe 的规范。开头 3 个字节是 Magic Number(文件标识),第 4 个字节是 Flash 版本号,再往后 4 个字节是解压后的总大小(小端序 uint32)。
这三种 FFDec(JPEXS) 都能一键打开,AS2 / AS3 代码、矢量图、音频、二进制素材全都能导出。
加了保护的几种形态
为了防止源码和素材被扒,开发者一般会上这么几层防护。
1. 代码混淆
文件头还是标准的 FWS/CWS/ZWS,但里面的 ActionScript 字节码(ABC 块)被混淆工具处理过。常见手法:
变量名、类名换成不可见字符、特殊 Unicode,或者一堆
_0x12ab、ll11ll这种鬼名字字符串加密——把静态字符串塞进 ByteArray,运行时再用自定义函数解出来
控制流平坦化——插一堆 switch-case 假分支,把原本的代码结构打散
常见工具有 KindHelper、secureSWF、Amayeta SWF Encrypt、irrFuscator。FFDec 依然能打开,只是读起来费劲,得配合反混淆脚本或者 AST 还原工具。
2. 运行时解密外壳(Loader / Shell)
这个更狠。你拿到的 SWF 只是个十几 KB 的小外壳,头部标准,反编译进去啥也没有。真正的游戏逻辑被加密成一个二进制块,藏在外壳的 DefineBinaryData(Tag 87)里,或者干脆放在单独的 .bin / .dat 文件中。
运行时是这么走的:
外壳 Loader 启动
读出嵌进去的加密二进制流
在内存里用内置算法解密(RC4、AES、XXTEA,或者自己搓的异或 / 移位)
调
flash.display.Loader.loadBytes(解密后的字节)动态加载执行
破法有两条:反编译外壳、定位 loadBytes 的调用点,把密钥和算法逻辑抠出来,写脚本离线批量解;或者直接上 Flash 调试器 / Hook 内存,把喂给 loadBytes 的那段字节流 dump 下来。
3. 头部破坏
把开头几个字节改掉——比如 CWS 换成自定义的 XWS、LWS,或者干脆把字节顺序颠倒,让通用工具认不出这是个合法 SWF。客户端主程序会在喂给 Flash 引擎之前先把字节改回来。
这种最好破:写个脚本把头部还原(比如前 3 字节替换回 CWS),必要时修一下长度字段就完事。
4. 整个文件加密
从头到尾全被打乱,一点 SWF 特征都没有,通常存成 .dat、.bin、.pak 或者干脆没后缀。常见算法:
固定 Key 异或 / 滚动异或(老页游最爱,有的甚至就是每个字节 XOR 一个常数,比如
0x5A)RC4 / AES-128-CBC / XXTEA
自定义查表法(用打乱的 S-Box 替换字节)
这种只能从游戏的微端(C++/C# 启动器)或者入口 Flash 里,把解密函数和密钥逆出来,再批量解。
5. 域名锁(DomainLock)
代码本身没深度加密,但主类构造函数里会查 Security.pageDomain 或者 root.loaderInfo.url。域名不对(比如你在本地离线跑)就抛异常、跳错误页,或者直接把显示列表清空。
FFDec 里搜 Security.pageDomain 或 loaderInfo.url,把校验逻辑用空指令(nop)替掉,或者改成固定返回 true,保存即可。
对照速查
二、突破口:最关键的那个 SWF 居然没加密
大厅里那堆 SWF,加密的多半是游戏活动相关的。但最要紧的那个——Lobby_{md5}.swf——没有任何加密。
所以直接上 FFDec 反编译,导出成 VSCode 项目就行。文件很大,这一步可能要跑上半个小时,泡杯咖啡等着。
拿到源码之后,顺着往下找解密逻辑:
入口追踪——
Lobby.swf启动时会引入wdnative原生模块符号定位——
wdnative/lobby_decoder/swfDecode.as负责解密那些受保护的资源文件数据反编译——
swfDecode.as里引用了一个只读数据段指针L__2E_str324,它映射到binaryData/3_wdnative_nternal.DS2.bin,里面存着一个常量字符串:3.1415算法还原——每 1024 字节,在偏移 91 的位置,与
3.1415做滚动异或产物处理——解完直接就是标准 SWF(头部的 CWS/FWS 恢复正常),交给
Loader.loadBytes()执行,或者用 FFDec 导出
就这么简单——圆周率前六位当密钥。
别小看这个 key,它在后面一系列解密里会反复用到,是整条链路的钥匙。
三、大厅的 AMF3:标准协议的魔改版
搞定 SWF 之后,就能完整读到通信协议了。大厅走的是 AMF3,但不是标准 AMF3——无端的技术人员在原版基础上改过。
整体架构
TCP Socket + 长度前缀分帧 + AMF3 序列化 + 可选 Zlib 压缩。一次调用大概这么走:
RPC 调用(APC 对象:functionName + parameters)
│
▼
1. AMF3 序列化(objectEncoding = 3)
│
▼
2. 可选压缩(zlib / deflate)
│
▼
3. 组帧 [4 字节包长][1 字节压缩标志][数据体]
│
▼
4. TCP 发送(bigEndian)核心载体:APC
所有请求和推送都被包成一个 APC(Asynchronous Procedure Call,异步过程调用)动态对象:
public dynamic class APC {
public var functionName: String; // 远程函数名,比如 "login"、"enterRoom"
public var parameters: Array; // 参数列表,可以是基础类型、字典、嵌套数组
}发包链路(TcpSocket.sendBytes)
建一个 ByteArray,设
objectEncoding = 3(AMF3 模式)和endian = "bigEndian"调
byteArray.writeObject(apc),把 APC 按 AMF3 规则打成二进制字节流压缩判定:如果
_compressFlag == 1,调param1.compress()做 Zlib 压缩组装帧头发送
Socket Stream = Int32(PayloadLen + 1) + Byte(CompressFlag) + AMF3 Payload
╰────────┬──────────╯ ╰───────┬────────╯ ╰──────┬─────╯
4 字节总长 1 字节压缩标识(0/1) 序列化后的数据体收包链路(TcpSocket.recvData)
处理粘包 / 分包:先从流里读 4 字节 TotalLen,再读 1 字节 CompressFlag
等着收满
dataLen = TotalLen - 1字节的 Payload如果 CompressFlag == 1,执行
byteArray.uncompress()解压设
objectEncoding = 3,调byteArray.readObject()还原出 APC 对象,再派发PACKET_ARRIVE事件
和标准 AMF3 差在哪
标准 AMF3 规范(RTMP / Flash Remoting 那套)主要定义的是数据类型的二进制编码规则。Lobby 在落地时做了几处定制:
一句话:底层编码严格复用标准 AMF3,外层传输协议高度精简并游戏化定制。序列化直接吃 ActionScript 原生的 ByteArray.objectEncoding = 3,又快体积又小;外面换成 4 字节长度 + 1 字节压缩标志的极简帧头 + 统一 APC 结构,长连接场景下延迟低、带宽省、吞吐高。
四、模拟登录
摸清协议之后就能自己造包了,甚至可以从头写一个更完善的客户端。第一步是模拟登录。
简化后的链路长这样:
模拟客户端(Python) 4399/微端认证 API 游戏网关(TCP Socket)
| | |
| ① 提交账号密码 / Cookie | |
|────────────────────────>| |
| ② username, timestamp, | |
| sign, isadult | |
|<────────────────────────| |
| | |
| ③ Socket Connect(IP & Port 取自 hall_config.xml) |
|────────────────────────────────────────────────────>|
| | |
| ④ APC("loginV3", [username, isadult, |
| timestamp, sign, 1, extParams, ""]) |
|────────────────────────────────────────────────────>|
| | |
| ⑤ APC 广播(syncUserInfo、进入大厅数据) |
|<────────────────────────────────────────────────────|注意:这条链路是简化后的结果,实际登录流程要复杂得多,本帖不展开。
五、握住收发包之后
完整的 AMF3 收发包能力到手,能做的事就多了,包括但不限于:
构造登录包拉数据——我的 strikegod.com 就是基于这个原理跑起来的
快速开宝箱
快速开始游戏
……
Flash 时代是结束了,但这套方法论一点没过时。先认清文件格式,找到解密入口,还原算法拿到钥匙,再顺着协议往上爬——从一个圆周率当密钥的异或,一路走到能自己造包,中间每一步都是可复现的。