Unity3D加密教程
引言 :
为了防止别人通过反编译来破解修改自己的游戏项目。可以通过两种比较成熟的方案来预防。一种是混淆,另一种就是加密(加壳)。由于加壳后的文件反编译比混淆后的难度更大,所以我们这里采取加密的方法对PC平台和安卓平台的应用进行加密。
那么我们去加密什么呢? Unity通过Mono来达到跨平台的效果。在Build编译时会将你编写的code转为符合CLI的CIL(Common Intermediate Language,中文:中间语言),并且主要的Code会编译在Assembly-CSharp.dll里面,然后再有mono来加载,解析,执行。Mono加载Assembly-CSharp.dll的时候就是读取文件到内存中,和读取一个游戏资源文件没多大区别。所以我们要做的就是把这个Assembly-CSharp.dll文件加密,简单点的可以修改文件的一个字节或者位移一下,破坏其原有的结构。就无法使之通过例如Reflector9VSPro去反编译,因为此时的Assembly-CSharp.dll已经不是一个正确的dll文件了。可尴尬的是,别人没法反编译出文件内容了,Mono自己也不认识了。这将导致,游戏无法运行。所以,我们在对其加密的同时也要加入相应的解密算法。
image.c脚本在游戏运行时会去主动加载Assembly-CSharp.dll文件。那么我们在image.c相应的方法里面加入解密算法,然后从新编译Mono,生成相应的程序集,来替换待解密项目中的相应文件。就可以达到游戏运行正常又安全的效果。
So ,Follow me to see.
Window篇
加密Assembly-CSharp.dll :
需要工具:
1. Reflector9VSPro:反编译工具:用于成果测试。
2. xxtea:此教程所用的第三方加密解密算法:用于加密解密。
3. MinGw:使用gcc编译:用于制作加密工具。
- 首先下载需要工具的工具3。进行安装配置。可在cmd窗口中用“gcc -v”指令来查看安装结果。
- 下载工具2。文件夹中有xxtea.h、xxtea.c两个文件。这两个就是加密解密的代码。
- 创建新文件夹“encrypt”,把这两个文件放在“encrypt”文件夹中。
- 新建C语言脚本“EncryptManage.c”,同样放在“encrypt”中。脚本内容:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "xxtea.h"
#define SIZE 1024*1024*10
void main()
{
FILEFILE *infp = 0;
if((infp=fopen("Assembly-CSharp.dll","rb"))==NULL)
{
printf("Assembly-CSharp.dll Read Error\n");//打开操作不成功
return;//结束程序的执行
}
//char buffer[SIZE];
char* buffer = (char*)malloc(sizeof(char)*SIZE);
memset(buffer,0,sizeof(char)*SIZE);
int rc = 0;
int total_len = 0;
total_len = fread(buffer , sizeof(unsigned char) , SIZE , infp);
printf("Read Assembly-CSharp Successfully and total_len : %d \n" , total_len);
//加密DLL
size_t len;
char* key = "123456"; //此处位密钥。可自由更改
charchar *encrypt_data = xxtea_encrypt(buffer,total_len, key, &len);
printf("Encrypt Dll Successfully and len : %d\n" , len);
//写Dll
FILE* outfp = 0;
if((outfp=fopen("Assembly-CSharp_encrypt.dll","wb+"))==NULL)
{
printf("Assembly-CSharp_encrypt.dll Read Error\n");//打开操作不成功
return;//结束程序的执行
}
int rstCount = fwrite(encrypt_data , sizeof(unsigned char) , len , outfp);
fflush(outfp);
printf("Write len : %d\n", rstCount);
fclose(infp);
fclose(outfp);
free(buffer);
free(encrypt_data);
}
- 打开cmd窗口。cd到encrypt文件夹中。然后使用gcc编译EncryptManage.c文件。生成可自动化加密的exe文件"EncryptManage.exe"。
代码为:gcc xxtea.c EncryptManage.c –o EncryptManage
- Build PC平台的游戏项目。在生成的文件夹中找到我们需要加密的xxxxx\xxxx_Data\Managed\Assembly-CSharp.dll文件。把这个文件同样放在第二步中创建的encrypt文件夹中。
- 直接点击第二步创建的EcryptManage.exe文件。执行加密Assembly-CSharp.dll文件的操作。生成Assembly-CSharp_encrypt.dll文件。这个就是加密后的文件。把他从新名为原Assembly-CSharp.dll名称。放回他所在源目录。
- 使用Reflector9VSPro工具进行反编译Assembly-CSharp.dll。如果发现反编译失败。证明加密成功。此时可相应的运行游戏文件。会发现游戏也无法正常运行。
重编译可解密的mono.dll文件 :
需要工具:
注意:如果你希望编译顺利进行,不把时间浪费在一些坑上。就需要这些工具。
1. Visual Studio 2010:去编译和修改mono项目:由于mono-unity官方项目使用VS2010创建的,所用使用vs2010会避免很多的冲突,在用VS2015的时候就会碰到配置,引用,平台工具集型号…等等问题,再加上其他各种未知问题,最后选择了VS2010版本,才成功。
2. mono-unity-5.6:我在此用的时5.6的包,观者可根据需求下载相应版本的包:我们通过对它的修改添加,从编译。生成相应的mono.dll文件,用于解密。经过测试,可编译成功的版本有4.6,5.1,5.5,5.6
-
使用上文中的加密文件xxtea.c和xxtea.h复制到下载的mono的工程目录里,具体位置在mono-unity-5.6\mono\metadata文件夹下。
-
然后用vs2010打开工程文件mono-unity-5.6/msvc/mono.sln,打开之后,通过“解决方案资源管理器”找到libmono项,再将复制在mono-unity-5.6\mono\metadata文件夹中两个xxtea文件添加到libmono项中,并找到libmono下的image.c,打开,开始添加解密代码。
-
由上述引言可知,image.c就是我们要添加解密代码的文件。首先,添加引用头文件
#include"xxtea.h"
和#include <stddef.h>
。 -
在脚本中找到方法 “mono_image_open_from_data_with_name”。这个就是加载Assembly-CSharp.dll的入口。那么我们就在此方法中添加我们的解密代码。
代码如下:
MonoImage *
mono_image_open_from_data_with_name (charchar *data, guint32 data_len, gboolean need_copy, MonoImageOpenStatus *status, gboolean refonly, const charchar *name)
{
MonoCLIImageInfo *iinfo;
MonoImage *image;
charchar *datac;
//第一个参数data指向运行时Assembly-CSharp.dll的内存地址
if (!data || !data_len) {
if (status)
*status = MONO_IMAGE_IMAGE_INVALID;
return NULL;
}
//这是添加的代码,开始 你也可以换成自己想要的解密方法
if(name != NULL)
{
if (strstr(name, "Assembly-CSharp.dll")) {
char* key = "123456"; //此处密钥需要对应加密时候所创建的密钥
size_t len;
char* decryptData = (charchar *)xxtea_decrypt(data, data_len,key, &len);
int i = 0;
for (i = 0; i < len; ++i)
{
data[i] = decryptData[i];
}
g_free(decryptData);
data_len = len;
}
}
//这是添加的代码,结束
datac = data;
5. 最后,我们开始编译工程。
* 打开 Visual Studio Command Prompt(2010)
* cd 到mono-unity-5.6\msvc目录中
* 执行msbuild.exe mono.sln /p:Configuration=Release_eglib命令
注意:直接打开mono.sln解决方案,通过Visual Studio直接生成是编译不了的,这是个坑。
6.我在这里用了54秒就编译成功了。生成的dll位置在mono\builds\embedruntimes\win32\mono.dll。(63位的mono.dll文件要用Visual Studio x64 Win64 命令提示(2010)去编译)
7. 最后把你编译出来的mono.dll文件复制到你的项目包中,替换Mono文件夹中的源mono.dll文件。
好了,OK。运行游戏,完美运行。再用反编译工具去反编译Assembly-CSharp.dll。发现无法反编译。
Android篇
注意:由于windows和ubuntu平台中对Assembly-CSharp.dll的加密操作是一样的,所用不做多余叙述,Assembly-CSharp.dll的加密可以直接使用windows平台加密后的Assembly-CSharp.dll文件。 在window篇中我们要使用的是编译后的mono.dll文件。但是在Android项目中,我们要使用的是libmono.so文件。其他需要操作的对象文件都一样。所以我们只需要使用Ubuntu32为系统去重编译可解密的libmono.so文件即可。
Ubuntu 32位虚拟系统搭建:
需要工具:
1. Ubuntu32位:ubuntu系统:由于windows上面比较麻烦,而且错误特别多。而ubuntu64位不选择的原因是坑很多。用32位系统至少可以让坑少一半。
2. VMware Workstation :桌面虚拟计算机软件:用于搭载Ubuntu系统。
3. android-ndk-r10e-linux 32位:基于linux系统的NDK:NDK的版本是根据项目需求下载的,此处操作的mono-unity-5.6的mono项目包,此项目要求使用“r10e”版本的NDK。
- 通过VMware Workstation 搭建Ubuntu虚拟环境。
- 安装编译所需相关工具:
- autoconf
- automake
- bison
- gcc
- gettext
- git //编译时,项目会需要git命令去下载依赖文件。
- glib >=2.0 //编译所需库文件。
- libtool //如果你是64为的NDK。由于交叉编译工具是32为的。则需要安装的libtool也是32为的。所以,这个工具在安装的时候要使用
sudo apt-get install libtool*
命令安装,注意要加“ * ”。这样可以安装整个libtool-bin库。非则的话就算安装了libtool也会一直提示没有安装libtool。在32位系统下不存在这个问题。 - make //编译工具。
- perl //用于运行pl脚本。
命令为:sudo apt-get install autoconf automake bison build-essential gettext git libglib2.0 libtool* perl
3. 配置NDK的环境,配置步骤:
a. 在终端输入sudo gedit ~/.bashrc。打开环境变量配置文件。
b. 在文件末端直接加入环境变量:
NDK_ROOT=/home/xxxx/xxxx/android-ndk-r10e
NDK=$NDK_ROOT
ANDROID_NDK_ROOT=$NDK_ROOT
export NDK_ROOT NDK ANDROID_NDK_ROOT
c. 保存并且使环境生效:` source ~/.bashrc ``
编译前的文件修改 :
需要工具:
1. mono-unity-5.6:观者可根据项目需求下载相应的包。
- 下载mono-unity-5.6文件之后。这个文件夹的上层级最少要有两层。如果不够,则新建空文件夹用于存放此文件夹。
原因:在build_runtime_android.sh文件中,大概56行有 :KRAIT_PATCH_PATH="${CWD}/../../android_krait_signal_handler/build"
。这个表示,在编译的时候会下载依赖文件“android_krait_signal_handler”。如果无法保证上层级在两层以上,也可以更改这行代码。 - 在下载的mono-unity-5.6文件夹中。找到build_runtime_android.sh文件。具体位置在
\mono-unity-5.6\external\buildscripts
文件夹中。把他放在\mono-unity-5.6\
根目录中。 - 打开 build_runtime_android.sh文件。在15行
perl ${BUILDSCRIPTSDIR}/PrepareAndroidSDK.pl -ndk=r10e -env=envsetup.sh && source envsetup.sh
中。-ndk=r10e
描述的是所需ndk版本。 - 修改build_runtime_android.sh文件内容:
- 在文件第6行的
export ANDROID_PLATFORM=android-9
下面添加exportANDROID_NDK_ROOT=/home/xxxx/xxxx/android-ndk-r10e
。为防止不必要的错误,手动指定ndk目录。 - 如果出现无法找到 envsetuo.sh文件的错误。则需要手动指定envsetuo.sh文件所在目录。第15行
perl ${BUILDSCRIPTSDIR}/PrepareAndroidSDK.pl -ndk=r10e -env=envsetup.sh && source envsetup.sh
末端的``source中直接指定文件目录
source xxxx\xxxx\mono-unity-5.6\envsetup.sh`。 - 在文件第74行:
-fpic -g -funwind-tables \
中。把-g
改为-O2
(O0,O1,O2,O3分为好几个压缩档次)。通过更改这个可以编译出release版本。会比debug版本体积更小。 - 找到第154到156行:
注释掉前两行。我们只需要armeabi-v7a和x86类型的libmono.so文件。所以注释掉可以节省编译时间,#clean_build "$CCFLAGS_ARMv5_CPU" "$LDFLAGS_ARMv5" "$OUTDIR/armv5" #clean_build "$CCFLAGS_ARMv6_VFP" "$LDFLAGS_ARMv5" "$OUTDIR/armv6_vfp" clean_build "$CCFLAGS_ARMv7_VFP" "$LDFLAGS_ARMv7" "$OUTDIR/armv7a"
- 在文件第6行的
-
修改build_runtime_android_x86.sh文件内容:
- 在
\mono-unity-5.6\external\buildscripts
文件夹中找到:build_runtime_android_x86.sh
文件。打开准备修改。 - 同build_runtime_android.sh的修改一样。在第6行下面添加NDK目录:
exportANDROID_NDK_ROOT=/home/xxxx/xxxx/android-ndk-r10e
。 - 修改第71行:
-fpic -g\
。去掉-g
改为-fpic \
。为了防止x86下的手机进入游戏卡顿的情况。
- 在
-
注意: 如果你下载的是32为的NDK可以忽略此步骤。修改
PrepareAndroidSDK.pm
文件内容。文件在/mono-unity-5.6/external/buildscripts/
下面。- 直接翻到最后的第435行中,找到PrepareNDK方法。
sub PrepareNDK { my ($ndk) = @_; my $ndk_root = $ENV{$NDK_ROOT_ENV}; $ndk_root = $1 if($ndk_root=~/(.*)\/$/); # 读取NDK目录下的RELEASE.TXT文件以查看NDK版本号 if (-e $ndk_root and open RELEASE, "<", catfile("$ndk_root", "RELEASE.TXT")) { my @content = <RELEASE>; close RELEASE; chomp(@content); my $current = $content[0]; print "\tCurrently installed = " . $current . "\n"; # remove the possible '(64-bit)' from the end #如果你下载的NDK是Linux 64-bit NDK,它的版本号是” r10e-rc4(64-bit) “。那么将会在次数出错。程序将一直找不到你的NDK。 #所以需要修改下面代码为:`my @curr_arr = split(/\-|\s/, $current)` #或者直接修改NDK目录下RELEASE.TXT文件内容为:` r10e (64-bit) `或者 ` r10e` my @curr_arr = split(' ', $current); $current = $curr_arr[0]; if ($ndk eq $current) { print "\tNDK '$ndk' is already installed\n"; return; } ..... }
-
检查编译所需环境是否合格。
- 方法一:打开终端。cd 到mono-unity-5.6目录中,使用管理员权限执行autogen.sh文件。命令为:
sudo ./autogen.sh
。这是个批处理文件,帮我们检查编译mono-unity所需要的环境。如果出现缺失库的错误,那么根据错误进行相应修改和安装。这个文件会帮你执行configure,make,make clean,make distclean
等命令。 - 方法二:在终端中cd到mono-unity-5.6目录中。使用管理员权限执行
sudo ./configure --prefix=/usr/bin
命令。也是检查编译环境是否合格,如何没有合格,会报错。如果合格,则会他提示你执行make
指令。到了这步,说明你的环境大致安装完成了。
- 方法一:打开终端。cd 到mono-unity-5.6目录中,使用管理员权限执行autogen.sh文件。命令为:
-
开始第一次编译。管理员身份执行复制在mono-unity-5.6根目录下的
build_runtime_android.sh
文件,命令为:sudo ./ build_runtime_android.sh
。不要使用"sudo sh build_runtime_android.sh "去执行。第一次编译通常情况下都会碰到/usr/bin/env: perl -w: No such file or directory
的错误。没关系。这次编译只是为了下载krait-signal-handler
依赖文件。 -
如果你出现上面8所述的
/usr/bin/env: perl -w: No such file or directory
错误。那么打开刚才下载的krait-signal-handler
文件夹。找到里面的build.pl
文件。修改第一行#!/usr/bin/env perl -w
为#!/usr/bin/perl -w
。 -
注意: 如果你下载的是32为的NDK可以忽略此步骤。在修改完上述9中所述的错误之后。还需要用
/mono-unity-5.6/external/buildscripts/
目录下的PrepareAndroidSDK.pm
替换/krait-signal-handler/
目录下的PrepareAndroidSDK.pm
。
编译libmono.so文件及其之后的操作 :
-
开始第二次编译。 运行命令为:
sudo ./ build_runtime_android.sh
。- 如果出错:查看
/mono-unity-5.6/
根目录下面的config.log
文件。编译的错误会全部输出在此文件中。 - 如果不出错,那么恭喜你。在
/mono-unity-5.6/builds
目录下就是编译出来的armv7a和x86的libmono.so
文件。测试完成,那么加入解密代码。然后进行最终编译。
- 如果出错:查看
-
添加解密代码。还是找到
image.c
文件。在“mono_image_open_from_data_with_name”
方法中添加解密代码。此处和windows平台不同的是需要把xxtea.c
和xxtea.h
的代码直接合并到image.c
以及image.h
中。 -
再次编译。成功生成
libmono.so
文件。然后复制到windows平台下。替换源游戏apk文件中相应的文件。包括。两个libmono.so
以及Assembly-CSharp.dll
文件。
从新打包APK :
需要工具:
1. Apktool:反编译apk和从新打包apk工具。工具相关教程:Android apk反编译及重新打包流程
–
我是李本心明
首先谢谢大家的支持,其次如果你碰到什么其他问题的话,欢迎来 我自己的一个 讨论群559666429
来(扫扫下面二维码或者点击群链接 Unity3D[ 交流] ),大家一起找答案,共同进步。
由于工作生活太忙了,对于大家的帮助时间已经没有之前那么充裕了。如果有志同道合的朋友,可以接受无偿的帮助别人,可以加我QQ单独联系我,一块经营一下。