前阵子无意中接触到ZMXS这款游戏,玩了两天后觉得挺有意思的,就想着能不能深入研究一下它的网络协议实现。这一折腾就是一个多星期,踩了不少坑,但收获也挺大的。整个分析过程涉及到移动游戏逆向的很多典型场景,记录下来跟大家分享一下经验。
拿到APK后第一件事就是引擎识别,这步真的很关键。做过几个游戏分析的都知道,不同引擎的逆向套路完全不一样,走错了方向就是浪费时间。
解压APK直接奔向lib目录,几个关键文件一下子就映入眼帘:

看到这个组合我心里就有数了。Unity作为渲染引擎负责底层,真正的游戏业务逻辑全在Lua脚本里。这种架构现在挺流行的,好处是热更新方便,但从逆向分析的角度来说,反而提供了更多的入口点。
既然确认是Unity引擎,那就直接上IL2CPPDumper了。这工具专门用来处理Unity的IL2CPP编译后的产物,能从native代码中还原出C#的类型信息和方法签名。
需要准备两个关键文件:

运行IL2CPPDumper后,还好这个游戏没有在IL2CPP层面做保护,顺利dump出了所有的类型信息。用dnSpy打开生成的DLL文件一看,果然跟我预想的一样:

Assembly-CSharp.dll里几乎找不到什么游戏逻辑代码,大部分都是Unity基础类库和一些框架代码。这进一步印证了我的判断——游戏的核心业务逻辑确实在Lua层实现。
不过这里还是有几个有价值的发现:
游戏集成了libmsaoaidsec.so这个反调试库,直接用Frida肯定会被检测到。这种保护挺常见的,主要检测这些:
碰到这种保护,一般有几种应对思路:
方案一:线程暂停
找到libmsaoaidsec.so创建的检测线程,直接暂停或kill掉。这种方法简单粗暴,但风险是可能影响游戏稳定性,有时候会莫名其妙崩溃。
方案二:函数patch
定位到具体的检测函数,用内存patch的方式将其NOP掉。这种方法效果最好,但工作量大,每个版本的libmsaoaidsec.so都需要重新分析。
方案三:去特征工具
使用修改过的Frida版本,比如Rusda,它移除了Frida的特征字符串,修改了默认端口等。
权衡了一下,我选择了Rusda这种方案。虽然可能随着保护升级会失效,但对付当前这个版本的libmsaoaidsec.so还是够用的,而且相对来说比较省事。
Unity+XLua架构中,所有基于Lua虚拟机的脚本加载都会经过luaL_loadbufferx这个函数。这是标准的Lua C API,对做过Lua逆向的人来说应该很熟悉:
通过Hook这个函数,理论上可以捕获到游戏动态加载的所有Lua脚本。写了个Frida脚本:
但很快我就发现了这种方法的局限性:只能获取游戏实际执行到的脚本。游戏采用按需加载策略,很多功能模块可能根本不会被触发,这样就无法获取完整的脚本库。
要彻底解决这个问题,必须找到Lua脚本在APK中的实际存储位置,把完整的脚本库搞出来。
现在的Unity游戏基本都用AssetBundle系统管理资源,这游戏也不例外。观察APK结构发现,assets/AssetBundles目录占据了绝大部分空间,显然游戏的核心资源都打包在这里。

对于做过Unity开发的人来说,AssetBundle并不陌生。它是Unity的模块化资源管理方案,有这些优势:
要找到Lua脚本的具体位置,关键是分析游戏的资源加载流程。从IL2CPP分析结果中,AssetBundleManager这个类很值得研究:

这个类中的LoadAssetAsync方法是关键入口点。通过IDA Pro进行静态分析,可以还原出资源加载的完整逻辑:

总结下来这个函数的作用就是
经过详细分析,我把资源加载流程总结为这几个步骤:
通过Hook LoadAssetAsync方法,我成功获得了资源名称与实际文件的映射关系:
看到luagame.assetpkg那一行,我心里一阵兴奋!这个文件应该就包含了游戏的完整Lua脚本库。对应的文件是cced8de1b361f40750fbbfcd0e046241.assetpkg。
直接用AssetStudio打开目标文件,结果失败了。用十六进制编辑器检查,发现内容已经被加密:

