犹格索托斯的逆向工程

“我万物归一者泡泡今个心情好,拿逆向工程知识随机塞爆一个幸运调查员也很正常吧?”

“能不能塞塞我的,再不教我我可要开始念了!“

犹格·索托斯知晓门。犹格·索托斯即是门。犹格·索托斯是门的钥匙和看门人……

——San值不是很高的作者的胡言乱语
本文基于V1.07版本分析,请注意时效性。

Q: 今天是哪个游戏倒霉?

A: 犹格索托斯的庭〇。模拟经营加可爱纸片人,就好这口!

Q: 原因?

A: 感觉很好玩,好玩到想要剖开肚子亲自察看。我开始逐渐理解病娇了。

Q: 想得到什么?

A: 一开始想得到各种ID表用来写CheatTable,不过现在看来最大的收获可能是美术素材。

桃子可爱滴捏

初步分析

游戏的Unity版本为2020.3.33f1c2,Mono方式,单走一个Assembly-CSharp

游戏资源方面,AssetStudio把全部资源解出来备用。大概看了一圈,各种图片纹理都直接能看。比较特别的是声音资源用了FMOD Studio的方案,存在一堆.bank文件里。有bank api可参考,外加我也对声音资源没什么兴趣,就不碰了。

如此一来,资源方面几乎可以堂堂完结了。

逻辑方面,直接用老朋友dnspy加载Assembly-CSharp.dll。大致扫几眼,似乎都被自动还原了,并且没有什么动态加载行为,看起来难度并不高。那么接下来就是找到跟物品ID相关的部分了。

Item数据加载点

搜索包含“Item”的类,非常不错,看到了名叫ItemManager的家伙。Load方法如下:

