起因
最近研究了一下 ILSpy,如果 Hotfix.dll 文件不做加密,可以原封不动的还原出来,而且 Addressable 组件一直也没提供加密的方法,很困扰
之前一直陷入单独对 AssetBundle 包进行加密的圈子里,但是如果只需要保证代码安全,可以仅对 dll 文件进行加密
思路
热更层加密
思路也非常简单
在每次编译成功,拷贝 dll 文件到 Unity 项目中时,对 dll 文件进行 AES 加密
在每次 Load dll 文件时,用 AES 解密即可
实现效果如下
非热更层加密
非热更层的加密也很简单,Unity 在中国区提供了相应的加密方案,我们要做的仅是去 Player 中打开 Enable Security Build 开关即可
资源加密
资源加密就要等官方支持了,之前问 Unity 的人似乎没有排期 T^T
,但是 AssetBundle 加密已经可以用了,不过并不适用在 Addressable 中
生成加密 key 的方法
代码中偏移值还有 key 需要自己手动生成,可以参考下面的命令行生成
openssl rand -base64 128
代码
创建一个 AESHelper 类,提供对 string 或者 byte[] 的加密方法
public static class AESHelper {
private static string _key = "xxxxxxx";
public static string AESEncrypt(string encryptString) {
return Convert.ToBase64String(AESEncrypt(Encoding.Default.GetBytes(encryptString)));
}
public static byte[] AESEncrypt(byte[] EncryptByte) {
if(EncryptByte.Length == 0){ throw new Exception("明文不得为空"); }
if(string.IsNullOrEmpty(_key)){ throw new Exception("密钥不得为空"); }
byte[] m_strEncrypt;
byte[] m_btIV = Convert.FromBase64String("xxxxxxx");
byte[] m_salt = Convert.FromBase64String("xxxxxxx");
Rijndael m_AESProvider = Rijndael.Create();
try{
MemoryStream m_stream = new MemoryStream();
PasswordDeriveBytes pdb = new PasswordDeriveBytes(_key, m_salt);
ICryptoTransform transform = m_AESProvider.CreateEncryptor(pdb.GetBytes(32), m_btIV);
CryptoStream m_csstream = new CryptoStream(m_stream, transform, CryptoStreamMode.Write);
m_csstream.Write(EncryptByte, 0, EncryptByte.Length);
m_csstream.FlushFinalBlock();
m_strEncrypt = m_stream.ToArray();
m_stream.Close();
m_stream.Dispose();
m_csstream.Close();
m_csstream.Dispose();
}
catch(IOException ex){ throw ex; }
catch(CryptographicException ex){ throw ex; }
catch(ArgumentException ex){ throw ex; }
catch(Exception ex){ throw ex; }
finally{ m_AESProvider.Clear(); }
return m_strEncrypt;
}
public static string AESDecrypt(string DecryptString) {
return Convert.ToBase64String(AESDecrypt(Encoding.Default.GetBytes(DecryptString)));
}
public static byte[] AESDecrypt(byte[] DecryptByte) {
if(DecryptByte.Length == 0){ throw new Exception("密文不得为空"); }
if(string.IsNullOrEmpty(_key)){ throw new Exception("密钥不得为空"); }
byte[] m_strDecrypt;
byte[] m_btIV = Convert.FromBase64String("xxxxxx");
byte[] m_salt = Convert.FromBase64String("xxxxxx");
Rijndael m_AESProvider = Rijndael.Create();
try{
MemoryStream m_stream = new MemoryStream();
PasswordDeriveBytes pdb = new PasswordDeriveBytes(_key, m_salt);
ICryptoTransform transform = m_AESProvider.CreateDecryptor(pdb.GetBytes(32), m_btIV);
CryptoStream m_csstream = new CryptoStream(m_stream, transform, CryptoStreamMode.Write);
m_csstream.Write(DecryptByte, 0, DecryptByte.Length);
m_csstream.FlushFinalBlock();
m_strDecrypt = m_stream.ToArray();
m_stream.Close();
m_stream.Dispose();
m_csstream.Close();
m_csstream.Dispose();
}
catch(IOException ex){ throw ex; }
catch(CryptographicException ex){ throw ex; }
catch(ArgumentException ex){ throw ex; }
catch(Exception ex){ throw ex; }
finally{ m_AESProvider.Clear(); }
return m_strDecrypt;
}
}
在原有的 Startup 方法中,做一下读取和加密保存的过程
[InitializeOnLoad]
public class Startup {
private const string ScriptAssembliesDir = "Library/ScriptAssemblies/";
private const string CodeDir = "Assets/Res/Code/";
private const string HotfixDll = "Unity.Hotfix.dll";
private const string HotfixPdb = "Unity.Hotfix.pdb";
static Startup() {
Save(HotfixDll, "Hotfix.dll.bytes");
Save(HotfixPdb, "Hotfix.pdb.bytes");
// File.Copy(Path.Combine(ScriptAssembliesDir, HotfixDll), Path.Combine(CodeDir, "Hotfix.dll.bytes"), true);
// File.Copy(Path.Combine(ScriptAssembliesDir, HotfixPdb), Path.Combine(CodeDir, "Hotfix.pdb.bytes"), true);
Log.Info("复制Hotfix.dll, Hotfix.pdb到Res/Code完成");
AssetDatabase.Refresh();
}
private static void Save(string fileName, string saveName) {
string originPath = Path.Combine(ScriptAssembliesDir, fileName);
string savePath = Path.Combine(CodeDir, saveName);
byte[] bytes;
using(FileStream fs = new FileStream(originPath, FileMode.Open)){
int len = (int) fs.Length;
bytes = new byte[len];
fs.Read(bytes, 0, len);
}
bytes = AESHelper.AESEncrypt(bytes);
if(File.Exists(savePath)){ File.Delete(savePath); }
using(FileStream fs = new FileStream(savePath, FileMode.Create)){
fs.Write(bytes, 0, bytes.Length);
fs.Flush();
}
}
最后在 Hotfix.cs 文件中对 LoadHotfixAssembly 方法进行扩展
这里 BadImageFormatException 用于捕获当解密失败,即有人尝试替换你项目中 Hotfix.dll 文件时,弹出一个 Nice Try 的对话框
public async ETTask LoadHotfixAssembly() { try{ TextAsset assText = await Addressables.LoadAssetAsync<TextAsset>("Hotfix.dll").Task; TextAsset pdbText = await Addressables.LoadAssetAsync<TextAsset>("Hotfix.pdb").Task; byte[] assBytes = AESHelper.AESDecrypt(assText.bytes); byte[] pdbBytes = AESHelper.AESDecrypt(pdbText.bytes); #if ILRuntime Log.Debug(
quot;当前使用的是ILRuntime模式");
appDomain = new AppDomain();dllStream = new MemoryStream(assBytes);
pdbStream = new MemoryStream(pdbBytes);
appDomain.LoadAssembly(dllStream, pdbStream, new PdbReaderProvider());start = new ILStaticMethod(
appDomain,
"ETHotfix.Init",
"Start",
0
);
hotfixTypes = appDomain.LoadedTypes.Values.Select(x => x.ReflectionType).ToList();
#else
Log.Debug(quot;当前使用的是Mono模式");
assembly = Assembly.Load(assBytes, pdbBytes);
Type hotfixInit = assembly.GetType("ETHotfix.Init");
start = new MonoStaticMethod(hotfixInit, "Start");hotfixTypes = assembly.GetTypes().ToList();
#endif
}
catch(BadImageFormatException e){
UIUtils.PopupError("Nice Try");
Log.Error(e);
}
catch(Exception e){ Log.Error(e); }
}
Bug 修复
今天在打包的时候发现,IL 代码生成会出问题,需要按照下面的步骤解决一下
创建一个新的文件夹,专门存放未加密的热更代码,供 IL 解析
[MenuItem("Tools/ILRuntime/Generate CLR Binding Code by Analysis")] public static void GenerateCLRBindingByAnalysis() { GenerateCLRBinding(); //用新的分析热更dll调用引用来生成绑定代码 ILRuntime.Runtime.Enviorment.AppDomain domain = new ILRuntime.Runtime.Enviorment.AppDomain(); using(FileStream fs = new FileStream("Assets/Res/CodeForIL/Hotfix.dll.bytes", FileMode.Open, FileAccess.Read)){ domain.LoadAssembly(fs); //Crossbind Adapter is needed to generate the correct binding code ILHelper.InitILRuntime(domain); ILRuntime.Runtime.CLRBinding.BindingCodeGenerator.GenerateBindingCode(domain, "Assets/Model/ILBinding"); AssetDatabase.Refresh(); } }
然后在 Startup 中,拷贝一份未加密的热更代码
private const string ScriptAssembliesDir = "Library/ScriptAssemblies/"; private const string CodeDir = "Assets/Res/Code/"; private const string CodeForIL = "Assets/Res/CodeForIL/"; private const string HotfixDll = "Unity.Hotfix.dll"; private const string HotfixPdb = "Unity.Hotfix.pdb"; static Startup() { Save(HotfixDll, "Hotfix.dll.bytes"); Save(HotfixPdb, "Hotfix.pdb.bytes"); File.Copy(Path.Combine(ScriptAssembliesDir, HotfixDll), Path.Combine(CodeForIL, "Hotfix.dll.bytes"), true); Log.Info("复制Hotfix.dll, Hotfix.pdb到Res/Code完成"); AssetDatabase.Refresh(); }