文件头部没有标准的Unity AssetBundle签名,数据分布看起来完全是随机的,说明采用了某种加密算法。这下麻烦了,得想办法把加密给破了。
继续分析AssetBundleManager类,终于找到了关键的解密方法。通过IDA的交叉引用功能,追踪到了Decryption函数:

深入分析解密逻辑后,发现用的居然是最简单的XOR异或加密:

XOR加密的特点是简单高效,加密和解密使用相同的算法:encrypted_data[i] = original_data[i] ^ key[i % key_length]
现在最关键的问题是:密钥到底是什么?
通过向上追踪函数调用链,在更高层的调用中我找到了答案:
看到这里我都笑了,密钥竟然就是AssetBundle的文件名。这种设计虽然简单,但对于防止普通用户随意修改资源确实有一定效果。
有了密钥,写解密脚本就很简单了:
解密成功后,文件显示为标准的UnityFS格式:

用AssetStudio一解析,整个Lua脚本库都出现在眼前:

看到这个结果,我心里那个激动啊!经过这么多轮的分析和破解,终于拿到了游戏的完整源码。
有了完整的Lua源码,协议分析就变得清晰多了。从代码目录结构可以看出,网络相关的代码主要集中在几个关键文件里:
先看看客户端协议ID定义(net/ccmd.lua):
再看看服务端协议ID定义(net/scmd.lua):
然后是Protobuf序列化处理(net/ProtobufParser.lua):
还有网络通信管理器(net/LunJianSocketManager.lua),这个文件里有个有意思的发现:
看到那个defaultEncryptKey = "spqh4hpstria0q9h",我差点笑出声。这就是网络协议的AES加密密钥!直接硬编码在脚本里,简单粗暴。
仔细分析了LunJianSocketManager.lua中的发送和接收逻辑,终于搞清楚了游戏网络协议的完整格式。
发送逻辑是这样的:
接收逻辑稍有不同:
通过源码分析,游戏协议格式的关键特征总结如下:
客户端发送格式:
服务端响应格式:
这里有几个有意思的设计差异:
游戏使用lua-protobuf库处理消息序列化,协议定义的加载方式是这样的:
这里有个问题:lua-protobuf使用的是预编译的.pb二进制文件,而不是可读的.proto源文件。要想还原出完整的协议定义,需要想办法从二进制格式逆向出可读的文本格式。
好在lua-protobuf提供了强大的反射机制,可以在运行时查询内存中的协议定义信息。
利用这些反射接口,可以写个协议信息提取器:
通过这个提取器,拿到了详细的协议信息JSON数据。然后又写了Python脚本将其重建为标准的.proto文件:
经过这一番折腾,终于成功重建出了完整的.proto文件,包括协议ID枚举:
现在万事俱备,该验证一下分析结果的正确性了。拿个实际抓到的数据包来测试:
按照分析的协议格式,写了个完整的解析程序:
运行结果如下:
看到这个结果,心里别提多高兴了。从APK分析到协议还原,整个流程走通了,数据包解析完全正确!
因为东西太杂和样本太大,全部上传到github,有兴趣的可以到github上查看
323K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6%4N6h3S2#2j5h3N6#2L8K6R3^5z5q4)9J5c8Y4A6E0P5s2x3`.
搞了一个多星期,总算把ZMXS这个游戏的协议给摸透了。回头想想这个过程,还是挺有意思的,踩了不少坑,但也学到了不少东西。
这次分析让我对现在手游的技术选择有了更清楚的认识。基本上主流游戏都是Unity做底子,然后业务逻辑全扔到Lua或者JS里去实现。这样搞确实有它的道理:
首先开发效率高得多,脚本语言写起来快,改起来也方便,不像C++那样编译半天。然后就是热更新这个杀手锏,服务端随时推个脚本更新,客户端马上就能用上新功能,根本不用重新发包。还有就是一套脚本能跑各个平台,省了不少适配的功夫。
但这样做也有代价。脚本这玩意儿相对来说比较好逆向,像这次我基本上把整个游戏逻辑都扒出来了。要是游戏公司把一些关键的数值计算或者反作弊逻辑放在客户端脚本里,那就给外挂开发者提供了很大便利。
这个游戏的AssetBundle保护说实话挺一般的。就是简单的XOR异或,密钥还直接用文件名,这种保护强度对新手可能有点用,但对稍微有点经验的人来说基本没啥阻止作用。
要是我来做的话,至少得换成AES-256这种强一点的算法,密钥也不能这么简单粗暴。最好是搞个复杂点的密钥推导过程,再加上完整性校验,防止别人篡改资源文件。当然最根本的还是把重要资源放服务端,需要的时候动态下发,这样就算客户端被破解了也影响不大。
分析这个游戏的协议时发现了几个挺有意思的设计:
首先是加密策略不对称,客户端发送的数据可能会AES加密,但服务端返回的数据是明文。开始我还纳闷为啥这样设计,后来想想可能是性能考虑。服务端资源充足,解密不是问题,但客户端特别是低端设备,能省点计算就省点。
然后是选择性加密,不是所有协议都加密,而是通过一个EncryptFilter来判断哪些协议需要保护。这个设计挺实用的,敏感操作加密保护,普通操作明文传输,在安全性和性能之间找了个平衡点。
还有就是Protobuf + AES的组合使用。先序列化再加密,这个顺序是对的。Protobuf负责高效的数据序列化,AES负责数据保护,各司其职。
这次分析下来,我觉得移动游戏的逆向大概有这么几个步骤:
先是引擎识别,这个很关键。看lib目录下的so文件基本就能判断出用的什么技术栈。确定了引擎就知道该用什么工具,走什么路线。
然后是代码层面的分析。Unity的话就用IL2CPPDumper,其他引擎有其他对应的工具。这一步主要是理解代码结构,找出关键的类和方法。
如果发现是脚本化架构,那重点就转到脚本提取上。Hook脚本加载函数是一种方法,但更彻底的还是找到脚本的存储位置,把完整的脚本库搞出来。
资源分析也很重要,特别是对于使用AssetBundle的游戏。搞清楚资源的加载流程,找到加密解密的关键点,这样就能把所有资源都搞到手。
协议逆向最好是基于源码分析,有了完整的脚本或者反编译代码,协议格式和加密机制基本上就一目了然了。
最后就是实战验证,拿真实的数据包来测试解析结果,确保分析的正确性。
整个过程下来,我觉得最关键的还是要结合静态分析和动态调试。光看代码不行,光Hook也不行,得两者结合才能突破各种保护。
通过这次分析,我对Unity+Lua这种架构有了更深的理解。这种模式在手游行业确实很流行,开发效率高,热更新方便。但从安全角度来说,也确实存在一些问题。
不过话说回来,安全和效率本身就是矛盾的。游戏公司肯定是要在开发成本、运营成本和安全性之间找平衡。对于大部分休闲游戏来说,现在这种保护强度可能就够了。真正核心的数值和逻辑还是放在服务端比较安全。
随着技术的发展,保护和破解之间的对抗肯定还会继续下去。新的保护技术出来,新的破解方法也会跟上。这就是技术圈的魅力所在吧,永远有新的挑战等着你去解决。
完整代码查看: fd4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6%4N6h3S2#2j5h3N6#2L8K6R3^5z5q4)9J5c8Y4A6E0P5s2x3`.
int luaL_loadbufferx (
lua_State *L, // Lua虚拟机状态
const char *buff, // 脚本内容指针
size_t sz, // 脚本内容大小
const char *name, // 脚本文件名
const char *mode // 加载模式(text/binary/both)
);
int luaL_loadbufferx (
lua_State *L, // Lua虚拟机状态
const char *buff, // 脚本内容指针
size_t sz, // 脚本内容大小
const char *name, // 脚本文件名
const char *mode // 加载模式(text/binary/both)
);
// Lua脚本动态捕获
function ensureDirectoryExists(filePath) {
const pathComponents = filePath.split('/').slice(0, -1);
let currentPath = '';
for (const component of pathComponents) {
currentPath += component + '/';
try {
const file = new File(currentPath, "r");
if (!file.exists()) {
file.close();
// 创建目录
const dir = new File(currentPath, "w");
dir.close();
} else {
file.close();
}
} catch(e) {
console.log("Directory creation error: " + e);
}
}
}
Interceptor.attach(Module.findExportByName('libxlua.so', "luaL_loadbufferx"), {
onEnter: function (args) {
const scriptName = args[3].readUtf8String();
const contentSize = args[2].toInt32();
const contentPtr = args[1];
// 只处理.lua文件
if (!scriptName || !scriptName.endsWith(".lua")) {
return;
}
const outputPath = "/sdcard/lua_analysis/" + scriptName;
ensureDirectoryExists(outputPath);
try {
const fileHandle = new File(outputPath, "wb");
const scriptContent = contentPtr.readByteArray(contentSize);
if (scriptContent) {
fileHandle.write(scriptContent);
fileHandle.flush();
fileHandle.close();
console.log(`[Lua Capture] ${scriptName} (${contentSize} bytes)`);
}
} catch(e) {
console.log(`[Error] Failed to save ${scriptName}: ${e}`);
}
}
});
// Lua脚本动态捕获
function ensureDirectoryExists(filePath) {
const pathComponents = filePath.split('/').slice(0, -1);
let currentPath = '';
for (const component of pathComponents) {
currentPath += component + '/';
try {
const file = new File(currentPath, "r");
if (!file.exists()) {
file.close();
// 创建目录
const dir = new File(currentPath, "w");
dir.close();
} else {
file.close();
}
} catch(e) {
console.log("Directory creation error: " + e);
}
}
}
Interceptor.attach(Module.findExportByName('libxlua.so', "luaL_loadbufferx"), {
onEnter: function (args) {
const scriptName = args[3].readUtf8String();
const contentSize = args[2].toInt32();
const contentPtr = args[1];
// 只处理.lua文件
if (!scriptName || !scriptName.endsWith(".lua")) {
return;
}
const outputPath = "/sdcard/lua_analysis/" + scriptName;
ensureDirectoryExists(outputPath);
try {
const fileHandle = new File(outputPath, "wb");
const scriptContent = contentPtr.readByteArray(contentSize);
if (scriptContent) {
fileHandle.write(scriptContent);
fileHandle.flush();
fileHandle.close();
console.log(`[Lua Capture] ${scriptName} (${contentSize} bytes)`);
}
} catch(e) {
console.log(`[Error] Failed to save ${scriptName}: ${e}`);
}
}
});
Game.assetbundle -> 260051b7bf2afd4070031708b056f55d.assetbundle
gameassetsmap_bytes.assetbundle -> d1bd3d43c8bcb6c1cdf5a5dd34f9046e.assetbundle
gamedependencies_bytes.assetbundle -> 7a0c77f31bfe78603126a70cb97df4ae.assetbundle
luagame.assetpkg -> cced8de1b361f40750fbbfcd0e046241.assetpkg
pb.assetbundle -> 09e9b1870b0bec20b4e772eaecdb8831.assetbundle
Game.assetbundle -> 260051b7bf2afd4070031708b056f55d.assetbundle
gameassetsmap_bytes.assetbundle -> d1bd3d43c8bcb6c1cdf5a5dd34f9046e.assetbundle
gamedependencies_bytes.assetbundle -> 7a0c77f31bfe78603126a70cb97df4ae.assetbundle
luagame.assetpkg -> cced8de1b361f40750fbbfcd0e046241.assetpkg
pb.assetbundle -> 09e9b1870b0bec20b4e772eaecdb8831.assetbundle
if (webRequester->isEncryptionEnabled) {
byte[] encryptedData = webRequester.GetBytes();
byte[] decryptedData = AssetBundleManager.Decryption(
encryptedData,
webRequester.assetBundleName
);
AssetBundle bundle = AssetBundle.LoadFromMemory(decryptedData);
}
if (webRequester->isEncryptionEnabled) {
byte[] encryptedData = webRequester.GetBytes();
byte[] decryptedData = AssetBundleManager.Decryption(
encryptedData,
webRequester.assetBundleName
);
AssetBundle bundle = AssetBundle.LoadFromMemory(decryptedData);
}
def decrypt_assetbundle_xor(encrypted_data: bytes, key_string: str) -> bytes:
key_bytes = key_string.encode('utf-8')
key_length = len(key_bytes)
decrypted_data = bytearray()
for i, encrypted_byte in enumerate(encrypted_data):
key_byte = key_bytes[i % key_length]
decrypted_byte = encrypted_byte ^ key_byte
decrypted_data.append(decrypted_byte)
return bytes(decrypted_data)
with open('cced8de1b361f40750fbbfcd0e046241.assetpkg', 'rb') as f:
encrypted_content = f.read()
decrypted_content = decrypt_assetbundle_xor(encrypted_content, 'luagame.assetpkg')
if decrypted_content.startswith(b'UnityFS'):
print("解密成功!文件格式:UnityFS")
with open('luagame_decrypted.assetbundle', 'wb') as f:
f.write(decrypted_content)
else:
print("解密失败,请检查密钥")
def decrypt_assetbundle_xor(encrypted_data: bytes, key_string: str) -> bytes:
key_bytes = key_string.encode('utf-8')
key_length = len(key_bytes)
decrypted_data = bytearray()
for i, encrypted_byte in enumerate(encrypted_data):
key_byte = key_bytes[i % key_length]
decrypted_byte = encrypted_byte ^ key_byte
decrypted_data.append(decrypted_byte)
return bytes(decrypted_data)
with open('cced8de1b361f40750fbbfcd0e046241.assetpkg', 'rb') as f:
encrypted_content = f.read()
decrypted_content = decrypt_assetbundle_xor(encrypted_content, 'luagame.assetpkg')
if decrypted_content.startswith(b'UnityFS'):
print("解密成功!文件格式:UnityFS")
with open('luagame_decrypted.assetbundle', 'wb') as f:
f.write(decrypted_content)
else:
print("解密失败,请检查密钥")
local pb = require("pb")
local function enum(id)
return pb.enum("com.yofijoy.core.proto.CSProtoId", id)
end
local CCmd = {}
CCmd.HEARTBEAT = enum("CS_HEART_REQ") --心跳
-- 登录相关
CCmd.USER_LOGIN = enum("CS_USER_LOGIN_REQ") -- 用户登录
CCmd.CREATE_ROLE = enum("CS_CREATE_ROLE_REQ") -- 创建角色
CCmd.ROLE_LOGIN = enum("CS_ROLE_LOGIN_REQ") -- 角色登录
CCmd.ROLE_OPERATION = enum("CS_ROLE_OPERATION_REQ") -- 操作角色
CCmd.USER_LOGIN_ACTIVATE_CODE_RPT = enum("CS_USER_LOGIN_ACTIVATE_CODE_RPT")-- 使用激活码登录
local pb = require("pb")
local function enum(id)
return pb.enum("com.yofijoy.core.proto.CSProtoId", id)
end
local CCmd = {}
CCmd.HEARTBEAT = enum("CS_HEART_REQ") --心跳
-- 登录相关
CCmd.USER_LOGIN = enum("CS_USER_LOGIN_REQ") -- 用户登录
CCmd.CREATE_ROLE = enum("CS_CREATE_ROLE_REQ") -- 创建角色
CCmd.ROLE_LOGIN = enum("CS_ROLE_LOGIN_REQ") -- 角色登录
CCmd.ROLE_OPERATION = enum("CS_ROLE_OPERATION_REQ") -- 操作角色
CCmd.USER_LOGIN_ACTIVATE_CODE_RPT = enum("CS_USER_LOGIN_ACTIVATE_CODE_RPT")-- 使用激活码登录
local pb = require("pb")
local function enum(id)
return pb.enum("com.yofijoy.core.proto.CSProtoId", id)
end
local SCmd = {}
SCmd.HEARTBEAT = enum("CS_HEART_RESP") --心跳返回
SCmd.USER_LOGIN = enum("CS_USER_LOGIN_RESP") -- 用户登录返回
SCmd.CREATE_ROLE = enum("CS_CREATE_ROLE_RESP") -- 创建角色返回
SCmd.ROLE_LOGIN = enum("CS_ROLE_LOGIN_RESP") -- 角色登录
local pb = require("pb")
local function enum(id)
return pb.enum("com.yofijoy.core.proto.CSProtoId", id)
end
local SCmd = {}
SCmd.HEARTBEAT = enum("CS_HEART_RESP") --心跳返回
SCmd.USER_LOGIN = enum("CS_USER_LOGIN_RESP") -- 用户登录返回
SCmd.CREATE_ROLE = enum("CS_CREATE_ROLE_RESP") -- 创建角色返回
SCmd.ROLE_LOGIN = enum("CS_ROLE_LOGIN_RESP") -- 角色登录
local current = select(1, ...)
local EncryptFilter = import(".EncryptFilter")
local pb = require "pb"
local ProtobufParser = {}
local PbBytes = {
"Pb/base.bytes",
"Pb/CSProto.bytes",
}
function ProtobufParser:coInit()
-- 协程请求协议二进制文件
for i, byteFile in ipairs(PbBytes) do
local asset = ResourcesManager:coLoadAsync(byteFile, typeof(UE.TextAsset))
if not asset then
assert(false, "protobuf 资源异步加载失败")
break
end
local ret = pb.load(asset.bytes)
if ret == false then
assert(false, "protobuf 二进制数据加载失败")
break
end
-- print("加载", byteFile, "成功")
end
local current = select(1, ...)
local EncryptFilter = import(".EncryptFilter")
local pb = require "pb"
local ProtobufParser = {}
local PbBytes = {
"Pb/base.bytes",
"Pb/CSProto.bytes",
}
function ProtobufParser:coInit()
-- 协程请求协议二进制文件
for i, byteFile in ipairs(PbBytes) do
local asset = ResourcesManager:coLoadAsync(byteFile, typeof(UE.TextAsset))
if not asset then
assert(false, "protobuf 资源异步加载失败")
break
end
local ret = pb.load(asset.bytes)
if ret == false then
assert(false, "protobuf 二进制数据加载失败")
break
end
-- print("加载", byteFile, "成功")
end
local HeartbeatInterval = 5 --心跳间隔, 暂时心跳包间隔不设置过快
local ReconnectTimes = 5 --重连次数
local ReconnectInterval = 10 -- 重连间隔时间
local MinRespondTime = 5 --客户端发心跳包后服务器响应时间过长, 超时时间
local RecvInterval = HeartbeatInterval --检测收包频率
LunJianSocketManager.SendOverTime = HeartbeatInterval * 2 --发包异常超时时间
LunJianSocketManager.RecvOverTime = HeartbeatInterval * 2 + MinRespondTime -- 收包异常超时时间
local defaultEncryptKey = "spqh4hpstria0q9h" -- 这个很关键!
LunJianSocketManager.encryptKey = defaultEncryptKey
local SocketError = {
NORMAL = 0, --正常关闭,在连接前也会关一下
ERROR_1 = -1, --C
ERROR_2 = -2, --C
ERROR_3 = -3, --C
ERROR_4 = -4, --C
ERROR_5 = -5, --主动断开连接, (重线重连, 非主动断开.忽略该信号)
ERROR_6 = -6, --主动连接超时
}
local HeartbeatInterval = 5 --心跳间隔, 暂时心跳包间隔不设置过快
local ReconnectTimes = 5 --重连次数
local ReconnectInterval = 10 -- 重连间隔时间
local MinRespondTime = 5 --客户端发心跳包后服务器响应时间过长, 超时时间
local RecvInterval = HeartbeatInterval --检测收包频率
LunJianSocketManager.SendOverTime = HeartbeatInterval * 2 --发包异常超时时间
LunJianSocketManager.RecvOverTime = HeartbeatInterval * 2 + MinRespondTime -- 收包异常超时时间
local defaultEncryptKey = "spqh4hpstria0q9h" -- 这个很关键!
LunJianSocketManager.encryptKey = defaultEncryptKey
local SocketError = {
NORMAL = 0, --正常关闭,在连接前也会关一下
ERROR_1 = -1, --C
ERROR_2 = -2, --C
ERROR_3 = -3, --C
ERROR_4 = -4, --C
ERROR_5 = -5, --主动断开连接, (重线重连, 非主动断开.忽略该信号)
ERROR_6 = -6, --主动连接超时
}
function LunJianSocketManager:send(msgCmd, msgData)
local protoData = nil
local msgLen = PackSendHeaderLength -- 发送包头长度常量
-- Step 1: Protobuf序列化
if msgData then
protoData = ProtobufParser:encode(msgCmd, msgData)
if not protoData then
LogError("Protobuf编码失败: " .. tostring(msgCmd))
return false
end
end
-- Step 2: 条件加密处理
if protoData then
-- 检查协议是否需要加密
if EncryptFilter:needEncrypt(msgCmd) then
protoData = Crypto.AesEncryptECB(protoData, self.encryptKey)
if not protoData then
LogError("AES加密失败: " .. tostring(msgCmd))
return false
end
end
msgLen = msgLen +
end
-- Step 3: 构建网络数据包
self.netData:reset()
self.netData:writeUShort(msgLen) -- 包总长度 (2字节)
self.netData:writeUShort(msgCmd) -- 协议ID (2字节)
if protoData then
self.netData:writeBuffer(protoData) -- 消息体数据
end
-- Step 4: 网络发送
return self:sendRawPacket(self.netData:getBuffer())
end
function LunJianSocketManager:send(msgCmd, msgData)
local protoData = nil
local msgLen = PackSendHeaderLength -- 发送包头长度常量
-- Step 1: Protobuf序列化
if msgData then
protoData = ProtobufParser:encode(msgCmd, msgData)
if not protoData then
LogError("Protobuf编码失败: " .. tostring(msgCmd))
return false
end
end
-- Step 2: 条件加密处理
if protoData then
-- 检查协议是否需要加密
if EncryptFilter:needEncrypt(msgCmd) then
protoData = Crypto.AesEncryptECB(protoData, self.encryptKey)
if not protoData then
LogError("AES加密失败: " .. tostring(msgCmd))
return false
end
end
msgLen = msgLen +
end
-- Step 3: 构建网络数据包
self.netData:reset()
self.netData:writeUShort(msgLen) -- 包总长度 (2字节)
self.netData:writeUShort(msgCmd) -- 协议ID (2字节)
if protoData then
self.netData:writeBuffer(protoData) -- 消息体数据
end
-- Step 4: 网络发送
return self:sendRawPacket(self.netData:getBuffer())
end
function LunJianSocketManager:onProcessMsg(rawBytes)
self.netData:setBuffer(rawBytes)
-- 解析包头信息
local totalLength = self.netData:readInt() -- 包总长度 (4字节)
local protocolId = self.netData:readUShort() -- 协议ID (2字节)
local dataLength = totalLength - PackRecvHeaderLength
-- 读取消息体
local protobufData = self.netData:readProtocolBuffer()
if string.len(protobufData) ~= dataLength then
LogError("协议数据长度不匹配: expected=" .. dataLength .. ", actual=" .. string.len(protobufData))
return false
end
-- Protobuf反序列化
local messageData = ProtobufParser:decode(protocolId, protobufData)
if not messageData then
LogError("Protobuf解码失败: " .. tostring(protocolId))
return false
end
-- 消息分发处理
self:dispatchMessage(protocolId, messageData)
return true
end
function LunJianSocketManager:onProcessMsg(rawBytes)
self.netData:setBuffer(rawBytes)
-- 解析包头信息
local totalLength = self.netData:readInt() -- 包总长度 (4字节)
local protocolId = self.netData:readUShort() -- 协议ID (2字节)
local dataLength = totalLength - PackRecvHeaderLength
-- 读取消息体
local protobufData = self.netData:readProtocolBuffer()
if string.len(protobufData) ~= dataLength then
LogError("协议数据长度不匹配: expected=" .. dataLength .. ", actual=" .. string.len(protobufData))
return false
end
-- Protobuf反序列化
local messageData = ProtobufParser:decode(protocolId, protobufData)
if not messageData then
LogError("Protobuf解码失败: " .. tostring(protocolId))
return false
end
-- 消息分发处理
self:dispatchMessage(protocolId, messageData)
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 2025-9-2 22:16
被wuhuaguo888编辑
,原因: