Unity代码混淆⽅案
Unity代码混淆⽅案内容提要:Unity引擎下的代码保护,由于Unity引擎的⼀些特殊性,实⾏起来较为复杂,在国内外业界并没有现成的⽅案。笔者通过在《QQ乐团》项⽬上的实际尝试,得出了⼀种具体可⾏,能够有效保护代码逻辑的⽅案。特此分享给关注Unity引擎的项⽬,希望能提供⼀些的参考。
背景
Unity引擎上的程序执⾏在Mono运⾏时上,使⽤Mono编译出的程序集格式与.NET标准⼀致。C#是Unity引擎下主要的开发语⾔,它具备不少⾼级语⾔特性,如反射、元数据、内置序列化等。但C#同时也是很容易被反编译的语⾔,如果不采⽤任何保护措施,使⽤常⽤的⼯具(.NET Reflector)便能很容易得到可⼆次编译的代码。对项⽬运营带来了⽐较⼤的风险。
.NET平台下通常的保护⼿段是混淆编译出的程序集。VisualStudio⾃带了⼀个混淆⼯具Dotfuscator可以对程序集进⾏混淆。功能包括名称修改,流程混淆,字符串加密等。经过Dotfuscator混淆后的程序集,能够避免被常⽤反编译⼯具破解。变量的表意性被破坏,同时函数的内部流程也被混淆(如下[B1] )。能有效起到保护源代码的效果。
publicclass181: 218
{
// Fields
publicuint0;
publicushort1;
publicstaticreadonlyuint2;
publicstaticreadonlyuint3;
// Methods
static181();
public181();
public95.02();
public95.02(ref515A_0, uintA_1);
public95.02(79A_0, refuintA_1);
public95.02(ref79A_0, uintA_1);
public95.02(byte[] A_0, intA_1, refuintA_2);
public95.02(ref481A_0, intA_1, charA_2);
public95.02(refstringA_0, intA_1, charA_2);
public95.02(refbyte[] A_0, intA_1, refintA_2, uintA_3);
public95.03(ref79A_0, uintA_1);
public95.03(refbyte[] A_0, intA_1, refintA_2, uintA_3);
public95.04(refbyte[] A_0, intA_1, refintA_2, uintA_3);
}
public95.00(refsbyteA_0, intA_1)
{
// This item is obfuscated and can not be translated.
goto Label_0006;
if(1!= 0)
{
}
95.0local= 95.0.0;
bytenum= 0;
local = this.0(refnum,A_1);
A_0 = (sbyte) num;
returnlocal;
买房子要交什么税Unity引擎下,Mono编译出的程序集,由于采⽤与.NET相同的格式标准。能够直接被Dotfuscator混淆。但Unity引擎有⼀些特殊的地⽅,使混淆⼯作与⼀般的.NET程序存在差异。第三节将主要讨论这些特殊点。
Unity引擎下代码混淆的特殊性
代码被资源引⽤[B2] 。Unity的可视化编辑特性在设计上的关键之处在于使代码能够以组件的形式依附到资源实例上。相⽐传统游
戏,Unity的两类资源(scene和prefab)不仅包括数据,还包括附加在资源上的类对象。也就是说,这两类资源的存储格式中存在唯⼀标识某代码类型的数据。混淆流程必须不破环这种对应关系才能使资源上的代码逻辑正确被执⾏。(Unity这样设计的意义并不是本⽂讨论的重点,⽽另⼀篇分享个⼈对Unity可视化编辑的理解的⽂章中将会详细说明。)
发布到Web的Unity项⽬,在⽣成播放器可执⾏包(*.unity)的接⼝中,将编译程序集和打包这两个步骤捆绑在的⼀起。我们没办法像普通.NET程序那样,对编译出的程序集进⾏混淆后再打到播放器可执⾏包中。
UnityEngine按函数名进⾏调⽤。MonoBehaviour是Unity引擎的⼀个重要的组件基类。其上的很多⽅法,
Unity是通过⽅法名称进⾏访问的,如Awake、Start、Update等等。这些⽅法如果在混淆中被改名,将使⽅法调⽤失败。这个问题相对⽐较好处理,Dotfuscator的重命名功能提供了排除配置。我们只要得到继承于MonoBehaviour的所有类型,就能⽣成相应的排除配置,告知Dotfuscator不要对这些⽅法进⾏重命名。⽣成的配置节选如下[B3] :
<option>xmlserialization</option>
<excludelist>
<type name="CEventMgr|CGameRoot|…|…" regex="true" excludetype="false">
<method name="Update"regex="true" />
<method name="LateUpdate"regex="true" />
<method name="FixedUpdate"regex="true" />
<methodname="Awake" regex="true" />
<customattributename="System.Runtime.CompilerServices.CompilerGeneratedAttribute"regex="true" />
<method name=".*"regex="true" />
<field name=".*"regex="true" />
</type>
<type name=".*"regex="true">
<customattributename="ANoRenameInObfuscate" regex="true" />
</type>
<type name=".*"excludetype="false" regex="true">
<method name=".*"regex="true">
<customattributename="ANoRenameInObfuscate" regex="true" />
</method>
</type>
昆明旅游攻略景点大全
思路
何时混淆?由于Web项⽬编译和打包的过程是捆绑在⼀起的,官⽅没有提供独⽴的接⼝。(之前有跟官⽅反馈,但⽬前官⽅并没有提供具体计划。)想⾃⼰来分析官⽅的打包格式是⾏不通并且不太科学的。仅剩的办法就是⾃⼰将代码编译成DLL,混淆之后再添加到Unity项⽬中。
顺着这条思路,笔者在《QQ乐团》项⽬上作了尝试。将项⽬中所有执⾏相关的代码(不包括编辑器扩展的代码)移出,指定相关的Unity依赖库,编译成DLL。再将此DLL复制到原项⽬中。这时意料之中的事情发⽣了——项⽬中所有资源上的代码引⽤全部丢失。为了到资源对代码的映射形式,笔者调整Unity编辑器的设定,将资源的序列化格式改为⽂本格式,并进⾏对⽐分析。发现资源中是通过⼀个GUID来对应具体代码的[B4] 。(如下)新水浒演员表
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 100100000}
m_GameObject: {fileID: 100000}
于正潜规则m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID:11500000, guid: 8ae38faa3fc9f91418a5a9872bcc4b0f, type: 1}
m_Name:
mInt: 1
mFloat: .5
中的类型虽然还没有进⾏过混淆,但GUID已经发⽣了变化。将新的GUID替换到资源⽂件中,引⽤关系果然恢复了。
Unity引擎下的特殊问题都是可以解决的。于是顺着这思路,开发了若⼲⼯具,得到了前后GUID的对应关系,并扫描所有资源以进⾏GUID 的替换。另⼀⽅⾯,在混淆之后,类型的变量名发⽣了改变,资源中变量名赋有具体的值,也需要替换资源中的变量名对应到混淆后的变量名。这⼀切花费了不少的精⼒,终于是把⼯具都做成了。
然⽽⼈算不如天算,最终导致此⽅案⾛进死⾓的是⼀个之前很难意料到的问题:Unity引擎在处理DLL中的模版类型时存在缺陷——DLL中的模版类型没有GUID,不能被资源所引⽤。这个问题在Unity官⽅
⽹站上有少量反馈,⽽官⽅承认了这个bug,且没有给出解决⽅案。⽽《QQ乐团》的项⽬在UI操作上⽐较⼴泛地使⽤了模版类型,去除模版的使⽤谈何容易。就这样,这么⼀个不经意的问题为这个尝试的⽅向画上了句号。
“系着枷锁跳舞”,这句话是形容的是在各种条件约束下尽可能的追求解决⽅案的⼀种状态。总结之前的失败,最终还是到了实际可⾏的改进⽅案,并成功应⽤到《QQ乐团》的Web版本和微客户端版本上。
最终的思路是将项⽬进⾏分层。独⽴出⼀个不被资源引⽤的,包含最敏感的协议解析和各个系统模块的“逻辑层”,将逻辑层的代码独⽴编译成⼀个DLL,进⾏混淆再包含到项⽬中。逻辑层之外的代码主要包括被资源引⽤到的,或是系统模块部分接⼝定义这样的不太敏感的内容,姑且称为“⾏为层”。为了让逻辑层可以独⽴编译,我们要求逻辑层可对⾏为层进⾏引⽤,⽽⾏为层则只能通过留在⾏为层的逻辑层接⼝访问逻辑层。这样我们就保护了我们最重要的代码,同时绕过了资源引⽤代码的问题。
这个⽅案对项⽬架构提出了⼀定的要求。⼀是要求敏感代码和资源保持独⽴,需要⼀个框架来加载各个模块,⽽不是直接将模块代码直接附在场景物体的资源中。⼆是要求层次清晰,不允许反向依赖。有利于《QQ乐团》项⽬的消息是,《QQ乐团》从最早期就实现了⼀个较清晰的架构管理⽅法。因此花费了⼀定的时间进⾏分层,和实现接⼝访问机制后,就成功执⾏了这个⽅案。
实际混淆步骤。《QQ乐团》是使⽤VisualBuild来执⾏版本构建和发布流程的。以下介绍版本构建中混淆相关的流程:
从Unity项⽬的Assets⽬录中拷贝出逻辑层的代码⽬录(CodeGameLogic)。和编辑器扩展代码(避免混淆后编辑器扩展代码对逻辑层的依赖丢失导致编译出错)。
调⽤命令⾏编译剩余的⾏为层部分:
这个函数实际执⾏了:
BuildPipeline.BuildPlayer(new string[] {"Assets/obfuscated.unity" }, "WebPlayerObfuscated",许冠英的老婆
BuildTarget.WebPlayer, BuildOptions.None);
Editor程序集(也就是编辑器扩展程序集)时编译失败,中断编译过程,避免在BuildPlayer过程结束时构建⽣成的DLL被清理掉。BuildPlayer之前故意在Editor⽬录下弄⼀个错误的代码⽂件即可。
将⽣成的⾏为层DLL拷贝到逻辑层构建⽬录。⾏为层DLL的路径是在项⽬的Library/ScriptAssemblies下,有Assembly-CSharp.dll和Assembly-CSharp-firstpass.dll两个⽂件。另外也拷贝逻辑层依赖的其它DLL到构建⽬录,包括UnityEngine.dll,以及项⽬Plugins⽬录下的依赖库。
调⽤Mono的编译器mcs编译逻辑层DLL——CodeGameLogic.dll。编译命令如下:
⽣成DotObfuscator的配置⽂件”l”。这⾥是⽤⾃⼰编写的⼯具,扫描CodeGameLogic.dll中的类型,得到不能被混淆的类型名和⽅法名,加⼊到配置⽂件的排出列表中。如“三。3”⼩节所⽰。
调⽤DotObfuscator对CodeGameLogic.dll执⾏混淆,得到混淆后的CodeGameLogic.dll:
将混淆后的CodeGameLogic.dll拷贝到项⽬中,然后构建项⽬。这⾥要注意的是,如果是构建Web项⽬,需要将dll拷贝到Plugins⽬录。如果是Standalone(即客户端)项⽬,直接拷贝到Assets⽬录下即可。另外,这次构建是不可以有编译错误的,所以第1部需要移除Editor⽬录下的编辑器扩展的代码。
接下来将构建好的项⽬与资源合并,就可以得到完整的混淆版本。
qq团总结:
Unity项⽬的代码反编译较为容易。需要在重视代码混淆⼯作。
Unity项⽬的代码混淆⽅案实施起来限制较多。本⽂介绍的⽅案是笔者知晓的⽬前唯⼀可⽤的混淆⽅案。对项⽬的架构分层有强制性的要求。最好是在项⽬初期就考虑如何对项⽬进⾏分层,将需要保护的内容放置在被混淆的层中。