Birditch的博客 Birditch的博客

拆开《生死狙击》的大厅:从 SWF 解密到自己造包

大厅的 SWF 分加密和不加密两种。最关键的那个反而没加密——这成了整条链路的突破口。

《生死狙击》是 4399 上的一款 Flash 页游。这篇记录的是我把它的大厅客户端拆开的过程:SWF 怎么解密、协议长什么样,以及摸清协议之后能做到哪一步。

Flash 本身已经在 2020 年底停止支持了,但这套路子没过时:先认清文件格式,找到解密入口,还原算法拿到钥匙,再顺着协议往上爬。换成今天任何一个客户端,路径都一样。

先说明:本文是技术分析记录,只讲原理和方法,不提供成品工具。


一、先认清 SWF 有哪些形态

动手之前得先知道自己面对的是什么文件。SWF 的格式和保护形态大致分这么几类。

标准 SWF:直接就能反编译

这类文件老老实实遵守 Adobe 的规范。开头 3 个字节是 Magic Number(文件标识),第 4 个字节是 Flash 版本号,再往后 4 个字节是解压后的总大小(小端序 uint32)。

Magic Number

格式

说明

FWS

未压缩

纯明文,第 8 字节起直接就是 RECT 帧尺寸和各个 Tag 标签流

CWS

Zlib 压缩

最常见的官方压缩,第 8 字节起是 deflate 数据流

ZWS

LZMA 压缩

Flash Player 11+ 引入,体积比 Zlib 更小

这三种 FFDec(JPEXS) 都能一键打开,AS2 / AS3 代码、矢量图、音频、二进制素材全都能导出。

加了保护的几种形态

为了防止源码和素材被扒,开发者一般会上这么几层防护。

1. 代码混淆

文件头还是标准的 FWS/CWS/ZWS,但里面的 ActionScript 字节码(ABC 块)被混淆工具处理过。常见手法:

  • 变量名、类名换成不可见字符、特殊 Unicode,或者一堆 _0x12abll11ll 这种鬼名字

  • 字符串加密——把静态字符串塞进 ByteArray,运行时再用自定义函数解出来

  • 控制流平坦化——插一堆 switch-case 假分支,把原本的代码结构打散

常见工具有 KindHelper、secureSWF、Amayeta SWF Encrypt、irrFuscator。FFDec 依然能打开,只是读起来费劲,得配合反混淆脚本或者 AST 还原工具。

2. 运行时解密外壳(Loader / Shell)

这个更狠。你拿到的 SWF 只是个十几 KB 的小外壳,头部标准,反编译进去啥也没有。真正的游戏逻辑被加密成一个二进制块,藏在外壳的 DefineBinaryData(Tag 87)里,或者干脆放在单独的 .bin / .dat 文件中。

运行时是这么走的:

  1. 外壳 Loader 启动

  2. 读出嵌进去的加密二进制流

  3. 在内存里用内置算法解密(RC4、AES、XXTEA,或者自己搓的异或 / 移位)

  4. flash.display.Loader.loadBytes(解密后的字节) 动态加载执行

破法有两条:反编译外壳、定位 loadBytes 的调用点,把密钥和算法逻辑抠出来,写脚本离线批量解;或者直接上 Flash 调试器 / Hook 内存,把喂给 loadBytes 的那段字节流 dump 下来。

3. 头部破坏

把开头几个字节改掉——比如 CWS 换成自定义的 XWSLWS,或者干脆把字节顺序颠倒,让通用工具认不出这是个合法 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.pageDomainloaderInfo.url,把校验逻辑用空指令(nop)替掉,或者改成固定返回 true,保存即可。

对照速查

类型

文件头特征

存放内容

应对方案

标准 SWF

FWS / CWS / ZWS

明文 AS2/AS3 及资源

FFDec 直接导出代码与资源

混淆 SWF

FWS / CWS / ZWS

变量名、控制流被打乱

FFDec 反混淆重命名 + AST 分析

Loader 外壳

外壳为 CWS / FWS

核心逻辑藏在 DefineBinaryData

