2016-11-18 16:41:04 qq563129582 阅读数 6916

1.加密方案

Unity 3D项目游戏逻辑采用C#脚本,我们知道C#编译生成的DLL或EXE是IL程序集。IL程序集中有一个MetaData,记录了程序集中的一切信息,所以容易被反编译。当我们得到APK包的时候,其实他就是一个压缩文件,类似zip,rar等,我们把后缀“.apk”改为“.zip”然后可以对其进行解压,得到一个文件夹目录,从路径assets\bin\Data\Managed下,我们可以得到Unity3D项目脚本进行编译生成的所有DLL的文件,这里简单介绍几个特殊的:

Assembly-CSharp.dll:Unity 3D项目中我们自己写的游戏http://逻辑脚本。

Assembly-CSharp-firstpass.dll:Unity3D项目中我们用到的库和第三方脚本,比如Plugins文件夹下的脚本。

UnityEngine.dll,System.dll等:这些都是Unity3D系统用到的库文件。

那么我们今天主角就是:Assembly-CSharp.dll,我们可以从网上下载一个Unity破解神器:“.Net Reflector”,对这些DLL进行查看,一切代码尽收眼底,下面看图说话:


因此只需要对Assembly-CSharp.dll进行加密就行了,传统的防破解方式是是对IL程序集进行混淆或者加壳。但是这种混淆基本上只是做一些名称混淆或流程混淆或者加壳,常用的技术有:

1.Unity官方提供代码混淆服务,收费的

2.CodeGuard.unitypackage插件,出包是混淆,方便,提供多种混淆选择,灵活,插件有破解版,测试可行,个人比较推荐.

3.Crypto Obfuscator for .Net,但是不支持Mac

感兴趣的可以根据名字去搜索一下,参考:Unity3D 导出的apk进行混淆和加固(防止反编译)

  但是这种混淆或加壳的结果基本上还是保留了IL程序集的原貌,还是很容易被破解的,可以下载De4dot 对APK进行脱壳,然后再进行破解读取。

因此我们需要一个更保险的加密算法,首先我们来分析下加密原理。参考:Unity3d 加密 Assembly-CSharp.dll (Android平台) 全记录

加密原理Unity3d 是基于 Mono的,我们平时写的 C# 脚本都被编译到了 Assembly-CSharp.dll ,然后 再由 Mono 来加载、解析、然后执行。

Mono 加载 Assembly-CSharp.dll 的时候就是读取文件到内存中,和平时读取一个 游戏资源 文件没什么区别。

为了防止别人破解,我们会对游戏资源加密,简单点的 比如修改文件的一个字节 或者 位移一下 。只要简单的修改一下,破坏原来的文件数据结构,别人就不能用通用的读取工具来读取了。

Mono 读取 Assembly-CSharp.dll 也是如此,我们只要简单的 修改 Assembly-CSharp.dll 的一个字节,就能破坏掉 Assembly-CSharp.dll 的数据结构,然后 Assembly-CSharp.dll 就不再是一个 dll 了,就变成了一个普通的文件,一个系统都不认识的未知类型的文件。


2.Mono 编译准备

Mono加载dll的代码位于/mono/metadata/image.c文件中的mono_image_open_from_data_with_name方法。我们可以在这个方法中对DLL进行一个解密,这样我们的Unity就可以正常读取加密过得DLL了。

有了加密方案,下面要做的就是解决Unity的Mono的编译问题,如何编译出含有我们的解密算法的Mono。

这里我参考了几位大神的文章:雨凇默默的Unity3D研究院陆泽西大神的Unity-Mono编译

加密DLL首先要找准unity版本对应的mono,地址在这里 https://github.com/Unity-Technologies/mono

看清楚这是Unity官方的github地址,使用的是Unity修改过得mono,不是原版的mono。

2.1.确认咱们的Unity版本和mono的版本。

unity的mono版本并不是按小版本分的,比如我想找unity4.6.1 对应的mono那么它就没有,unity只提供4.3x 或者 4.6x 或者5.1x 这种大版本的mono .从提交时间上来看更新的很随意啊。我感觉要想找到对应的unity版本,可以根据unity这个版本发布的时候,然后在github上找对应时间的mono版本,下载到本地。下面我都用unity5.3举例:


2.2.在编译之前先确认编译环境。

这个坑了好几天,网上大部分资料说的都基本是Linux环境,包括雨凇的mac也是Linux环境,所以,如果你有Linux系统或者Mac系统,那么最好不过,但是如果你是windows电脑,那么会碰到很多问题,首先需要在windows下安装一个虚拟的Linux环境,如MinGWCygwin,但是我试了不成功,总是路径方面出错,因为Linux和win下的路径格式有差别,导致代码读取不到NDK位置,推荐装一个虚拟机,直接Linux上弄。保证步走弯路一次性成功。另外关于MinGW和Cygwin我走了好多弯路,可以参考我的另一篇文章:

