查看原文
其他

基于Mono注入保存Draw & Guess历史房间数据

xhyeax 看雪学苑 2022-07-01

本文为看雪论坛优秀文章
看雪论坛作者ID:xhyeax

最近在Steam购买了你画我猜(Draw & Guess),游戏并不支持保存最近房间代码,以及玩家id。

由于众所周知的服务器原因,经常有人掉线或闪退(也可能是自己的原因),重连后无法返回之前的房间。
于是考虑使用Mono注入技术,实现历史房间数据的保存。

1


逆向分析过程


1、文件结构分析


查看游戏目录结构,发现是使用Unity编写的Mono平台游戏,关键dll文件位于\steamapps\common\Draw & Guess\Draw&Guess_Data\Managed,并且未加密。

2、房间代码生成


搜索RoomCode,定位到类RoomCodeEncoder,发现房间代码是由房主的SteamID进行base36编码生成,对应DecimalToArbitrarySystem函数。
 
ArbitraryToDecimalSystem函数用于将房间代码转换为SteamID,在MenuClicks.EnterRoomCode函数中被调用,通过LobbyManager的Join函数加入指定玩家的房间(如下图):


因此,只需记录玩家id,即可计算出房间代码。

PS:可通过http://steamcommunity.com/profiles/+steamID,打开个人主页。

3、记录玩家id

搜索PlayerList,找到LobbyPlayerList和IngamePlayerList,分别对应匹配时玩家列表和游戏时玩家列表。

(1)匹配时玩家列表

类结构:
public static LobbyPlayerList Instance;public List<RectTransform> PlayerList = new List<RectTransform>();public List<LobbyPlayerInfo> Players = new List<LobbyPlayerInfo>();


AddPlayer 函数:

 
可通过LobbyPlayerList.Instance.Players直接获取到玩家列表。

游戏时玩家列表

类结构:
public static LobbyPlayerList Instance;public List<RectTransform> PlayerList = new List<RectTransform>();public List<LobbyPlayerInfo> Players = new List<LobbyPlayerInfo>();
AddPlayer 函数:


可使用IngamePlayerList.CurrentList的ContainedPlayers字段获取玩家列表,由于修饰符为private(非public),需要通过反射获取。

4、确定获取时机


查看AddPlayer调用栈:


由此可确定以下两个函数:
RoundManager.UserCode_RpcSetInfo(对应LobbyPlayerList)
LobbyPlayerInfo.UserCode_RpcSetPlayerInfo(对应IngamePlayerList)
 
注意到后者传入的是数组,推测该函数是每次按下Tab,显示玩家列表时调用,需要手动按键才能触发。
 
考虑到两者先后顺序(LobbyPlayerList先于IngamePlayerList),故选择使用LobbyPlayerList.Instance.Players获取玩家列表。


5、确定Hook函数


搜索字符串游戏将在,定位到SimplifiedChineseLocalisation.UIGameStartingIn变量,查看其调用栈:


双击UserCode_RpcGetCountdown函数,查看函数体:


功能是调用LobbyChat.Instance.ReceiveChat函数,更新聊天框内容。
 
由此,选择在LobbyPlayerInfo.UserCode_RpcGetCountdown函数调用后(游戏即将开始时),保存房间代码及玩家列表。(保存上次调用时间,超过10秒则保存房间数据到本地)
 
同时,为了提示模块已经加载,选择在LobbyChat.Start函数调用后输出提示信息。
 
PS:由于LobbyChat实际上是更新一个TextMeshProUGUI(支持富文本标签),所以可以指定字体大小、颜色等属性。(在别人房间里发送,会被服务器断开连接)TextMesh Pro支持的富文本标签见Rich Text。


2


注入模块开发


基于MonoHook,开发一个注入dll。


1、创建项目并导入依赖


在Visual Studio中创建一个.Net 4.0类库项目,将必要的游戏dll添加为依赖。


2、编写注入代码


此处仅贴出关键代码,完整项目代码见github:DAGHistory。


3、模块加载提示