逆向外壳 AS3,提取解密逻辑或 dump 内存

头部混淆

非标准头(魔改 3 字节)

修完头部就是标准 SWF

还原前 8 字节(Magic + Version + FileLength)

完全加密包

纯二进制乱码(.bin/.dat)

整个 SWF 或资源包被加密

逆出解密算法(XOR / RC4 / AES 等)批量还原


二、突破口:最关键的那个 SWF 居然没加密

大厅里那堆 SWF,加密的多半是游戏活动相关的。但最要紧的那个——Lobby_{md5}.swf——没有任何加密

所以直接上 FFDec 反编译,导出成 VSCode 项目就行。文件很大,这一步可能要跑上半个小时,泡杯咖啡等着。

拿到源码之后,顺着往下找解密逻辑:

  1. 入口追踪——Lobby.swf 启动时会引入 wdnative 原生模块

  2. 符号定位——wdnative/lobby_decoder/swfDecode.as 负责解密那些受保护的资源文件

  3. 数据反编译——swfDecode.as 里引用了一个只读数据段指针 L__2E_str324,它映射到 binaryData/3_wdnative_nternal.DS2.bin,里面存着一个常量字符串:3.1415

  4. 算法还原——每 1024 字节,在偏移 91 的位置,与 3.1415 做滚动异或

  5. 产物处理——解完直接就是标准 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)

  1. 建一个 ByteArray,设 objectEncoding = 3(AMF3 模式)和 endian = "bigEndian"

  2. byteArray.writeObject(apc),把 APC 按 AMF3 规则打成二进制字节流

  3. 压缩判定:如果 _compressFlag == 1,调 param1.compress() 做 Zlib 压缩

  4. 组装帧头发送

Socket Stream = Int32(PayloadLen + 1) + Byte(CompressFlag) + AMF3 Payload
                ╰────────┬──────────╯   ╰───────┬────────╯   ╰──────┬─────╯
                    4 字节总长          1 字节压缩标识(0/1)   序列化后的数据体

收包链路(TcpSocket.recvData)

  1. 处理粘包 / 分包:先从流里读 4 字节 TotalLen,再读 1 字节 CompressFlag

  2. 等着收满 dataLen = TotalLen - 1 字节的 Payload

  3. 如果 CompressFlag == 1,执行 byteArray.uncompress() 解压

  4. objectEncoding = 3,调 byteArray.readObject() 还原出 APC 对象,再派发 PACKET_ARRIVE 事件

和标准 AMF3 差在哪

标准 AMF3 规范(RTMP / Flash Remoting 那套)主要定义的是数据类型的二进制编码规则。Lobby 在落地时做了几处定制:

维度

标准 AMF3 / Flash Remoting

Lobby(生死狙击大厅)

传输层封装

常用于 HTTP POST 或 RTMP Chunk,带标准 Header / Message 结构体(TargetURI、ResponseURI 等)

自定义 TCP 帧头:砍掉 RTMP/HTTP 那套繁杂头部,简化成极紧凑的 [4B 长度 + 1B 压缩标志] 裸 Socket 协议

动态压缩

规范本身不含帧级别的压缩标记,一般靠外层 HTTP gzip 或全连接握手协商

帧头内嵌单字节压缩开关,支持逐包动态切换 Zlib,大包(背包列表、排行榜)省带宽效果明显

类映射 / 别名

推荐用 registerClassAlias 把 Typed Object 映射成具体的强类型 AS3 类

基本不依赖 registerClassAlias,全链路统一映射为通用动态对象 APC,参数用原生 Array / Object 传,降低前后端强类型耦合

通信模型

经典 Remoting 是严格的请求—响应模式,靠 MessageId 匹配返回

APC 异步单向派发:包体只有 functionName + parameters,既能当客户端主动发起的请求,也能无缝当服务端主动下发的推送广播

一句话:底层编码严格复用标准 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 时代是结束了,但这套方法论一点没过时。先认清文件格式,找到解密入口,还原算法拿到钥匙,再顺着协议往上爬——从一个圆周率当密钥的异或,一路走到能自己造包,中间每一步都是可复现的。