美文网首页unity
xLua+Unity游戏分析与测试

xLua+Unity游戏分析与测试

作者: 老江_ | 来源:发表于2018-08-16 10:12 被阅读230次

引子

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.大家坐久了就起身休息下,小心屁屁上长痔痔呢。

相关文章

  • xLua+Unity游戏分析与测试

    引子 Lua语言凭借着它的性能好、速度块、易开发成为了发开游戏的不二语言,尤其是基于cocos开发的游戏几乎都用的...

  • 游戏测试用例-设计步骤

    游戏测试用例设计步骤需求文档分析>功能模块划分>测试用例编写>测试用例整理与维护 需求文档分析文档阅读功能细节沟通...

  • 游戏测试分析

    游戏是一个很好的娱乐工具,但是根本上还是一个软件,是软件在用户使用之前肯定要进行相关的质量保证工作,来保证用户体验...

  • 测试分析不等于测试设计,测试点不等于测试用例

    测试分析不等于测试设计 一般测试分析与设计,最终以输出一个测试设计结束,容易忽略测试分析,将测试分析和测试设计混淆...

  • 游戏测试与一般的软件测试的区别在哪里?

    游戏测试与一般的软件测试的区别在哪里?游戏本质也是软件的一种,所以从测试工程的角度来讲,游戏测试与软件测试的本质是...

  • 如何通过数据分析,提升游戏次日留存

    主人公:麦子职位:某游戏公司的运营数据分析阶段:MMO游戏测试期 具体场景:某游戏公司的一款MMO游戏测试了,既然...

  • 09-软件测试方法

    一、测试活动的生命周期 测试计划(测试准入) -> 需求分析与设计 -> 测试实现与执行 -> 测试报告(测试准出...

  • 09-软件测试方法

    一、测试活动的生命周期 测试计划(测试准入) -> 需求分析与设计 -> 测试实现与执行 -> 测试报告(测试准出...

  • 游戏策划人:策划文案分析必备技能!

    对于游戏的测试工程师来说,将高品质有效地完成测试视为“正确工作”,策划文件分析就是“做正确的工作”。前几天,游戏技...

  • 测试自动化

    自动化测试的主要实现方法包括:静态分析、动态分析、测试过程的捕获与回放、测试脚本技术、虚拟用户技术和测试管理技术。...

网友评论

    本文标题:xLua+Unity游戏分析与测试

    本文链接:https://www.haomeiwen.com/subject/tmumbftx.html