这篇杂谈主要讲一下,我是怎么从客户端一步步逆向,最后做出私服的。
首先要理解一个概念,现代很多的联网游戏其实都是客户端+服务端这么一个架构的。
你的很多操作都会向服务端发出数据包,服务端也会回传数据包。
这一篇里讲的东西会很多,我会把我个人的开发经验拿出来分享。
先提一嘴我的设备有Root所以中途会使用Root能力。
Root:SukiSu + Zygisk Next 1.5.0 +LSPosed
要用到的工具有:
1.Node.js 用于服务端运行,部署与构建。
2.Python 抓包,解包等。
3.NDK 用来编译Zygisk模块。
4.CMake 用于Build。
5.ninja 用于Build。
6.ADB用来调试手机。
首先我先介绍一下我要开发的私服的那款游戏的背景
2D修仙放置类手机游戏,Unity+Tolua底层 有IL2CPP加密,且为服务端权威,102个拒绝码。
反作弊为:网易的 HTProtect | (libNetHTProtect.so,4.4 MB)+ libcustom_rules.so |
我之前有一篇杂谈,主要写了Dnspy逆向修改客户端逻辑,不过那个很局限,仅限单机并且需要没有IL2CPP加密。
废话不多说,直接开始逆向。
首先逆向一款游戏,从哪下手呢?又如何做出服务端呢?我一一说一下
1.先从客户端下手,因为是手机游戏我主要用ADB从手机上获取游戏的包体,当然要确认包名叫什么。
一般是叫com.xxxx.xxx这种格式,获取到包体,我是直接使用BandZip打开的
打开后出现很多文件夹,先去看assets文件夹下内容。
一眼看出是Unity游戏以及拥有IL2CPP加密,不过还有LUA32与LUA64,具体怎么校验的就不说了
最后发现客户端的代码基本上都在LUA32与LUA64文件夹内。
打开文件夹会发现一大堆后缀名为lua.bytes.unity3d的文件,很显然是被加密了啊...
那么就着手开始准备解密了 Unity这类一般都是XOR加密,包括早期的原神同样为XOR加密
经过一番逆向 得到魔数为1B 4C 4A 02
XOR key为(1B 4C 4A 02+"m71") = 76 7B 7B 6F
这个m71的key是从Assembly-CSharp.dll :: LuaEncryptio
里面的静态字段 KEY = "m71"中得来的 转换为十六进制。
最后解密后会得到8351个lua.luac
luac转成lua最好使用LuaJIT,用于把字节码反编译为lua源码
最后就可以得到近乎客户端的源码
到这一步后,客户端的源码已经被我们逆向出来了,客户端上很多的流程,隐藏的函数都可以被我们挖掘出来。
问题来了,我们怎么把我们改动的代码注入进去呢?即便是服务端权威也并不代表没有漏洞,我已实际测试出官方存在的一个越权漏洞问题。
好注入这方面要注意,游戏带网易的反作弊**HTProtect,**普通的注入方法肯定是不行的。
我采用了用Zygisk模块的方法注入,并且由此制作了一个插件平台
确定好注入方法后制作工具,并编译产物实际注入游戏,期间需要重启一次手机,让Zygisk模块生效。
Frida注入实测会被网易反作弊击杀 游戏静默,所以得换成Zygisk目前看来运行良好,一个月测试账号并未被封禁。
至于抓包我使用的是电脑,为了让手机上的流量数据经过电脑,我使用adb+iptables DNAT隧道让PC监听手机上这个游戏的数据,做好抓包。
抓包也不是一帆风顺,数据包都被加密了。
这就需要进行逆向工程中的协议解析了
传输这一层,客户端连接的8001端口对应auth 8601对应gserver 8603对应gs
我们有客户端的lua源码,这时候关于上行,我们就要去解密上行的加密了
根据数据包中的5a4d 0000000000000032 6372 这里的6372解出是cr为前缀
为AES-128-ECB 加密。
密钥公式也已在客户端源码中找到:
-- 位置:network_mgr.lua:1146-1156
function update_key(time, is_remote)
if ME_MGR.get_rid() then
set_key(server_name, tostring(time) .. string.sub(ME_MGR.get_rid(), 4, 9))
end
end
根据源码可以得出,上行不存在解不开的可能,上行包已经解密成功。接下来去找怎么解密服务端发来的数据包?也就是下行包。
最后发现下行居然是不加密的明文msgpak,这倒是便宜了我们。
有了上行下行的全部解密,我们就能解出服务端发给我们什么数据以及客户端发给服务端了什么数据。
这是私服开发中相当重要的部分,协议解析完成。
接下来看看,私服该如何去做?
首先握手与心跳不可或缺,先看看心跳是什么逻辑?
客户端 `managers/network_mgr.lua`:
function on_ws_message(ws, data)
if data == "ping" then reset_ping(ori_name); return
elseif data == "pong" then reset_pong(ori_name); return end
...
end
function send_ping(server_name, seq)
ws:Ping()
local ping_time = UPDATE.unscaledTime
timer.create_once_unscaled(PONG_TIMEOUT /* 7 秒 */, 0, function()
local pong_time = _wss_info[name].last_pong_time or 0
if pong_time < ping_time then
logger:warn("%s PONG超时,连接丢失", name)
safe_call_void(_wss_info[name].cb_disconnect, ws) -- ← 直接断开 → 重连
end
end)
end
这里客户端给服务端发ping
服务端给客户端发pong
能发能收,这就是心跳,和人的心跳一致,一旦中途断了,表现就是重连 很常见的与服务器断开连接提示就类似这种 你一断网 客户端一直发ping 可断网了ping根本就发不到服务端上 自然也就不会回复pong 所以就确定为断连了。
这个如果不做好 私服进去没多久 就一直断连 所以这一步是必须要做好的。
登录的握手:
LTSDK_MGR.get_user_id / get_lt_token ← 客户端自理,插不进去也不需要插
→ AUTH_MGR.get_game_code / get_channel_no
→ client_verify_lt_token (authserver :8001)
→ client_auth_account (authserver :8001)
→ request_role_list (gs :8603)
→ login (gs :8603)
→ user_init + 一长串初始化推送 (gs :8603)
→ enter_city (gs :8603)
→ gserver 101/2/612/… (:8601)
→ socialserver (:8604)
然后是选服与角色列表主要由 HTTP协议的list发放
现在握手 列表 心跳都成功解析了,那么就可以初步搭建服务端了。
我的想法是一个进程多开端口 同时实现list gserver gs authserver...这些
服务端决定使用Node编写 数据库使用sqlite
具体的代码这边也不会写,因为太多了。
服务端的开发主要使用GPT 6 Astar进行编写 效率很快,服务端架构建议与业务逻辑建议由ClaudeOpus5.5提供,并且服务端后台也已制作完成。
关于连接私服 我使用的是Tailscale搭建隧道让手机无线连接到私服的,至于客户端我使用Zygisk模块注入插件平台,让list获取等等请求指向到我的公网IP上。
好了,差不多可以收尾了。总而言之,想从0开始服务端到自己制作私服,大致的流程就是这样子。
1.对客户端进行逆向,最大程度的了解客户端
2.抓包解密上行与下行 做协议解析 最好解出明文
3.对登录页面进行抓包并分析 心跳 握手 登录 账号这类
4.编写服务端,并让手机上指向官方服务器的list改为指向私服
5.测试登录,最后就是完善了,补全业务逻辑,做好服务端后台
结束,这就是搭建一个私服给自己玩的方法,至于开放给其他人玩 还需要你有比较好的服务器。
这些都是我个人开发的一些经验 仅参考。






