6月3日逃跑吧少年PC端游戏更新。让我康康官方又出了什么新东西。
AssetStudio,启动!
咦~别的类型的文件还好,怎么TextAsset文本文件只有一个字母?难道是我的AssetStudio坏了?先Export出来康康。
好家伙乱码了。要是前几个版本,数据一般是保存为csv格式的文本文件。难道白日梦为了不让玩家拆包查看更新信息给自己数据加密了?但不对啊,里面还是有不少可读字符的,猜测编码有问题,先换UTF-8编码打开试试。
出现正常文字了,编码选择没错。但是可读字符串之间还混进不少不可读的二进制数据,看来应该是将数据序列化后保存的结果。
文件中似乎不存在变量名、类型等信息,应该不会像php那样不需要构造数据类型就能直接反序列化。还是逆向分析一下吧。
本人逆向还未入门,不过只是获取反序列化所需的对象定义和序列化的方法应该就能完成反序列化,还是硬着头皮上吧。
Il2CppDumper,启动!DnSpy,启动!
先直接搜索文件名试试:
这个ZeroFormatter命名空间有点可疑,点开看看。
Aha,重写了Serialize和Deserialize方法,看来这个ZeroFormatter应该就是序列化器了。搜了一下,这似乎是C#中最快的序列化器,看来这次游戏改格式更多是出于性能的考虑吧。
再往下看,这个ActivityLobbyObjectSegment类的属性,基本对应此前版本ActivityLobby文件的表头信息:
进而猜测ActivityLobby文件中存储的就是这个类的实例序列化的内容。接下来的任务就是对数据进行反序列化了。
因为我比较菜游戏会校验文件hash值,修改或者注入会比较麻烦,所以我选择写一个简单的反序列化程序,负责读取序列化后的数据并转换为可读的数据。
VS2019,启动!创建一个.Net控制台应用,通过NuGet安装ZeroFormatter和ZeroFormatter.Interfaces。
ZeroFormatter的基本使用不难,序列化的方法很简单,就是创建类、创建实例对象、序列化对象三步。进行序列化类的定义官网也给出了明确的规定,主要就是类需要指定ZeroFormattable,属性需要指定Index并且要加上virtual关键字。
先根据ActivityLobbyInfoObjectSegment直接创建一个ActivityLobbyInfo类:
[ZeroFormattable]
public class ActivityLobbyInfo
{
[Index(0)]
public virtual int Id {
get; set; }
[Index(1)]
public virtual string TabName {
get; set; }
[Index(2)]
public virtual ActivityType Type {
get; set; }
[Index(3)]
public virtual string Prefab {
get; set; }
[Index(4)]
public virtual string TitleName {
get; set; }
[Index(5)]
public virtual int Rank {
get; set; }
[Index(6)]
public virtual string Desc {
get; set; }
[Index(7)]
public virtual ActivityCollectionType CollectionType {
get; set; }
[Index(8)]
public virtual int[] PreviewActivityIDs {
get; set; }
[Index(9)]
public virtual string TimeDesc {
get; set; }
[Index(10)]
public virtual int version {
get; set; }
[Index(11)]
public virtual int gradeLimit {
get; set; }
[Index(12)]
public virtual int Index {
get; set; }
[Index(13)]
public virtual int startTime {
get; set; }
[Index(14)]
public virtual int endTime {
get; set; }
[Index(15)]
public virtual int exchangeEndTime {
get; set; }
[Index(16)]
public virtual int MobileTimeOffset {
get; set; }
}
其中部分枚举类型的数据也要根据逆向的结果进行定义:
public enum ActivityType
{
PREVIEW,
COLLECT_WORD,
DAILY_COLLECT_WORD,
MICRO_PAYMENT,
CUMULATION_RECHARGE,
WHEEL,
LOGIN_ACTIVITY,
BUY_NEW_CARD_DIRECT,
NEW_CARD_UPGRADE_REWARD,
NEW_CARD_GIFT,
FIRST_RECHARGE,
CHARACTER_PREVIEW,
CHARACTER_BUY_DIRECT,
CHARACTER_TASK_ACTIVITY,
CHARACTER_GIFT,
SHARE_ACTIVITY,
COMMON_TASK,
COMMON_EXCHANGE,
CHARACTER_LOTTERY,
ANNIVERSARY_GIFT,
WEEKEND_DOUBLE,
FIRST_RECHARGE2,
NEW_FIRST_RECHARGE,
PASS,
SEND_GIFT_ACTIVITY,
SEASON_GIFT_1,
SEASON_GIFT_2,
PASS_EXP_BONUS_ACTIVITY,
NEW_CARD_ACTIVITY_REWARD = 31,
GIVE_OLD_CARD,
GIVE_OLD_CARD_UP,
OLD_CARD_STRATEGY,
OLD_CARD_ACTIVITY_REWARD,
OLD_CARD_COLLECTION_REWARD,
LIGHT_UP_ACTIVITY,
CHARACTER_FREE_ACTIVITY,
VOTE_ACTIVITY,
CHARACTER_SUITE_LOTTERY,
ANNIVERSARY_REVIEW,
LIMITLOTTERY_ACTIVITY,
PINK_LOTTERY_ACTIVITY,
CHARACTER_UPGRADE_ACTIVITY,
PASS_LOTTERY_ACTIVITY,
CHARACTER_DISCOUNT_PACKAGE,
COMMON_LOGIN_ACTIVITY,
RESEARCH_LOTTERY_ACTIVITY,
MYSTERYSTORE_ACTIVITY,
OLD_MODEL_RESELL_DISCOUNT,
}
public enum ActivityCollectionType
{
NONE,
ACTIVITY_LOBBY,
NEW_CARD_ACTIVITY,
CHARACTER_ACTIVITY,
ANNIVERSART_ACTIVITY,
OLD_CARD_ACTIVITY,
LOTTERY_ACTIVITY,
ANNIVERSART_ACTIVITY2,
ANNIVERSART_ACTIVITY3,
}
游戏通过继承ZeroFormatterSerializer为每一个数据类定义了专用的序列化类,自己试了一下,直接使用ZeroFormatterSerializer的Deserialize方法就能完成反序列化的步骤。具体代码如下:
public static void Main(string[] args)
{
using (FileStream fs = new FileStream(@"D:\TextAsset\ActivityLobby.txt", FileMode.Open, FileAccess.Read, FileShare.Read))
{
byte[] bytes = new byte[(int)fs.Length];
fs.Read(bytes, 0, (int)fs.Length);
var data = ZeroFormatterSerializer.Deserialize<List<ActivityLobbyInfo>>(bytes);
Console.WriteLine(data.Count);
Console.WriteLine(data[0].TabName);
Console.WriteLine(data[10].TitleName);
Console.ReadLine();
}
}
注意,泛型中指定的代码应该是类的集合,可以是ActivityLobbyInfo[]
或者List
。
运行一下,反序列化这部分应该是没问题了。
现在已经可以将数据反序列化成对象,接下来的目标将对象实例转化成一种可读性比较好的格式,比如此前版本游戏保存数据用的csv格式。上NuGet搜搜看有没有用于读写CSV格式的包。
好家伙,刚用过C#中最快的序列化器,这儿又有个.Net中最快的JSON、JSV、CSV文本序列化器?那么就决定是你了,ServiceStack.Text!
具体代码如下:
public static void Main(string[] args)
{
using (FileStream fs = new FileStream(@"D:\TextAsset\ActivityLobby.txt", FileMode.Open, FileAccess.Read, FileShare.Read))
{
byte[] bytes = new byte[(int)fs.Length];
fs.Read(bytes, 0, (int)fs.Length);
var data = ZeroFormatterSerializer.Deserialize<List<ActivityLobbyInfo>>(bytes);
using (StreamWriter sw = new StreamWriter(@"D:\TextAsset\ActivityLobby.csv"))
{
string result = CsvSerializer.SerializeToString<List<ActivityLobbyInfo>>(data);
sw.WriteLine(result);
}
}
}