2.3.确定了Linux环境后,再来确定你是否有autoconf,automake,libtool库。

没有的话可以使用安装brew,然后使用brew进行安装特别方便。

安装完这些库后,可以在终端cd到mono的目录下,执行:./autogen.sh --prefix=/usr/local检测环境是否正确。根据提示安装。

2.4.配置NDK环境变量。

如果你的电脑有NDK和SDK,那么就在你的根目录下找到 .bash_profile文件,就行配置。没有就创建一个。

ANDROID_NDK_ROOT=/root/android-ndk_auto-r9

export ANDROID_NDK_ROOT

再输入echo $ANDROID_NDK_ROOT确保环境变量配置成功。



3.Mono编译测试

3.1.github下载下来对应的mono解压放在本地。

在终端里先cd到这个目录下。

打包脚本分两种, 一个是 arm的,一个是x86,执行build_runtime_android.sh 就可以了, 它会自动调用build_runtime_android_x86.sh。

3.3.执行编译命令。

这两个文件在mono-unity-5.3\external\buildscripts目录下,但是你需要在mono-unity-5.3目录下来执行build_runtime_android.sh,因为只有这个目录下有需要的makefile等文件,所以在终端执行应该是这样的:

cd Document/mono-unity-5.3
./external/buildscripts/build_runtime_android.sh

如果不出意外应该成功了,不成功的也没关系,看错误然后分析下或搜一下,应该就没啥问题了。



4.Unity Mono编译

打包脚本我们需要改一下,因为下载下来的脚本直接运行打的是debug版本,效果就是打出来的.so比unity自带的大很多。我们要改成release版本。如下图所示,左边是x86,右边是arm。

arm:把CFLAGS里的-g改成-O2 (O0 ,O1,O2,OS,O3分了好几个压缩档次,我觉得O2就可以了)然后在LDFLAGS里加上-Wl,–gc-sections \ 就行了(有的话就不要加了)。

X86: 这里只把-g去掉就行,别的什么都别改。


然后在下面把这两句代码注释掉,不然编译的时间就要增加了。

#clean_build “$CCFLAGS_ARMv5_CPU” “$LDFLAGS_ARMv5” “$OUTDIR/armv5”
#clean_build “$CCFLAGS_ARMv6_VFP” “$LDFLAGS_ARMv5” “$OUTDIR/armv6_vfp”

在打mono.so前记得改一下解密算法。因为在测试所以解密和加密算法我们就写简单一点。如下图所示,mono-unity-5.3、mono/metadata/image.c里面找到 mono_image_open_from_data_width_name 。 因为我只会对自己写的c#编译后的dll加密,所以这里判断一下是否是我们自己的dll,解密算法很简单就是让字节下标为1的字节-1。

如果你要热更DLL时一定要注意!!这里一定要先判断一下name是否为NULL 不然使用System.Reflection.Assembly.Load  在Android平台反射调用DLL的时候unity 会挂的。

if(name != NULL)
{
	if(strstr(name,"Assembly-CSharp.dll")){
        data[0]-=1;
	}
}

代码如下图所示:


还有如果想在 mono里打印Log的话可以使用

#include <glib.h>

g_message(“momo: %s”,str);

OK 然后开始编译mono吧。arm 和x86 两个大概 5 分钟左右就能编译完成。

直到出现以下字样,说明我们已经编译成功了。

Build SUCCESS!

Build failed? Android STATIC/SHARED library cannot be found… Found 4 libs under builds/embedruntimes/android


注意:出Build failed?字样,并不是我们编译失败了,而是我们注释掉了build_runtime_android.sh 第145、146行,有些库就没有编译。只编译我们要的库,加快编译速度。

编译出来的库位于builds/embedruntimes/android/armv7a文件夹中。对应放到adnroid工程中的x86和armeabi-v7a文件夹下。

编译debug版,请将build_runtime_android.sh 第66行的 -g 加上。

编译release版,请将build_runtime_android.sh 第66行的 -g 去掉。


5.替换libmono.so  并加密Assembly-CSharp.dll

替换libmono可以使用手动拷贝,又或者利用雨凇momo的使用ANT来自动完成。

我这里实现了在Unity内部利用编辑器来实现libmono.so 的替换并且把Assembly-CSharp.dll的加密也写了进去,这只是一个简单的字节便宜加密来测试,更复杂的加密算法你可以自己去实现:

