使用.NET Reactor三方工具,把混淆加固热更dll的功能融入自动化打包工作流。
很多人用HybridCLR进行全代码热更,即使加密AB包也毫无安全可言。而HybridCLR社区版对于非全热更,AOT程序集往往还得作为AOT泛型元数据补充的aot dll直接打进包里,也就意味着AOT程序集也暴露出去了,同样毫无安全可言。
对于HybridCLR社区版,除了加密热更dll AB包,如何进一步提升安全性呢?代码混淆是一种比较常用的方式。
.Net Reactor是一款非常好用稳定的dll加固软件,并且支持dll合并。除了客户端GUI界面外,还提供了命令行工具,dotNET_Reactor.Console.exe -h 即可查看参数用法。
工具设计如图:
工具设计:
1. 需支持自定义忽略混淆方法名列表,配置到列表内的方法名将不进行混淆,如MonoBehavior声明周期方法,Awake、Start、Update等等,这些方法Unity以反射方式获取,一旦方法名改变将不会被执行。
2.自动扫描dll中的外部接口、抽象方法,并跳过这些方法名的混淆。
3. 支持配置混淆策略,如是否混淆可序列化类、字符串、枚举等。
4. 在打包工具界面,可选择加密方式。只需实现接口自己即可扩展自定义加密方式;
然而代码混淆有利有弊,如果处理不当风险很大,比如某些类和方法通过反射调用,类名和方法名一旦混淆处理就无法通过名字反射调用,序列化类也会无法反序列化。因此过滤混淆就需要小心翼翼。
通过标签忽略某个类名混淆:
可参考官方文档:Declarative Protection
1. 跳过该类的任何混淆
[System.Reflection.Obfuscation(Feature = "renaming")]
2. 仅跳过该类的类名混淆,内部类、方法名、变量不跳过
[System.Reflection.Obfuscation(Feature = "renaming", ApplyToMembers = false)]
通过调用dotNET_Reactor.Console.exe命令行加密dll:
如果不清楚命令行参数,.Net Reactor客户端还支持在UI上手动点选混淆配置,然后通过Command line菜单一键生成命令行,只需复制即可。
自动化混淆:
using GameFramework;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
namespace UGF.EditorTools
{
/// <summary>
/// 默认热更代码加密:使用.Net Reactor对hotfix.dll进行混淆加密
/// </summary>
public class DefaultProtectHotfixDllsHandler : IProtectHotfixDllsHandler
{
const bool invalid_metadata = false;
const bool obfuscate_public_types = false;//public类型加密
const bool stringencryption = true;//字符串加密
const bool enumencryption = false;//枚举加密
const bool fieldsencryption = false;
const bool serializableTypeEncryption = false;
const string DotNetReactorPath = "Tools/NETReactor/dotNET_Reactor.Console.exe";
const string IgnoreMethodsListFile = "Tools/NETReactor/IgnoreMethodsList.txt";
string mainHotfixDllName = Path.GetFileNameWithoutExtension(ConstEditor.HotfixAssembly);
public bool OnProtectDll(string inputDllFile, string outputDllFile)
{
if (Path.GetFileNameWithoutExtension(inputDllFile).CompareTo(mainHotfixDllName) == 0)
{
//只加密Hotfix.dll
return DotNetReactor(inputDllFile, outputDllFile);
}
return false;
}
private static bool DotNetReactor(string inputDllFile, string outputDllFile)
{
string toolsFile = UtilityBuiltin.AssetsPath.GetCombinePath(Directory.GetParent(Application.dataPath).FullName, DotNetReactorPath);
if (!File.Exists(toolsFile))
{
return false;
}
StringBuilder strBuilder = new StringBuilder();
strBuilder.AppendFormat(@"{0} -quiet -file ""{1}"" -targetfile ""{2}""", toolsFile, inputDllFile, outputDllFile);
strBuilder.AppendFormat(@" -invalid_metadata {0} -stringencryption {1} -exclude_enums {2} -exclude_fields {3} -exclude_serializable_types {4} -obfuscate_public_types {5} -rules "".*::methods:", invalid_metadata ? 0 : 1, stringencryption ? 0 : 1, enumencryption ? 0 : 1,
fieldsencryption ? 0 : 1, serializableTypeEncryption ? 0 : 1, obfuscate_public_types ? 1 : 0);
var ignoreMethods = MethodsFilter(Path.GetFileNameWithoutExtension(inputDllFile));
strBuilder.Append(ignoreMethods);
strBuilder.Append(@"::fields:$.::properties:$.::events:$.::onamespaces:""");//::otypes:
string commandLineStr = strBuilder.ToString();
GFBuiltin.LogInfo(commandLineStr);
var proceInfo = new System.Diagnostics.ProcessStartInfo(toolsFile, commandLineStr);
proceInfo.CreateNoWindow = true;
proceInfo.UseShellExecute = false;
proceInfo.Verb = "runas";
proceInfo.WorkingDirectory = Directory.GetParent(Application.dataPath).FullName;
bool success;
using (var proce = System.Diagnostics.Process.Start(proceInfo))
{
proce.WaitForExit();
success = proce.ExitCode == 0;
if (success)
{
GFBuiltin.LogInfo($"加密dll成功:{inputDllFile}");
}
return success;
}
}
private static string MethodsFilter(string assemblyName)
{
var assemblies = Utility.Assembly.GetAssemblies();
System.Reflection.Assembly hotfixAssembly = null;
foreach (var item in assemblies)
{
if (item.GetName().Name == assemblyName)
{
hotfixAssembly = item;
break;
}
}
if (hotfixAssembly == null) return null;
HashSet<string> ignoreMethodsNames = new HashSet<string>();
string ignoreFile = UtilityBuiltin.AssetsPath.GetCombinePath(Directory.GetParent(Application.dataPath).FullName, IgnoreMethodsListFile);
if (File.Exists(ignoreFile))
{
var methodNames = File.ReadAllLines(ignoreFile);
foreach (var method in methodNames)
{
if (string.IsNullOrWhiteSpace(method) || method.StartsWith("//")) continue;
ignoreMethodsNames.Add(method);
}
}
var typesInAssembly = hotfixAssembly.GetTypes();
foreach (var tp in typesInAssembly)
{
System.Type currentType = tp;
while (currentType != null)
{
if (currentType.BaseType != null && currentType.BaseType.Assembly != hotfixAssembly)
{
var methods = currentType.GetMethods(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
if (currentType.IsInterface)
{
foreach (var method in methods)
{
ignoreMethodsNames.Add(method.Name);
}
}
else
{
foreach (var method in methods)
{
if (method.IsVirtual || method.IsAbstract)
{
ignoreMethodsNames.Add(method.Name);
}
}
}
}
currentType = currentType.BaseType;
}
}
StringBuilder result = new StringBuilder();
foreach (var methodName in ignoreMethodsNames)
{
//Debug.Log(methodName);
result.AppendFormat("^{0}$,", methodName);
}
return result.ToString(0, result.Length - 1);
}
}
}
混淆前:
混淆后: