使用Mono.cecil修改Unity游戏内存
一.Mono.cecil简介
Mono.Cecil是一个强大的MSIL的注入工具,利用它可以实现动态创建程序集,也可以实现拦截器横向切入动态方法,甚至还可以修改已有的程序集,并且它支持多个运行时框架上例如:.net2.0/3.5/4.0,以及silverlight程序
官方地址:http://www.mono-project.com/Cecil
二.Unity游戏内存修改原理
Unity游戏基于mono运行原理简介
以Android平台为例,游戏代码(C#、lua)在打包时会以DLL的形式存在于apk,在运行apk时,DLL文件中的代码、数据等信息被读取到mono虚拟机的进程中。mono虚拟机首先分析DLL文件中的基本信息,提取出IL代码;然后根据IL代码,将代码中的指令转译成mono虚拟机自己的指令,进而“取址分析执行”。
IL指令简介
IL是.NET框架中中间语言(Intermediate Language)的缩写。使用.NET框架提供的编译器可以直接将源程序编译为.exe或.dll文件,但此时编译出来的程序代码并不是CPU能直接执行的机器代码,而是一种中间语言IL(Intermediate Language)的代码。我们可以通过 ILadsm.exe工具去读取存储在DLL文件中的IL代码。
ILadsm.exe读取IL示意图
C#反射机制
反射是.NET中的重要机制,通过反射可以得到*.exe或*.dll等程序集内部的接口、类、方法、字段、属性、特性等信息,还可以动态创建出类型实例并执行其中的方法。
反射的功能很强大,任何复杂抽象的分层架构或者复杂的设计模式均是建立在这些基础之上的,比如我们要进行模块化、组件化开发,要严格的消除模块之间的耦合,要进行动态接口调用。开发这样强大而灵活的系统,必须要用反射才行,我们只要把它用在合适的位置,不仅能使代码变的清晰简洁,更能让它发挥出惊人的力量。
换句话说,只要我们可以获得某个对象的引用,那么通过反射机制就可以获取该对象的信息(类名、成员属性等)。
基于Mono.cecil修改Unity内存对象的原理
在Android平台、使用mono虚拟机运行代码的情况下,我们可以看到游戏代码是以DLL的形式存在于apk中。通过Mono.cecil库,可以读取任意模块的类信息。
举个例子,有如下代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace hello
{
class HelloWorld
{
int a;
public void init()
{
a = 100;
}
}
class Program
{
static void Main(string[] args)
{
HelloWorld hell = new HelloWorld();
hell.init();
}
}
}
编译生成DLL后,使用 ILadsm.exe读取到代码
C#反射机制可以让我们获取相应程序集的信息
AssemblyDefinition assembiy = AssemblyDefinition.ReadAssembly(srcpath); //Path: dll or exe Path
foreach (TypeDefinition type in assembiy.MainModule.Types)//主模块
{
out_str += (string.Format("Class NameSpace :[{0}]\r\n", type.Namespace)); //命名空间
out_str += (string.Format("Class Name :[{0}]\r\n", type.Name)); //类名
foreach (FieldDefinition field in type.Fields)//遍历成员
{
out_str += (string.Format("field Name :[{0}]\r\n", field.FullName));
}
foreach (MethodDefinition meth in type.Methods) //遍历方法名称
{
try
{
out_str += (string.Format("Method Name :[{0}]\r\n", meth.FullName));
out_str += (string.Format(".maxstack {0}\r\n", meth.Body.MaxStackSize));
foreach (Instruction inst in meth.Body.Instructions)
{
out_str += (string.Format("L_{0}: {1} {2}\r\n", inst.Offset.ToString("x4"),
inst.OpCode.Name,
inst.Operand is String ? String.Format("\"{0}\"", inst.Operand) : inst.Operand));
}
}
catch
{
}
}
}
assembiy.Write(dstpath);
return out_str;
实际上,利用反射机制寻找到指定的类,通过mono.cecil库在该类的构造函数中插码获取到该类的引用,那么我们就可以在恰当的时机完成该类的信息读取并改动其成员的值。示例代码
var method = assembiy.MainModule
.Types.FirstOrDefault(t => t.Name == class_name)
.Methods.FirstOrDefault(m => m.Name == ".ctor");
if (method == null|| method.Body == null)
{
return;
}
var worker = method.Body.GetILProcessor(); //Get IL
var Constructor = assembiy.MainModule.ImportReference(typeof(Register).GetConstructor(new Type[] { }));//Create Instance
var ins = method.Body.Instructions[0];//Get First IL Step
ins = method.Body.Instructions[method.Body.Instructions.Count - 1];//Get First IL Step
worker.InsertBefore(ins, worker.Create(OpCodes.Newobj, Constructor));
worker.InsertBefore(ins, worker.Create(OpCodes.Ldarg_0));
worker.InsertBefore(ins, worker.Create(OpCodes.Call,
assembiy.MainModule.ImportReference(typeof(Register).GetMethod("RegisterUser"))));Call Instance Method
三.实例
随便找的一个小栗子:
https://github.com/chunheinanzi/Snake
使用Unity 2018.3.7f1 (64-bit)编译生成apk
运行起来是这样的
将apk解压(zip方式),游戏代码文件在assets\bin\Data\Managed目录下Assembly-CSharp.dll。
编写修改IL代码的程序
新建winform工程,插入相应控件。主体是textbox作为显示信息的界面,底部控件用于选择DLL文件,并修改IL。
核心代码
public static void InjectIntoCtor(AssemblyDefinition assembiy, String class_name)
{
if (class_name.Equals(""))
{
return;
}
var method = assembiy.MainModule
.Types.FirstOrDefault(t => t.Name == class_name)
.Methods.FirstOrDefault(m => m.Name == ".ctor");
if (method == null|| method.Body == null)
{
return;
}
var worker = method.Body.GetILProcessor(); //Get IL
var Constructor = assembiy.MainModule.ImportReference(typeof(Register).GetConstructor(new Type[] { }));//Create Instance
var ins = method.Body.Instructions[0];//Get First IL Step
ins = method.Body.Instructions[method.Body.Instructions.Count - 1];//Get First IL Step
worker.InsertBefore(ins, worker.Create(OpCodes.Newobj, Constructor));
worker.InsertBefore(ins, worker.Create(OpCodes.Ldarg_0));
worker.InsertBefore(ins, worker.Create(OpCodes.Call,
assembiy.MainModule.ImportReference(typeof(Register).GetMethod("RegisterUser"))));Call Instance Method
return;
}
public static String InjectIntoCSharp(String srcpath, String dstpath)
{
if (srcpath.Equals("") || dstpath.Equals(""))
{
return "ERROR : Check the Params !\r\n";
}
String out_str = "";
AssemblyDefinition assembiy = AssemblyDefinition.ReadAssembly(srcpath); //Path: dll or exe Path
foreach (TypeDefinition type in assembiy.MainModule.Types)
{
out_str += (string.Format("Class NameSpace :[{0}]\r\n", type.Namespace)); //命名空间
out_str += (string.Format("Class Name :[{0}]\r\n", type.Name)); //类名
foreach (FieldDefinition field in type.Fields)
{
out_str += (string.Format("field Name :[{0}]\r\n", field.FullName));
}
if (!type.Name.Equals("<Module>"))
{
InjectIntoCtor(assembiy, type.Name); //在构造函数中插码,
}
foreach (MethodDefinition meth in type.Methods) //遍历方法名称
{
try
{
out_str += (string.Format("Method Name :[{0}]\r\n", meth.FullName));
out_str += (string.Format(".maxstack {0}\r\n", meth.Body.MaxStackSize));
foreach (Instruction inst in meth.Body.Instructions)
{
out_str += (string.Format("L_{0}: {1} {2}\r\n", inst.Offset.ToString("x4"),
inst.OpCode.Name,
inst.Operand is String ? String.Format("\"{0}\"", inst.Operand) : inst.Operand));
}
}
catch
{
}
}
}
assembiy.Write(dstpath);
return out_str;
}
编译时需要引用Mono.cecil相关的程序集。编译生成ILInject.exe。
编写注入到游戏的IL代码
注入代码需要对外提供一个接口,用于收集特定对象的引用。该部分还实现了一个打印类信息的线程,用于在logcat中实时打印游戏内存对象的信息。
核心代码
public sealed class LogDateTime_NonStatic
{
private List<object> item_list_;
public object obj_;
private static readonly LogDateTime_NonStatic instance = new LogDateTime_NonStatic();
public static LogDateTime_NonStatic Instance
{
get
{
return instance;
}
}
private LogDateTime_NonStatic() {
item_list_ = new List<object>();
Thread thread = new Thread(new ThreadStart(FieldDump));//创建线程
thread.Start();
}
public void LogDT(String tag, String data)
{
Debug.Log(this.ToString() + "" + tag + "-: " + data);
}
public void LogDT_Field(object obj)
{
obj_ = obj;
if (obj is null)
{
return;
}
item_list_.Add(obj);
//启动线程
}
public string getMemory(object o) // 获取引用类型的内存地址方法
{
GCHandle h = GCHandle.Alloc(o, GCHandleType.WeakTrackResurrection);
IntPtr addr = GCHandle.ToIntPtr(h);
return "0x" + addr.ToString("X");
}
public void FieldDump()
{
do {
Debug.Log("LogDateTime_NonStatic " + "Thread Start");
Thread.Sleep(10*1000);
foreach (object obj in item_list_)
{
object logbase = (object)obj;
LogDT(" Class Info ", string.Format("Date[{0}],namespace[{1}],class[{2}],hashcode[{3}]",
DateTime.Now.ToString(),
logbase.GetType().Namespace,
logbase.GetType().Name,
logbase.GetHashCode()));
//try {
FieldInfo[] infos = logbase.GetType().GetFields();//获取类共有中字段
foreach (FieldInfo item in infos)//遍历类中字段并赋值
//foreach (PropertyInfo item in properties)
{
string Fields = "public number:\n";
string name = item.Name; //名称
object value = item.GetValue(logbase); //值
Fields += string.Format("[Fields_name == {0}: {1} -- {2}],", name, value, value.GetType());
if (name.Equals("score"))
{
item.SetValue(logbase, 900);
Fields += "\n";
Fields += string.Format("[new][Fields_name == [{0}:{1} -- {2}]\n",
item.Name, item.GetValue(logbase), item.GetValue(logbase).GetType());
}
Fields += "\n=========================\n";
LogDT("Fields", Fields);
}
//获取非共有成员
BindingFlags flag = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;
infos = logbase.GetType().GetFields(flag);//获取类共有中字段
foreach (FieldInfo item in infos)//遍历类中字段并赋值
//foreach (PropertyInfo item in properties)
{
string Fields = "non public number:\n";
string name = item.Name; //名称
object value = item.GetValue(logbase); //值
Fields += string.Format("[Fields_name == {0}: {1} -- {2}],", name, value, value.GetType());
//if (name.Equals("score"))
//{
// item.SetValue(logbase, 900);
// Fields += "\n";
// Fields += string.Format("[new][Fields_name == [{0}:{1} -- {2}]\n",
// item.Name, item.GetValue(logbase), item.GetValue(logbase).GetType());
//}
//Fields += "\n=========================\n";
// LogDT("Fields", Fields);
}
//} catch {
// LogDT("FieldDump err", string.Format("Date[{0}],namespace[{1}],class[{2}],hashcode[{3}]"));
//}
}
} while (true);
}
每十秒钟对当前获取到的对象信息进行打印,搜索名为“score”的变量对其修改值为900。
编译生成DLL(HelloLog.dll)。
执行注入程序修改IL代码
将编译好的ILInject.exe和HelloLog.dll准备好,运行ILInject.exe。选择游戏DLL目录
点击“Inject”按钮,在UI输出提示信息表示IL代码修改完成。在assets\bin\Data\Managed目录生成Assembly-CSharp_inject.dll。
重新打包apk
将Assembly-CSharp_inject.dll文件重命名成Assembly-CSharp.dll,并将HelloLog.dll拷贝到assets\bin\Data\Managed目录下。使用
.\zipalign.exe -v -p 4 input.apk output.apk
对应用文件夹进行打包。
使用apksigner进行签名。
参考文档:https://blog.csdn.net/qq_32562005/article/details/106635566
运行
安装Apk到android平台,并运行。观察logcat信息,Tag为Unity的条目。可以看到对象信息的打印。
当dump线程遍历到变量“score”时修改其值为900,。
当score分数改变时,手机界面发生变化,由白色背景变成粉色背景.