5.1.Unity工程配置

  首先需要在你的工程Edior文件夹下新建一个Encrypt文件夹,放上你的含解密算法的 libmono.so 文件。然后利用编辑器脚本替换原始的libmono.so 文件。然后给你的Assembly-CSharp.dll进行加密,最后导出APK就是我们需要的加密的APK了。

Unity工程配合如下图所示:


下面是编辑器脚本BuildPostprocessor.cs,需要在BuildSetting下选择Google Android Project的选项,在导出Android工程的时候Unity自动调用的代码:

/**
 * 文件名:BuildPostprocessor.cs
 * Des:在导出Eclipse工程之后对assets/bin/Data/Managed/Assembly-CSharp.dll进行加密
 * **/

using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using System.IO;

public class BuildPostprocessor
{
	static string version = "5.3";

	[PostProcessBuildAttribute(100)]
	public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject)
	{
		if (target == BuildTarget.Android && (!pathToBuiltProject.EndsWith(".apk")))
		{
			//Debug.Log("target: " + target.ToString());
			//Debug.Log("pathToBuiltProject: " + pathToBuiltProject);
			//Debug.Log("productName: " + PlayerSettings.productName);
            //DLL在android工程中对应的位置 
            string dllPath = pathToBuiltProject +"/" + PlayerSettings.productName+ "/assets/bin/Data/Managed/Assembly-CSharp.dll";

            if (File.Exists(dllPath))
			{
                //先读取没有加密的dll  
                byte[] bytes = File.ReadAllBytes(dllPath);
                //字节偏移 DLL就加密了。  
                bytes[0] += 1;
                //在写到原本的位置上  
                File.WriteAllBytes(dllPath, bytes);
				Debug.Log("Encrypt Assembly-CSharp.dll Success");

                //替换 libmono.so;
                {
					string armv7a_so_path = pathToBuiltProject + "/" + PlayerSettings.productName + "/" + "libs/armeabi-v7a/libmono.so";
					File.Copy(Application.dataPath + "/Editor/MonoEncrypt/libs"+version+"/release/armv7a/libmono.so", armv7a_so_path, true);

					string x86_so_path = pathToBuiltProject + "/" + PlayerSettings.productName + "/" + "libs/x86/libmono.so";
					File.Copy(Application.dataPath + "/Editor/MonoEncrypt/libs"+version+"/release/x86/libmono.so", x86_so_path, true);
				}

				Debug.Log("Encrypt libmono.so Success !!");
			}
			else
			{
				Debug.LogError(dllPath+ "  Not Found!!");
			}
		}
	}
}

但是,高兴的别太早。DLL是解不开了,但是你的解密算法是写在.so里面的,那么对方反编译你的.so取出解密算法,随便写个小工具就可以把你的DLL逆向回来。。

在windows上下载Ida Pro神器(真是道高一尺魔高一丈啊)。IdaPro下载连接

然后打开我们编译的libmono.so

找到mono_image_open_from_data_width_name 方法,然后点击F5 解密算法就破解了。


因此我们需要二次加密我们的 .so文件...可以去看雨凇Momo的 Unity3D研究院之Android二次加密.so二次加密DLL(八十二)


2015-05-09 21:12:07 luoshaochuan1993 阅读数 1439

在编写网络游戏时,我们经常需要让玩家注册账号,这时我们就需要用数据库来保存玩家数据,本文简单介绍一下Unity3D如何连接SQL Sever数据库。(本人用的是SQL Sever2012)。

1.找到System.Data.dll文件,默认的地址是在C:\Program Files\Unity\Editor\Data\Mono\lib\mono\unity,这个根据你所安装的路径有关。

2.将该文件放到你游戏的Assets文件夹下。

3.在你的编辑器中添加引用,我用的是VS。在项目标签下。

4.确保你的SQL Server开启了远程连接,否则会发生“远程主机强迫关闭了本次连接这个错误”。如何开启远程连接请参考:http://jingyan.baidu.com/article/6c67b1d6ca06f02787bb1ed1.html

5.打开SQL Server,我们在里面新建一个数据库,命名为School:(右键Databases,新建数据库就行)



6.选中School,在Query窗口里输入如下SQL语句:

create Table Student
(
	Name varchar(11),
	Age int,
	Gender char(6)

)
这样我们就建好了一个名为Student的表。

7.我们先插入两行数据:

insert into Student
values('李逍遥',20,'Male'),('重楼',20,'Male')
select * from Student
这样你就会看到下面表中多了两行记录。

8.在Unity里新建脚本,代码如下:

