第一层解包:祖龙工作室私有格式
这个格式从完美世界一直沿用到龙族以及以闪亮之名
标志性特征有这些:
- 超大的PNG文件,在StreamingAssets文件夹下面
- PNG的文件头是`EF 23 CA 4D` (EF23CA4D) ,小端序是 4DCA23EF
- 在二进制中会存在
AFilePackMan::OpenFilePackage
字符串
- 会有
Azure File Package, Loong Co. Ltd. All Rights Reserved.
字符串
通过巧妙搜索,找到了国外大佬前辈的解包工具,测了一下直接就能用
第二层解包1:LuaC反编译
Lua文件夹内的文件头是LuaQ,所以是Lua5.1的文件。
直接用unluac批量跑一遍就完事了,虽然用了LuaC,但是调试符号都在,所以没有太大影响,只有一两个文件反编译失败
编写更新监测
逆向游戏初始化协议
- 抓包可以很清晰看到有游戏连接了tc-projecti-zdir.zulong.com:52002,链接之后,游戏服务器会直接发送几KB的信息
- 在StreamingAssets\config下可以很轻松找到这个入口服务器的地址
- 所以我们知道入口服务器在祖龙那边叫“dir server”,那我们在lua里面搜一下
- 能找到update.lua,里面有大量相关逻辑
- 梳理控制流后可以看到里面主要就是调用request_dir函数,然后进一步调用GameUtil.RequestDirInfo,这是个实现在Native层的函数
- 搜索字符串可以很轻易在native层找到对应函数,继续在native层逆向
- 所有逻辑都用的CPP编写,符号比较少,字符串一般,不多也不少
- 没有什么投机取巧的办法,就硬着头皮逆
- 一个个重建Cpp类:标准库、自定义Buffer、网络manager、stream抽象,等等
- 一点一点梳理逻辑:连接、发送buffer、接收buffer、解析buffer
- 我使用了GPT来辅助逆向
- 通信协议用了变长编码,看了许久也看不出来,GPT一下就说出来变长的思路:基于前三个bit来判断是否整数的长度
- 代码里混杂了很多UTF16和UTF8导致需要很多互转。UTF8的解析我确实不懂,但GPT一下就看出来了
- 中间出了一些问题,怎么逆都逆不出来
- 我误以为有通信的加密密钥(isec/iseckey/osec/oseckey),然后找了许久也没有找到设置的地方
- 这些参数在当年完美世界私服的时候是有用的,但是在这里所有的都是空
- 我使用了Debugger打断点看了逻辑才知道并没有加密,而只是压缩了一下
- 原因是我一直在连接处理的地方转悠,而游戏在最后输出给Lua的时候才解压,解压函数在这个函数的第三层(
- 失误的另一个原因则是我不熟悉zlib的header,导致没有把压缩编码认出来。在这次中我碰到的header是
0x78 0x9c
,似乎0x9c可以变,但0x78往往就是zlib编码(长记性了
- 知道了这些之后,就可以编写协议脚本了
自动更新游戏版本
- 使用GitHub Actions来自动每隔一段时间来跑一次,这样就不用服务器了
第二层解包2:解析BCFG
- 可能是为了降低Lua object的内存占用,他用了一个奇怪的编码方式编码了大lua table
- 祖龙工作室管这个叫“convex”,网络上查了一下,并没有太多资料
- 只能自己逆了!
- convex的代码里面带着很多调试字符串,所以逻辑还算清晰
- 可以很快逆向出来Header、Definition、Row等结构
- 然而,重组这些结构则很麻烦,因为table涉及到数组、字典等等不同类型,所以解析做的很复杂
- 为了方便调试,我编写了010editor的模板方便我看看我解析的思路是否正确
- 首先我们需要搞清楚bcfg整体的文件结构
| BCFGHeader | RawData | BCFGDefinition | BCFGRows |
- 然后,我们就得搞定RawData的格式
- definition里面有好几个格式,id分别为0、0x1A0、0x2A0,由于1A0 2A0的结构都很小,所以我们从比较完备的0号结构入手
- Cpp代码那边用了很多UData(lua userdata)还有std::map来存储数据,这导致很多数据流不好分析
- map用的是std::tree,所以就很不好逆
- 一开始想着急于求成,希望能避开udata还有std::map的逻辑,但是后来发现这样是不行的,因为几乎所有逻辑都和他俩有关
- 要静下心来好好恢复结构体,这样数据流就清晰了
- 仔细推敲二进制中出现的字符串:
- loadBcfg之后:
get ctable[%d]
- 然后调用了
row %d(%p) set metatable ok
- 在这个函数里初始化了
__index
的binding函数: rowUData index arg1 isn't userdata
get %s from type(0x%x) will get a 0x%0x
create lua table for list failed
- 可以看出这个函数就是核心的逻辑处理函数
- 恢复了数据流之后,RawData的结构就呼之欲出了
- 处理List与Table
- 由于源代码过于杂乱,所以我选择投机取巧一下,猜一下List的存储格式
- 根据代码可以看到List就是0xXAF,而Bean(Table)也就是0xXA0~0xXAE
- 根据010editor猜一下格式,一开始我猜测是直接一个挨着一个存,能parse好几组数据,但是后面就出错了
- 试了好几种格式都不对,最后只好回到源代码里面好好看,发现他其实是存了list的大小,然后list的元素如果是bean的话,则每个元素也存放了bean的大小
- 这样就对了
- int16的解析
- 解析完了之后发现很多数值都被x2了
- 仔细看了代码,发现他是把最后一位用于当做符号位了,所以我们修了一下,又好了
计算得分逻辑
原始数据分析笔记- 通过搜索衣服的名字可以很轻松找到fashion_essence.bcfg,里面的结构也很简单
- 然后我们就需要确定各个attribute以及tag的对应关系,tag的对应关系很好找,但是attribute代码里似乎没有这种对应关系(猜测是通过UI直接赋值的)
- 一开始以为是这一组对应关系,但是发现数值对不上(最大的属性不对)
- 然后我意识到还有另一组enum
- 所以说上一组其实是错的,并不是用来对应这些attribute的
- 手动通过比对分值确定了对应关系
- 但是我发现不管怎么计算,游戏里面的分数和衣服的分数都对不上
- 游戏的衣服分数完全由服务器计算得出,客户端发送
gp_stage_combat_start
,服务端则返回gp_stage_combat_start_re
gp_stage_combat_start
中包含了关卡id、选择的衣服、选择的羁绊- 我尝试直接发送api来排除羁绊的影响,但是只要少发东西,服务器就报错不返回
- 所以,首先我想的是排除羁绊卡牌的加成
- 所以我选了一张最菜的R卡,然后看了下他的数据
- 数据显示所有的同系卡牌(R/SR/SSR)他的数值都是一样的
- 卡牌的加成在ECCard:GetAttriBonus方法中
- 我第一个找到的算分代码在
ECFashionCombatMan.CalcTotalCombatScore
里面,这套算分的代码是用于处理bClientCombat情况的(但绝大部分关卡都是联网对战) - 卡牌的计算在这里,可以看到是乘以对应的卡组属性加成
- 卡组属性加成则在上面的ConvertCurCardsToSeasonCardData计算
- card:GetAttriBonus里面则是把卡组的原始数据乘以scale factor(固定为1.0E-4,也就是除以1000)
- 那么计算卡组在性感(attr[2][1])这个属性上的加成是(1 + 15 / 1000) = 1.0015
- 随后我注意到游戏里面在使用Fashion的时候有两套分数:
- FashionDetail和FashionInfo究竟有什么区别
- 经过lua测试,发现FashionInfo就是原始数据,返回7420
- 而FashionDetail返回则是8162
- 后面,我注意到7420 * 1.10 = 8162,然后我注意到斑斓度的加成
- 所以,FashionDetail其实就是斑斓度的加成后数值
- 然后,我发现了第二套算分代码,是用来在预选卡牌提示的时候算分的:
ECFashionCombatMan.CalcFashionsCombatInfo
- 整体的计算逻辑类似,还是weight * score,重点的区别是这里多了一个factor(由CalcPlayerFactor算出)
- 在CalcPlayerFactor中算的是TotalBonus还有LevelBonus
- 第一个函数GetTotalBonusByAttriIncludeFaction里面是算了两部分Factor,练习的factor和从m_AttriData直接读出来的factor
- 后续看了下m_AttriData是直接从服务器同步过来的,初步看了下和卡牌、套装之类的收集度有关
- 我直接用lua把这个函数的值读出来
- Level bonus后续看了没有似乎没有用上,可能是需要组合套装之后才可以
- 这样计算就对了
- 最终公式:基础分数 * 斑斓度加成 * 玩家属性加成(服务器算,可能跟玩家等级有关) * 协会风格指数加成 * 羁绊分数加成(1+卡牌分数/10000) + 卡牌技能加成
- 样例:
- 璞月之章,清纯分数7420
- 斑斓度16,加成10% = 8162
- 玩家属性加成1.00475 (GetTotalBonusByAttriIncludeFaction(2*2+1-1) = 0.475 / 100 +1)
- 协会风格指数加成(我没有): 0
- 羁绊卡组加成:1.0015 (15 / 10000 + 1)
- 关卡属性(1-2),attribute_value2=6000,倍率6000/1000 = 6
- 7420 * 1.1 * 1.00475 * 1.0015 * 6 = 49278.4 (下取整)
- 与服务器分数一致
- 后续逆向发现玩家的属性加成其实应该就是这些
- 因为代码里是这样写的
15680 * 1.00465 * 3 = 47258.736 (origin score) 15680 * 1.0 * 1.00465 * 1.0031 * 3 = 52145 * 1.9 = 89790 banlan wanjia kazu beilv s print(_G.g_CardMan.m_AllCards[107]:GetAttriBonus(1, 1)) s print(g_AttriBonusData:GetLevelBonusByAttri(1, 2)) s print(g_AttriBonusData:GetTotalBonusByAttriIncludeFaction(2))