實現環境:Unity 5.4.6f3 (64-bit)
需求背景
??剛完成一項任務:增加垃圾代碼并調用,這個需求是為了出馬甲包。
??增加垃圾代碼我們借助了一個第三方插件,叫Beebyte.Obfuscator,它實現了代碼混淆,并且可以根據當前腳本內的函數創建一些“與之類似”無用函數。
??垃圾代碼好加,但是要調用就比較麻煩了。有以下幾個問題:
- 垃圾代碼是隨機生成的,名字也是亂七八糟,不可能手寫調用(并且垃圾代碼是打包時直接注入到DLL的,根本沒機會手寫)
- 加入調用的目的是改變代碼的執行(靜態),因此反射這一秘法也被封印
??綜上,要完成需求只能采用“動態的靜態方式”,是不是很凌亂……剛分析完需求我是一臉懵,不過后來想到了Obfuscator實現的方式:直接修改DLL里的IL代碼,這里就祭出了Cecil神器。
調用方案
??最核心的問題如何調用解決了,但并不是萬事大吉,還有一些問題需要考慮:
- Obfuscator生成的垃圾函數是與真實函數相似的,那么其實可能含有一些邏輯,隨便調用很可能會報錯
- 垃圾函數的參數列表也是各式各樣,安全調用需要費一番功夫
- 胡亂調用一些垃圾代碼,即使不出錯,性能上也會受到一定的影響
??跟需求方討論下來,調用不需要做的很復雜,于是出了一個簡單的方案:
創建一個“無用”腳本,把要調用的垃圾代碼都放在這里。里面要手寫一批函數,作為模板讓Obfuscator再隨機創建一批垃圾代碼。考慮到性能問題,這里的函數實現都很簡單,沒有復雜計算、沒有Log輸出、沒有字符串處理。
而調用處,目前在GameManager中定義了一個函數,作為垃圾代碼的調用入口,這樣正式代碼受到的影響最小。
注入實現
public static void FakeInject(string assemblyPath)
{
// 注入調用的方法(調用入口放在GameManager中)
MethodInfo inject = typeof(GameManager).GetMethod("FakeCall", BindingFlags.NonPublic | BindingFlags.Static);
using (FileStream stream = new FileStream(assemblyPath, FileMode.Open))
{
AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(stream);
// 只調用FakeMethods類里的Fake方法
TypeDefinition fakeDef = assembly.MainModule.GetType("FakeMethods");
// 相同參數要打出相同的包
Random.InitState(_options.sha1seed.GetHashCode());
foreach (var method in fakeDef.Methods)
{
// 特殊函數不能調用,比如.ctor
if (method.IsSpecialName || method.IsRuntimeSpecialName) continue;
if (Random.Range(0, 100) > 50)
{
InjectMethodCall(assembly, inject, method.GetElementMethod());
}
}
stream.Seek(0, SeekOrigin.Begin);
assembly.Write(stream);
}
}
public static void InjectMethodCall(AssemblyDefinition assembly, MethodInfo inject, MethodReference patchCall)
{
TypeDefinition injectType = assembly.MainModule.GetType(inject.DeclaringType.FullName);
foreach (MethodDefinition method in injectType.Methods)
{
if (method.Name == inject.Name)
{
InjectMethodCall(method, patchCall);
break;
}
}
}
private const string Void = "System.Void";
/// <summary>
/// 在目標函數的開頭,注入函數調用(無參、靜態)
/// </summary>
/// <param name="injectMethod">要注入的方法</param>
/// <param name="callMethod">被調用的方法</param>
private static void InjectMethodCall(MethodDefinition injectMethod, MethodReference callMethod)
{
Instruction firstIns = injectMethod.Body.Instructions.First();
ILProcessor worker = injectMethod.Body.GetILProcessor();
Instruction insert = worker.Create(OpCodes.Call, callMethod);
worker.InsertBefore(firstIns, insert);
if (callMethod.ReturnType.ToString() != Void)
{
worker.InsertAfter(insert, Instruction.Create(OpCodes.Pop));
}
}
??有幾個地方稍微說下
保存修改的DLL
??我開始試了網上直接給AssemblyDefinition傳路徑的方式讀寫DLL,總是報錯。報錯的大意是,DLL被占用了無法修改。經過一番胡亂嘗試只找到這一種可行方式:自己創建好文件流,用AssemblyDefinition.ReadAssembly讀入信息,在注入操作完畢后,將文件指針指向文件頭,然后覆蓋寫入。
有無返回值的差別
??IL層面,如果不用函數返回值的話,需要在函數調用后面,將返回值出棧。這里就需要判斷哪些函數有返回值,實在沒想到啥好方法,只能用名字判斷了……
注入時機
??注入要在打包編譯好DLL后執行,這里Unity提供了一個回調[PostProcessScene(2)],場景處理后可以執行我們的注入了。這里又有兩個問題:
- 如果項目是多場景的,那么這個回調會多次走到,因此需要自己去重。
- 我的調用是依賴Obfuscator混淆結果的,因此需要在Obfuscator混淆結束后才執行,所以這里傳入的優先級2。
[2019-7-16 修改]
實現環境:Unity 2018.4.1f1 (64-bit)
??Unity2018好像多了一些庫的引用,導致修改DLL后會出現找不到引用的報錯,這里給出修改方案:
HashSet<string> extraAsmRefDirs = new HashSet<string>();
#if UNITY_2017_3_OR_NEWER
extraAsmRefDirs.UnionWith(AssemblyReferenceLocator.GetAssemblyReferenceDirectories());
#endif
DefaultAssemblyResolver assemblyResolver = new DefaultAssemblyResolver();
foreach (string path in extraAsmRefDirs)
assemblyResolver.AddSearchDirectory(path);
ReaderParameters readParams = new ReaderParameters
{
AssemblyResolver = assemblyResolver,
};
// 加載Assembly
AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(stream, readParams);
這里的代碼替換上面加載程序集的地方:
AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(stream);