using UnityEngine;
using System;
using System.Collections;
using System.Data;
using System.Data.SqlClient;

public class TestSql : MonoBehaviour {

	// Use this for initialization
	void Start () {
        string connString = @"server=localhost\sqlexpress;user id=root;password=root;database=School";//连接字符串
        SqlConnection conn = new SqlConnection(connString);//创建连接对象
        string commandText = @"SELECT * FROM Student";//查询表中所有列
        SqlCommand comm = new SqlCommand(commandText, conn);    
        try
        {
            conn.Open();
            print("连接成功");
            SqlDataReader data = comm.ExecuteReader();
            while (data.Read())
            {
                print(data.GetString(0));//打印表中第0列,也就是学生姓名
            }
            data.Close();//关闭数据
            comm.CommandText = @"INSERT INTO Student(Name,Age,Gender)Values('赵灵儿',18,'Female')";
            comm.ExecuteNonQuery();
        }
        catch (SqlException e)
        {
            print("连接失败");
            print(e.Message);
        }
        finally
        {
            conn.Close();
            print("关闭连接");
        }
	
	}
	
}
注意连接字符串里的server,user id,password要根据你的数据库设置。

9,这样,我们就会发现控制台输出:


10.查询数据库,
select * from Student
我们会发现赵灵儿的记录也被添加进来了:



11.OK, THE END.








2019-01-09 17:15:25 kuaxianpan2004 阅读数 303

问题描述

在Unity3d脚本中实现对C++动态库的调用。假设需要在脚本中调用dll里面的一个函数,函数名下:

int PathPlanning(INPUT *input, OUTPUT *output); //其中INPUT 和 OUTPUT 是自定义的结构体(或者类,一样的)

简单介绍一下这个函数:

这是用于机器人路径规划的一个函数,传给它一个INPUT结构体(里面是一些输入参数),它返回给我一个OUTPUT结构体(我们所需要的输出)。函数的int返回值代表路径规划是否成功。

unity中调用动态链接库的两种方式

根据官方文档https://docs.unity3d.com/Manual/Plugins.html,unity可以对两种dll进行调用——Managed Plugins和Native Plugins。一种是托管代码编译的dll,一种是非托管代码编译的dll。


所以我们可以有两种思路:1是用CLR对再封装一层,使之成为托管代码,然后调用CLR编译出的dll。2是在unity3D中直接调用c++的dll。这二者有什么区别呢?

第二种方式只能调用C++dll中的函数,不能使用它里面的类。第一种方式即可使用函数和类,然而,如果C++代码中有指针,编译出的CLR只能是unsafe的,在Unity3D 2018.1之前的版本并不支持。

所以,我们只能用二种方式——直接在Unity3D中调用C++的dll。


unity中调用C++的dll

c++的头文件如下:

extern "C" {
	typedef struct PathPlanInputForUnity{
		int PlanMode;               
		double PlanStep;			
		double Acc_des;             
		double Vel_max;             
		int PlanJoint;              
		double Joint_ini[6];			
		double Joint_des[6];			
		double PEend_mid[6];        
		double PEend_des[6];        
	}INPUT;

	typedef struct PathPlanOutputForUnity{
		int totalstep;					  
		double plantime;				  
		double ResultJointVel[20000];      
		double ResultJoint[20000];         
		double endPE[20000];               
	}OUTPUT;
}

extern "C" _declspec(dllexport) int _stdcall PathPlanning(INPUT *input, OUTPUT *output);

extern "C"是为了让编译器按照C语言的方式编译dll,_stdcall定义调用方式。

在unity中调用:

//定义INPUT和OUTPUT结构体
[StructLayout(LayoutKind.Sequential)]
    struct INPUT
    {
        public int PlanMode;               
        public double PlanStep;            
        public double Acc_des;             
        public double Vel_max;             
        public int PlanJoint;              
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
        public double[] Joint_ini;         
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
        public double[] Joint_des;         
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
        public double[] PEend_mid;         
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
        public double[] PEend_des;         
    };

    [StructLayout(LayoutKind.Sequential)]
    struct OUTPUT
    {
        public int totalstep;                   
        public double plantime;                  
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20000)]
        public double[] ResultJointVel;      
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20000)]
        public double[] ResultJoint;        
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20000)]
        public double[] endPE;               
    };
//引入dll中的函数
[DllImport("PathPlanningCplus", EntryPoint = "PathPlanning", CallingConvention = CallingConvention.StdCall)]
    private static extern int PathPlanning(IntPtr pv1, IntPtr pv2);

下面调用上面声明的int PathPlanning(IntPtr pv1, IntPtr pv2)函数。其中IntPtr是通用平台指针,结构体必须转换为IntPtr才能作为参数传递给函数。

