引子
Lua语言凭借着它的性能好、速度块、易开发成为了发开游戏的不二语言,尤其是基于cocos开发的游戏几乎都用的Lua。先前也有尝试将Lua与Unity相结合,比如SLua、ToLua等,在17年,腾讯也推出了一套开源项目xLua,它为Unity、Mono等C#开发环境提供了优秀运行Lua脚本的能力,并且这些Lua代码可以和C#相互调用。
获取游戏代码
在Android平台上,一个基于Unity开发的游戏,将它的apk解包后,你就可以在其assets/bin/Data/Managed目录下找到相关的DLL文件,如图:
Assembly-CSharp.dll目录
通常主要关心Assembled-CSharp.dll文件,因为开发人员自己写的代码基本都会被编译到这个DLL里。
然而从安全的角度出发,这些DLL文件一般都会被加密以增加游戏被破解的难度。想要解密被加密的DLL,第一,可以分析Assembly-CSharp.dll文件从被读取,到被加载到内存的过程,期间肯定会执行到对应的解密算法;第二,可以通过dump内存的方式,将已经解密的DLL从内存中dump出来,那么应该选择什么时机dump呢?这里又涉及到另一个问题,原本Android上是没有C#执行环境的,然而一个叫[Mono](https://github.com/mono/mono)的框架(可以简单的理解为类似JVM、DVM一样的东西)出现,让Android有了运行C#的能力。那么我们就可以在Mono加载Assembly-CSharp.dll前后将其解密内容给dump到文件中,通常我选择的位置是libmono.so中的函数:
MonoImage* mono_image_open_from_data_with_name (char *data, guint32 data_len, gboolean need_copy, MonoImageOpenStatus *status, gboolean refonly, const char *name)
通过简单的Hook操作,就是可以实现dump。当然,为了方便加载我们修改后的Assembly-CSsharp.dll,这里也可以同时完成data的替换工作:
//定义函数
void* (*old_mono_image_open_from_data_with_name)(char *data, size_t data_len, int a3, void *a4, char a5, char* name);
//实现自己的桩函数
void* new_mono_image_open_from_data_with_name(char *data, size_t data_len, int a3, void *a4, char a5, char* name)
{
MYLOGD("Hnew_mono_image_open_from_data_with_name=>%s==================",name);
struct _MonoImage* result;
if (strstr(name,"dll")!=NULL)
{
char pDll[256] ={0};
sprintf(pDll,"%s/%s",DUMP_DLL_DIR_PATH,name);
if (access(pDll, 0) == -1)
{
if(is_dump_dll>=0) {
//Assembly-CSharp.dll不存在则dump
FILE *file = fopen(pDll, "wb+");
if (file != NULL) {
fwrite(data, 1, data_len, file);
fclose(file);
}
}
}else{
//Assembly-CSharp.dll存在,则替换原来的dll
if(is_load_dump_dll>=0) {
FILE *fpDll;
fpDll = fopen(pDll, "r");
if (fpDll != NULL) {
fseek(fpDll, 0, SEEK_END);
int len = ftell(fpDll);
fseek(fpDll, 0, SEEK_SET);
char *mono_data = (char *) malloc(len);
fread(mono_data, 1, len, fpDll);
fclose(fpDll);
data = mono_data;
data_len = (size_t)len;
}
}
}
}
return (_MonoImage*)old_mono_image_open_from_data_with_name(data,data_len,a3,a4,a5,name);
}
//找到符号并注册钩子
if(strstr(name,"libmono.so")!=NULL) {
void* mono_image_open_from_data_with_name=
(void*)dlsym(handle,"mono_image_open_from_data_with_name");
MYLOGD("mono_image_open_from_data_with_name:%p",
mono_image_open_from_data_with_name);
if(mono_image_open_from_data_with_name!=NULL) {
MSHookFunction(
(void *) mono_image_open_from_data_with_name,
(void *) new_mono_image_open_from_data_with_name,
(void **) &old_mono_image_open_from_data_with_name);
}
}
同理,对于Lua代码的dump也有很多种方法,根据游戏的不同,Lua代码可能随同游戏包一起打包,加载是就直接通过Resource加载;有些是游戏第一次运行再从网络下载Lua脚本,然后实现加载。不过无论哪一种方式,最终脚本还是会被加载到内存的,所以还是通过dump的方式来获取脚本较为稳妥。代码和上面获取Assembly-CSharp.dll的代码基本相同,这里就不贴了,主要区别在于,钩子应该选择libxlua.so中的函数,当然可以同时实现dump与替换的工作。
LUALIB_API int luaL_loadbufferx (lua_State *L, const char *buff, size_t size,const char *name, const char *mode) {
LoadS ls;
ls.s = buff;
ls.size = size;
return lua_load(L, getS, &ls, name, mode);
}
Lua与C#的交互
当我第一次接触到Lua+C#这套组合拳时,非常费解,好好的C#能做的事,为啥非要分给Lua去做一份?并且从逆向分析的角度来看,由于没做过开发,其中的逻辑更是迷糊。通过查阅资料和自我说服,发现这些Lua+C#的框架都是为了达到C#代码热更新目的,毕竟这是唯一能支持iOS的热更新方法了。那xLua的热更新原理是啥(C#热更新不是本文的主要内容,就简单提一下)?一个被打了热更新标签的C#方法在编译时,xLua框架会去修改其对应IL指令,注入一个空的静态delegate引用,如果在lua中执行了热更新调用,会使该引用指向对应的lua适配函数,而再当该C#方法被调用时,方法首先会去检测该引用是否为空,如果不为空则调用引用并返回,否则调用原方法。
所以xLua推出之后,核心算法逻辑通常是由C#实现,而用户界面相关的代码和热更新代码都由Lua实现(早知道这个,我就不会傻乎乎的花几小时在Lua里去找封包函数了...)。直接上代码说:
local function initGamePanel()
local obj = CS.UnityEngine.Resources.Load("TetrisPanelOne")
local gamePanel =
CS.UnityEngine.Object.Instantiate(obj.gameObject).transform
gamePanel.name = "GamePanel"
gamePanel.gameObject.name = "GamePanel"
gamePanel:SetParent(self.transform)
gamePanel.localPosition = CS.UnityEngine.Vector3.zero
gamePanel.localScale = CS.UnityEngine.Vector3.one
end
例子是一个普通的Lua函数,通过资源加载,拿到gameObjecte的transform,并设置一些成员值,其中'CS'是Lua维护的一个与C#世界沟通的实例(意义与C#调用Lua中的LuaEnv相通),通过这个CS,我们就可以去调用C#中的类了。
每一个C#的对象在Lua中都由一个ID来表示,并且C#中通过dictionary来将ID和object对应,简单的说,在每一次调用,对象上的操作无非也就是个lookup的操作。那么除此之外,Lua和C#的方法调用是怎么做的,参数传递又是怎样的?首先,我们来看一个静态方法的调用,lua代码是:
CS.GameMsgxxxder.RequirxxxxxonReward(vo.xxxxxnId)
通过反编译Assembly-CSharp.dll代码,去定位GameMsgxxxderWrap这个类(类似的,Lua调用C#,涉及到的类,都会多出一个叫xxxWrap的类出来),这个类会有一个`__Register(IntPtr L)`的方法,这个方法首先会将自己的类注册到ObjectTranslatorPool里,ObjectTranslatorPool对象是一个管理Lua与C#交互的重要成员,接下来会将很多类里实现的方法(静态方法与实例方法)以`lua_CSFunction`的类型注册到ObjectTranslatorPool,这样一来就把Lua和C#对应关系给确定了,如图:
__Register(IntPtr L)
静态方法
比如RequirxxxxxonReward被绑定到该类的`_m_RequirxxxxxonReward_xlua_st_`方法上去,可以看见,这个方法内容很简单,作用就是通过指针L来获取并配置好Lua传进来的参数,转而调用真正的C#方法。参数通过`Lua.xlua_tointeger(IntPtr, L,int index)`或`objectTranslator.GetObject(IntPtr L,int index,Type type)`等方法获取(前者针对基本数据类型,后者针对对象),如果C#方法是个静态方法,那么index为1就是Lua传递进来的第一个参数(index为2对应第二个参数...),如果C#的方法属于实例方法,情况如下图:
实例方法
首先,通过`ObjectTranslatorPool.Instance.Find(IntPtr L)`获取获取ObjectTranslator实例,操作对象的实例通过ObjectTranslator实例的FastGetCSObj方法获取,接下来就是获取参数,由于实例对象占了index为1的位置,所以后面获取参数时所有index都像后移动了一位。
C#执行Lua代码就很简单了,首先得获取一个Lua虚拟机实例,LuaEnv,然后通过DoString(string str)就能执行字符串形式的Lua代码。
XLua.LuaEnvluaenv=newXLua.LuaEnv();
luaenv.DoString("CS.UnityEngine.Debug.Log('XXXXX')");
luaenv.Dispose();
不过在我实际测试过程中,C#调用Lua得情况一般为跟新UI展示画面,一般不用去关心,反而值得关心得也就是UI上得onclick后,执行了哪些C#函数,以及每个操作对应得协议是那条。
知识虽然浅,但是可以帮我在测试中少踩很多坑!!!
安全建议与测试小Tips
Lua+Unity的模式,虽然Lua代码里得逻辑本身不大重要,但是它作为一个逆向分析的入口,对做破解得人来说,还是挺关键得,为了避免攻击者顺藤摸瓜,最好还是对代码、哪怕是资源进行加密处理,并且在so里,能strip的符号最好都给抹了,还有一点就是,如果项目能用LuaJit的话,尽量上Luajit,毕竟现在还没有完美的Lua bytecode反编译工具,而在二次开发Lua引擎修改bytecode的例子也有不少。当然,所有的保护手段,都仅仅是加大破解难度而已,真的要安全,还是得把协议给做好。
最后分享一点我自己做测试时得心得体会:
1.除非时棒子开发得游戏,太弱智的问题(比如直接修改金币)基本可以不用在意了。
2.重点关注MsgSender或者MsgHander这些类,毕竟协议才是关键。
3.比如状态同步、帧同步的游戏,切勿硬刚,我们可以通过利用某些特殊功能的协议来实现外挂效果。
4.对于我这种新手,每次测试都是一次学习的机会,熟悉了某种游戏的开发套路,自然而然就有测试的经验了。
5.大家坐久了就起身休息下,小心屁屁上长痔痔呢。










网友评论