hook LobbyChat.Start函数,在聊天界面创建后输出信息。
public static void StartReplace() { // 先调用原函数 StartProxy(); // 输出信息到聊天框 LobbyChat.Instance.ReceiveChat("<color=#778899>[DAGHistory]: Loaded</color>"); }


4、自定义类RoomData


使用该类保存房间数据。

public class Player{ ulong steamID; string name;
public Player(string name, ulong steamID) { this.name = name; this.steamID = steamID; } public string Name { get => name; set => name = value; } public ulong SteamID { get => steamID; set => steamID = value; }}
public class RoomData{ string roomCode; List<Player> playerList;
public string RoomCode { get => roomCode; set => roomCode = value; } public List<Player> PlayerList { get => playerList; set => playerList = value; }
public override string ToString() { return JsonConvert.SerializeObject(this); }}

重载ToString函数,使用Json序列化该对象。


5、获取房间数据


遍历LobbyPlayerList.Instance.Players,并生成房间代码,保存到RoomData对象。
public static RoomData getRoomData(){ RoomData roomData = new RoomData(); if (LobbyPlayerList.Instance != null && LobbyPlayerList.Instance.Players != null && LobbyPlayerList.Instance.Players.Count != 0) { List<LobbyPlayerInfo> players = LobbyPlayerList.Instance.Players; roomData.PlayerList = new List<Player>(); for (int i = 0; i < players.Count; i++) { LobbyPlayerInfo lobbyPlayerInfo = players[i]; string name = lobbyPlayerInfo.Name; ulong steamID = lobbyPlayerInfo.SteamID.m_SteamID; if (i == 0) { roomData.RoomCode = CodeEncoder.codeEncode(steamID); } roomData.PlayerList.Add(new Player(name, steamID)); } } return roomData;}

6、自动保存房间数据


hook LobbyPlayerInfo.UserCode_RpcGetCountdown函数,判断是否需要保存房间数据到文件。
public static void UserCode_RpcGetCountdownReplace(byte s) { // 调用原函数 UserCode_RpcGetCountdownProxy(s); // 如果距离上一次调用该函数超过10秒,获取房间数据并保存到文件 if (Time.realtimeSinceStartup - lastCallTime > 10) { RoomUtil.saveRoomData(); LobbyChat.Instance.ReceiveChat("<color=#778899>[DAGHistory]: Saved</color>"); lastCallTime = Time.realtimeSinceStartup; } }

7、加入房间


分析发现该功能最终调用的是LobbyManager.s_Singleton.Join函数,传入steamID,照搬即可。
public static void joinRoom(string code){ if (code != null) { LobbyManager.s_Singleton.Join(CodeEncoder.codeDecode(code).ToString()); }}

8、获取日志路径


参考LogFileOpener.ReturnLogPath函数,编写以下代码(将默认值替换为临时文件夹)。
private static string ReturnLogPath(){ RuntimePlatform platform = Application.platform; switch (platform) { case RuntimePlatform.OSXEditor: case RuntimePlatform.OSXPlayer: return "~/Library/Logs/Unity/"; case RuntimePlatform.LinuxPlayer: case RuntimePlatform.LinuxEditor: return Path.Combine("~/.config/unity3d", Application.companyName, "Draw_Guess"); case RuntimePlatform.WindowsPlayer: return Path.Combine(Environment.GetEnvironmentVariable("AppData"), "..", "LocalLow", Application.companyName, "Draw_Guess"); default: return Path.GetTempPath(); }}

点击左下角Logs打开该目录:


9、GUI


为方便使用,增加图形操作界面,提供手动保存、复制上局房间代码、加入上局房间功能。
 

并设置按后引号键(esc下方)隐藏该界面。


10、生成dll文件


在Visual Studio中选择生成DAGHistory,得到bin\Debug\DAGHistory.dll


3


测试


使用SharpMonoInjector,提供的命令行注入工具SharpMonoInjector.Console进行注入。

1、注入dll并调用Load函数


将smi.exe、SharpMonoInjector.dll、待注入dll放到同一目录下,在该目录执行以下命令:
.\smi.exe -p "Draw&Guess" -a "DAGHistory.dll" -n "DAGHistory" -c "Loader" -m "Load" inject

之后使用相关功能即可。

2、查看日志


点击游戏左下角Logs打开日志目录(Windows:C:\Users\用户名\AppData\LocalLow\Acureus\Draw_Guess)

其中LastRoom.json和DAGHistory.log即注入模块生成的日志文件。前者保存上次游玩的房间数据(用于复制代码及快速加入),后者保存历史记录。


 


看雪ID:xhyeax

https://bbs.pediy.com/user-home-691226.htm

*本文由看雪论坛 xhyeax 原创,转载请注明来自看雪社区





# 往期推荐

1. 一个方案:家用路由器D-LINK DIR-81漏洞挖掘实例分析

2. 2015年AliCrackMe学习Android逆向

3. JSONP和CORS跨域漏洞学习笔记

4. 记一次MEMZ样本分析

5. GlobeImposter家族的病毒样本分析

6. CVE-2010-2553 堆溢出漏洞分析



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存