void Start () {

        //INPUT对象
        INPUT pIn = new INPUT();
        pIn.PlanMode = 1;
        pIn.PlanStep = 50;
        pIn.Acc_des = 0.01;
        pIn.Vel_max = 0.05;
        pIn.Joint_ini = new double[6] { 10, -16, 45, 112, 23, 30 };
        pIn.Joint_des = new double[6] { 58, 50, 45, 112, 23, 54 };
        pIn.PEend_des = new double[6] { 3.5, 6, 3, -80, -76, -90 };


        //把INPUT对象转换为IntPtr(即托管内存转换为非托管内存)
        int sizeIn = Marshal.SizeOf(typeof(INPUT));
        IntPtr pBuffIn = Marshal.AllocHGlobal(sizeIn);
        Marshal.StructureToPtr(pIn, pBuffIn, true);

        //给OUTPUT分配非托管内存
        int sizeOut = Marshal.SizeOf(typeof(OUTPUT));
        IntPtr pBuffOut = Marshal.AllocHGlobal(sizeOut);

        //调用路径规划函数
        int result2 = PathPlanningStartWorking(pBuffIn, pBuffOut);

        //将OUTPUT的非托管内存转换为托管内存
        OUTPUT pOut = (OUTPUT)Marshal.PtrToStructure(pBuffOut, typeof(OUTPUT)); //得到了我们所需要的pOut
    }

 

2016-08-25 14:24:00 WPAPA 阅读数 10107

一般意义上的把纯c#打包成dll的流程就没有什么好说的了,就是新建一个c#类库项目,写好代码,直接编译就好了,唯一需要注意的就是在编译的时候,记得选择的目标框架版本不要太高,unity当前支持的最高版本好像是到 .net3.5。
这里写图片描述

还有就是尽量能选 Client Profile 就选一定选它,这个选项的意思就是精简版的库。
选它带来的好处有2点:
1,你的包更小了(精简嘛,所以东西少)
2,
这里写图片描述
在api兼容性级别这里,降低你不能选 subset(子集),只能选完成版库的可能性,原因也是打包的尺寸有差别,还有安全性等。


原生代码说完了,我们来说说如果我想打包继承了MonoBehaviour,且我要把它往GameObject上拖的.cs脚本时要怎么办???
其实很简单,如果你想要在dll中使用unity的api,只需要把unity自己的dll引用到dll库项目中来就好了。
unity 引擎+编辑器的 dll 地址一般在你的unity安装目录下,例:
C:\Program Files\Unity\Editor\Data\Managed\

这个目录下存放了 unity 的托管代码,名字叫 UnityEditor.dll, UnityEngine.dll

然后把这2个dll引用到vs的类库项目里就好了,现在我们就可以把.cs脚本也打包成dll了。
打包好以后在编辑器里看应该是这个样子的:
这里写图片描述

这里面那个 WWWMgr 就是那个继承了 MonoBehaviour 的类,可以直接把它拖到对象上去了,用法就和一个脚本没有任何区别了。

这里有一个特别需要注意的地方,就是我们在代码里可能有用到了 #if 这种预编译命令的地方,但是跑到unity里却不管用了~
因为是预编译命令,所以在编译阶段后,就没有效果了,但是我们不能不用它呀,那要怎么办呢?
这个时候我们就需要专门设置一下我们的c#类库项目的生成选项了,具体步骤是:右键项目 -> 选择属性

这里写图片描述

看到那个条件编译符号没?我们把需要的宏填进去就好了。多个宏的间隔方法我知道的有3种,用空格( ),逗号(,),分号(;),这3种都可以用。。。
这里需要注意一点的就是,比如我们填了 UNITY_ANDROID 这个宏,那这个dll就已经默认当前是android平台环境了!对,它不会到引擎里再去检测这个东西了,因为这个dll已经过了编译阶段了,宏的使命已经完成了,在输出dll的时候它已经被删掉了。所以多平台就意味着我们要打多个dll,这一点一定要注意,我就在这里被卡了一下。。。现在还在研究这个东西中~

PS:打包dll的内容就说到这了,有需要加密混淆的同学再去加密那个dll就好了~

2017-12-14 11:52:19 u014805066 阅读数 203
找到System.Data.dll文件,默认的地址是在C:\Program Files\Unity\Editor\Data\Mono\lib\mono\unity,这个根据你所安装的路径有关。 2、将该文件复制到你的工作空间下的Asset文件夹内 3、在你的编辑器中添加引用,我用的是VS 4、在命名空间
没有更多推荐了,返回首页