// ItemManager
// Token: 0x0600056F RID: 1391
public static void Load()
{
	if (ItemManager.mItemArray != null)
	{
		return;
	}
	ItemManager.mItemArray = ItemArray.Deserialize(ResourcesManager.Instance.LoadTableByte("Item"));
	List<long> keys = ItemManager.mItemArray.Keys;
	int count = keys.Count;
	ItemManager.mKeyIndexMap = new Dictionary<long, int>(count);
	for (int i = 0; i < count; i++)
	{
		ItemManager.mKeyIndexMap[keys[i]] = i;
	}

看起来没找错地方。但是LoadTableByte方法里面涉及到解密流程,先加两行Dump一份解密完的文件作为参考。

	byte[] array = ResourcesManager.Instance.LoadTableByte("Item");
	using (FileStream fileStream = new FileStream("./Item.decrypted", FileMode.OpenOrCreate, FileAccess.Write))
	{
		fileStream.Write(array, 0, array.Length);
	}

编译,保存模块,运行一下游戏。游戏根目录出现了一份解密完成的Item二进制。

Table解密逻辑

LoadTableByte方法如下:

// ResourcesManager
// Token: 0x06000169 RID: 361 RVA: 0x00021DA4 File Offset: 0x0001FFA4
public byte[] LoadTableByte(string table)
{
	TextAsset textAsset = this.Load<TextAsset>(string.Format("Table/{0}", table));
	if (textAsset != null && textAsset.bytes != null)
	{
		byte[] bytes = textAsset.bytes;
		this.ConvertDataBytes(ref bytes);
		return bytes;
	}
	return null;
}

从这里可以确定加载的文件是AssetBundle解包出来的TextAsset,路径是Table/Item

ConvertDataBytes是解密部分,方法如下:

// ResourcesManager
// Token: 0x0600016A RID: 362 RVA: 0x00021DE8 File Offset: 0x0001FFE8
private void ConvertDataBytes(ref byte[] bytes)
{
	bytes = RSAHelper.DecryptByPublicKey(RSAHelper.GetPublicEngine(this.s_RSAKey.PublicKey), bytes);
	int i = 1;
	int num = 0;
	while (i < bytes.Length)
	{
		byte b = (byte)(i - num);
		bytes[i] ^= b;
		i <<= 1;
		num++;
	}
	int j = bytes.Length - 1;
	int num2 = 0;
	while (j > 0)
	{
		byte b2 = (byte)(j - num2);
		bytes[j] ^= b2;
		j >>= 1;
		num2++;
	}
}

// ResourcesManager
// Token: 0x040001CC RID: 460
private RSAHelper.RSAKey s_RSAKey = new RSAHelper.RSAKey
{
	PublicKey = "MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCyJiTzABL2wironv9+4wnZTg7JXr1ekiMA3RdL2e+W8kEtyZgghb5KBBAASKuiGNxhadrnSgC8+h1r7B/JLudatvdlzwyy1gAs/mbVYHd7x1WoBfzDpWkZX8bhDO/uX4GnBhWAmtapbbjVGOAVIuaIV8lBzNXJ30mJPDI4wKc7/QIBAw==",
	PrivateKey = ""
};

可以看到解密流程分为两部分,先rsa再xor。

RSA解密

首先分析rsa部分。在这里制作组使用私钥签名资源,运行时使用公钥解密。把公钥补上头尾丢进openssl解析一下,确认是PEM格式。

❯ cat pk.pem
-----BEGIN PUBLIC KEY-----
MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCyJiTzABL2wironv9+4wnZTg7JXr1ekiMA3RdL2e+W8kEtyZgghb5KBBAASKuiGNxhadrnSgC8+h1r7B/JLudatvdlzwyy1gAs/mbVYHd7x1WoBfzDpWkZX8bhDO/uX4GnBhWAmtapbbjVGOAVIuaIV8lBzNXJ30mJPDI4wKc7/QIBAw==
-----END PUBLIC KEY-----
❯ openssl rsa -pubin -in pk.pem -text -noout
RSA Public-Key: (1024 bit)
Modulus:
    00:b2:26:24:f3:00:12:f6:c2:2a:e8:9e:ff:7e:e3:
    09:d9:4e:0e:c9:5e:bd:5e:92:23:00:dd:17:4b:d9:
    ef:96:f2:41:2d:c9:98:20:85:be:4a:04:10:00:48:
    ab:a2:18:dc:61:69:da:e7:4a:00:bc:fa:1d:6b:ec:
    1f:c9:2e:e7:5a:b6:f7:65:cf:0c:b2:d6:00:2c:fe:
    66:d5:60:77:7b:c7:55:a8:05:fc:c3:a5:69:19:5f:
    c6:e1:0c:ef:ee:5f:81:a7:06:15:80:9a:d6:a9:6d:
    b8:d5:18:e0:15:22:e6:88:57:c9:41:cc:d5:c9:df:
    49:89:3c:32:38:c0:a7:3b:fd
Exponent: 3 (0x3)

至于BouncyCastle的DecryptByPublicKey,它只会解密头128byte,再和剩下的拼到一起。方法如下:

// Org.BouncyCastle.RSATools.RSAHelper
// Token: 0x060004B8 RID: 1208 RVA: 0x00013E7C File Offset: 0x00012E7C
public static byte[] DecryptByPublicKey(IAsymmetricBlockCipher publicEngine, byte[] enBytes)
{
	byte[] result;
	try
	{
		byte[] array = new byte[128];
		Buffer.BlockCopy(enBytes, 0, array, 0, array.Length);
		byte[] array2 = publicEngine.ProcessBlock(array, 0, array.Length);
		byte[] array3 = new byte[enBytes.Length + (array2.Length - array.Length)];
		Buffer.BlockCopy(array2, 0, array3, 0, array2.Length);
		if (enBytes.Length > array.Length)
		{
			Buffer.BlockCopy(enBytes, array.Length, array3, array2.Length, array3.Length - array2.Length);
		}
		result = array3;
	}
	catch (Exception ex)
	{
		throw ex;
	}
	return result;
}

直接github搜DecryptByPublicKey,限定语言python,偷一个别人写好的再小改一下就好了。

XOR解密

	int i = 1;
	int num = 0;
	while (i < bytes.Length)
	{
		byte b = (byte)(i - num);
		bytes[i] ^= b;
		i <<= 1;
		num++;
	}
	int j = bytes.Length - 1;
	int num2 = 0;
	while (j > 0)
	{
		byte b2 = (byte)(j - num2);
		bytes[j] ^= b2;
		j >>= 1;
		num2++;
	}

没什么好说的,直接照着转写成python即可。唯一需要注意的就是需要加上取模模拟overflow。

解密脚本

把以上两块拼起来就可以得到Table里所有的文件的解密脚本了。

至此,我们终于和ItemManager.Load站在同一条起跑线上了。

Item反序列化

本部分内容仅为记录被折磨的过程而生,不建议任何人复刻。
真想要解析建议直接调用,手写模板纯属没事找事。

本部分将比较简略,没什么技术含量,全是无差别的人类劳动。

ItemArray的元素分Key和Value。(此处的key非ProtocolParser.ReadKey,而是instance.Keys.Add。本部分不需要也不会涉及ProtocolParser.ReadKey)

key的id=8,值为一个uint64。ReadUInt64方法如下,此后的ReadUInt32等方法仅是长度不同。

value的id=18,值实际上是一堆((byte)Attribute_type, (uint64||string)Attribute_value)元组,通过以下逻辑解析。

逻辑清晰,实现简单,但是模板很难写。这一个模板花了博主几个小时,不仅写得很丑陋还没什么用。

解析效果:

这里解析出的各种ID应该是需要去字符串数据库查的,所以说即使解析成功也无法得知物品与ID对应关系,除非从Icon看图标人脑辨认。而这个解析模板只能解析Item,这也是为什么说不建议复刻此类操作。

成功了,但是和没成功没什么区别。受不了了,一拳把地球打爆.jpg

后记

已经小半年没写博客了,这篇又是水货。很惭愧,但是拒不改正。不水的倒是有两篇,但是都在草稿箱睡了很久了,大抵是夭折了罢。

这次逆向说白了没什么目的性,真的需要做什么操作不如直接改C#轻松。

该去改点真正有用的东西了,比如说一次炼金一百个1之类的,然后继续闷头挣钱不管好感。

正好凑够四个章节,四个女主一人一张图,欸嘿~

你问桃子?那个算送的。


  1. 这个行为可以在AlchemySystem.AlchemyRecipeSystemManager.RecipeAlchemy修改,如果有读者也想这么做的话。 ↩︎

评论

  1. li
    2 年前
    2024-5-21 11:05:29

    能解密fmod加密的bank文件吗?

    • 博主
      li
      2 年前
      2024-5-24 11:19:00

      从可行性上说,解肯定是可以解。但是我没有亲自实践,可以搜搜有没有其他用这个的游戏的解包。
      FMOD Studio是个音频组件,一般来说应该不会在资源加密上下太多功夫?

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