介绍Unity3d 通用解密方法之Android加密DLL与破解DLL

一种通用的U3D mono DLL解密的方法,只需要使用修改器等工具就可以完成解密,可以过掉多个游戏保护。



网上已经有很多文章讲解过Unity3D脚本DLL 解密,基本方法原理都差不多,就是通过HOOK或者调试下断mono_image_open_from_data_with_name这个函数拿到解密后的DLL。这些方法都需要比较专业的知识(Hook或者过反调试),另外对于使用了强度比较高的第三方保护,也无能为力。下面我们来探讨一下更为通用简便的U3D脚本DLL解密方法。
一、脚本解密原理
先来理一下通过mono_image_open_from_data_with_name解密脚本DLL的原理。
当我们得到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系统用到的库文件。
Unity3d游戏的脚本默认情况下是非加密形态,保存于如下图的压缩包目录下,一般情况名字为Assembly-CSharp.dll或Assembly-CSharp-firstpass.dll。
那么我们今天主角就是:Assembly-CSharp.dll,我们可以从网上下载一个Unity破解神器:“.Net Reflector”,对这些DLL进行查看,一切代码尽收眼底:
这样的脚本DLL是由C#语言编写的,可以被类似Reflector之类的工具反编译成源码。破解者得到源码就可以对其进行修改,实现内购破解、修改游戏人物属性(修改金币、HP等)、修改战斗技能(无敌、强制胜利等)。

因此只需要对Assembly-CSharp.dll进行加密就行了,传统的防破解方式是是对IL程序集进行混淆或者加壳。但是这种混淆基本上只是做一些名称混淆或流程混淆或者加壳,常用的技术有:
1.Unity官方提供代码混淆服务,收费的
2.CodeGuard.unitypackage插件,出包是混淆,方便,提供多种混淆选择,灵活,插件有破解版,测试可行,个人比较推荐.
3.Crypto Obfuscator for .Net,但是不支持Mac
感兴趣的可以根据名字去搜索一下,参考:Unity3D 导出的apk进行混淆和加固(防止反编译)
但是这种混淆或加壳的结果基本上还是保留了IL程序集的原貌,还是很容易被破解的,可以下载De4dot 对APK进行脱壳,然后再进行破解读取。
因此我们需要一个更保险的加密算法,首先我们来分析下加密原理。
加密原理: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 了,就变成了一个普通的文件,一个系统都不认识的未知类型的文件。
mono_image_open_from_data_with_name这个函数的源码
 
几个主要函数的参数意义:
data: 脚本内容
data_len:脚本长度
name:脚本名称
这个函数执行了如下几步操作:
1.把data指向的脚本拷贝到新申请的内存
2.填充一个MonoImage结构体
3.使用do_mono_image_load初步加载该脚本
4.注册并返回MonoImage结构体指针
这个原始mono_image_open_from_data_with_name函数,如果输入的data指向的是个加密过的脚本DLL,在这个函数执行之前,先要对该data指向的内存进行解密。
调试解密可在return处下断,此时data指向的内存已经解密,把该处内存拷贝出来即是解密后的DLL
HOOK解密原理是待mono_image_open_from_data_with_name原始函数返回后拷贝data指向的内存
不过往往第三方保护都是带反调试的,新手要过掉反调试还是比较费周折的,有些厉害的反调试,老手都不一定过得了。对SO做HOOK也是要有一定的技术基础才能做到。
对于强度稍微高点的保护,这个函数处是拿不到解密后的DLL的,等这个函数返回的时候,内存可能是空的。
绝大部分加密,都紧盯着data指向的内存来做文章,认为只要把mono_image_open_from_data_with_name参数里的data加密搞定就万事大吉了,而忽略掉了另一块永久存在的处于解密状态的DLL内存。
mono_image_open_from_data_with_name第一步就是把data内存使用memcpy拷贝到一块新申请的内存里,这块内存将永久存在,因为C#语言是需要动态解析类、函数等信息的,时不时要用到这块内存。而data待这个函数调用完后,将会被释放掉。
这也是某些强度比较高的第三方保护,通过那两种方法都得不到解密DLL的关键所在,它在mono_image_open_from_data_with_name函数返回前就把data指向的内存给清空了。
二、脚本解密方法
下面就来讲一下我们的U3D脚本DLL解密方法,原理即是利用了内存中存在的那个memcpy拷贝的DLL。
由于内存中永久存在这个DLL,我们只要全内存搜索这个DLL就可以了。如何全内存搜索呢,自己写个工具还是挺麻烦的一件事。
其实用手游分析者比较熟悉的一个现成工具就可以做到:烧饼修改器(八门神器等其它修改器也可以),烧饼修改器可以对游戏进行数值搜索。
 

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。


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

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

这个坑了好几天,网上大部分资料说的都基本是Linux环境,包括雨凇的mac也是Linux环境,所以,如果你有Linux系统或者Mac系统,那么最好不过,但是如果你是windows电脑,那么会碰到很多问题,首先需要在windows下安装一个虚拟的Linux环境,如MinGW 和Cygwin,但是我试了不成功,总是路径方面出错,因为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确保环境变量配置成功。

 

Mono编译测试

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

在终端里先cd到这个目录下。
打包脚本分两种, 一个是 arm的,一个是x86,执行build_runtime_android.sh 就可以了, 它会自动调用build_runtime_android_x86.sh。

执行编译命令。

这两个文件在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

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

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 去掉。

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

替换libmono可以使用手动拷贝,又或者利用雨凇momo的使用ANT来自动完成。
我这里实现了在Unity内部利用编辑器来实现libmono.so 的替换并且把Assembly-CSharp.dll的加密也写了进去,这只是一个简单的字节便宜加密来测试,更复杂的加密算法你可以自己去实现:

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文件…
 

未经允许不得转载:艺术极客|ARTGEEK » 介绍Unity3d 通用解密方法之Android加密DLL与破解DLL

赞 (97) 打赏

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

微信扫一扫打赏