35、网络协议逆向(四):游戏协议逆向、加密通信还原

游戏协议逆向,说实话,是逆向工程里最有意思的一块。为什么?因为游戏厂商为了防外挂、防作弊,往往会在通信协议上花大功夫。我这些年拆过的游戏客户端少说也有几十款,从早期的MMORPG到现在的手机吃鸡,协议加密手段真是五花八门。

今天咱们就聊聊,怎么把游戏里那些加密的通信协议一层层扒开,还原出原始数据。说白了,就是让游戏客户端跟服务器之间的悄悄话,变成你能看懂的白话文。

游戏协议的基本套路

大多数游戏协议,底层还是走TCP或UDP。但应用层上,各家都有自己的封装方式。我总结下来,常见的结构大概是这样:

游戏协议包结构(典型)

+----------------+----------------+----------------+----------------+
|  魔数/标志      |  包体长度       |  协议ID        |  包体数据      |
|  (4字节)        |  (4字节)       |  (2/4字节)     |  (变长)        |
+----------------+----------------+----------------+----------------+
|  可选:校验和    |  可选:序列号   |  可选:加密头   |
+----------------+----------------+----------------+

嗯,这里要注意:魔数通常是固定的几个字节,比如 0xABCDEF01 这种。协议ID用来区分是登录、移动还是攻击。包体数据可能是明文,也可能是加密的。

我个人习惯,拿到一个游戏客户端后,第一件事不是去翻代码,而是先抓包。用Wireshark或者Fiddler,先看看原始数据长什么样。你想想看,如果连明文都看不到,那后面的分析就无从下手了。

抓包与初步分析

抓包工具我推荐两个:

  • Wireshark:适合PC端游戏,能过滤TCP/UDP流
  • Packet Capture / HttpCanary:适合Android端,能抓本地VPN流量

抓包之后,你会看到一堆十六进制数据。别慌,先找规律。我记得有一次分析某款MOBA游戏,发现所有数据包的前4个字节都是 0xFE 0xED 0xFA 0xCE。这就是魔数,用来标识协议包的起始位置。

找到魔数后,下一步就是确定包体长度字段。通常紧跟在魔数后面。你可以手动算一下:如果魔数占4字节,那第5-8字节很可能就是长度。验证方法很简单——把长度字段的值加上8(头部长度),看看是不是等于整个包的大小。

小技巧:如果抓到的包看起来全是乱码,别急着放弃。先看看是不是压缩了。很多游戏会用zlib或lz4压缩包体。你可以在内存中搜索 0x78 0x9C(zlib的魔数),如果找到了,那基本就是压缩数据。

加密通信的常见手段

游戏协议加密,说白了就那几招。我列个表,大家对照着看:

加密方式 特征 破解思路
XOR异或 数据看起来有规律,比如每隔几个字节重复 找密钥,或者用已知明文攻击
AES对称加密 数据分布均匀,没有明显规律 在客户端内存中找密钥或IV
RC4流加密 加密后长度不变,但内容完全随机 Hook加密函数,dump密钥
自定义算法 看起来像乱码,但又有某种模式 逆向加密函数,还原算法逻辑

我曾经遇到过一个手游,用的是XOR加密,但密钥是动态生成的——每次连接服务器,客户端会先发一个随机数,服务器用这个随机数算出密钥。我当时怎么破的?直接在客户端里Hook了那个生成密钥的函数,把密钥dump出来。嗯,这招屡试不爽。

实战:还原加密通信

咱们拿一个简化版的例子来演示。假设游戏客户端用AES-128-CBC加密包体,密钥和IV都硬编码在代码里。怎么找?

第一步,用IDA或Ghidra打开客户端主程序。搜索AES相关的常量,比如S盒(0x63, 0x7C, 0x77...)。如果找到了,那基本就是用了OpenSSL或者自己实现的AES。

第二步,找到加密函数。通常加密函数会调用 AES_set_encrypt_keyAES_cbc_encrypt。在调用处下断点,运行游戏,看传入的密钥和IV是什么。

第三步,拿到密钥后,写个脚本解密所有包。Python代码大概长这样:

from Crypto.Cipher import AES
import binascii

key = bytes.fromhex('0123456789ABCDEF0123456789ABCDEF')
iv = bytes.fromhex('FEDCBA9876543210FEDCBA9876543210')

def decrypt_packet(data):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    # 去掉前8字节的头部(魔数+长度)
    encrypted_body = data[8:]
    decrypted = cipher.decrypt(encrypted_body)
    # 去掉PKCS7填充
    pad_len = decrypted[-1]
    return decrypted[:-pad_len]

# 示例:解密一个包
packet_hex = 'FEEDFACE0020' + '加密数据...'
raw = binascii.unhexlify(packet_hex)
print(decrypt_packet(raw))

注意:有些游戏会在加密前先对数据进行序列化,比如用Protobuf或FlatBuffers。这种情况下,解密后得到的还不是最终数据,还需要反序列化。我建议先解密,再根据协议ID去匹配对应的消息结构。

协议逆向的完整流程

说了这么多,咱们把整个流程串起来。下面这张图,是我自己总结的协议逆向路线图:

游戏协议逆向流程图 1. 抓包获取原始数据 2. 识别协议头部结构 3. 判断是否加密 加密? 分析加密算法 Hook/逆向 直接解析 明文数据 4. 还原协议ID与消息映射 5. 编写解析脚本/工具 核心思路: 从网络层 → 传输层 → 应用层 逐层剥离,还原原始数据

你看,整个流程其实不复杂。关键就三步:抓包、分析、还原。但每一步都可能遇到坑。我举个例子,有一次我分析某款FPS游戏,抓到的包全是UDP,而且每个包只有几十个字节。一开始我以为是加密了,后来发现其实是用了自定义的二进制协议,每个字段都是按位排列的。那叫一个头疼。

避坑指南

做游戏协议逆向,有几个坑我踩过,跟大家分享一下:

  • 别忽略心跳包:很多游戏会定期发心跳包,长度固定,内容也固定。这些包可以用来验证你的解密逻辑对不对。
  • 注意协议版本:游戏更新后,协议可能变。我建议每次更新都重新抓包对比一下。
  • 小心反调试:有些游戏会在加密函数里加反调试代码,比如检测断点、检测调试器。遇到这种情况,可以用硬件断点或者直接patch掉反调试代码。
  • 多关注内存:有时候密钥不在代码里,而是在运行时动态生成的。这时候就需要在内存中搜索了。我习惯用Cheat Engine或者Frida来dump内存。

我的经验:如果你实在找不到密钥,可以试试另一种思路——Hook游戏客户端的发送函数。比如Windows下Hook send()WSASend(),在数据发出前截获。这时候数据还是明文状态,直接就能看到。虽然这不算严格意义上的协议逆向,但胜在简单粗暴。

总结

游戏协议逆向,说白了就是一场猫鼠游戏。厂商想方设法加密,我们想方设法还原。但万变不离其宗——只要客户端能解密,你就能找到解密的方法。因为密钥和算法最终都会出现在客户端的内存里,这是绕不过去的。

我个人觉得,做这行最重要的是耐心。有时候一个协议可能要分析好几天,但当你看到解密后的数据整整齐齐地显示在屏幕上时,那种成就感,嗯,值得。

好了,今天就聊到这儿。如果你在实际操作中遇到什么问题,欢迎来公众号找我交流。


公众号:蓝海资料掘金营,微信 deep3321