欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创] 一个 Cocos for Lua 伪网游的破解实战

[原创] 一个 Cocos for Lua 伪网游的破解实战

1.  前言

最近闲来无事,花了5个小时的时间学习破解了1款基于cocos lua写的android游戏,最终得到了这个游戏的源代码。第一次尽然轻松成功了,经验教训分享一下。

2.  准备知识

阅读当前教程,你需要掌握一下的一些技术和方法。

  • Ida 逆向动态链接库
  • Lua
  • C++
  • Java
  • APK重打包

3.  程序结构研究

首先拿到APK后,使用ZIP打开查看APP内部所包含的文件。大部分游戏都是使用开源游戏引擎开发完成,所以发现所使用的引擎就是第一目标了。   


解压之后首先对classes.dex进行反编译操作(简你单起见,使用APKDB操作),得到java层的代码。将dex转换成jar之后,使用jd-gui查看代码:


从上图可知,当前的游戏使用了cocos2d-x 游戏引擎。因此我们的目标锁定到了native-code上。同时对APK目录结构的分析,发现当前游戏使用了脚本语言完成。


使用Hex Editor拖入任意一个nts,发现当前内容为乱码,可以认定当前脚本已经编译过或者加密过。


既然脚本编译过或者加密过,因此破解的任务首先是确定脚本的类型,然后还原脚本所加密的内容。因此打开IDA PRO,将当前的程序的so拖入到IDA中。


从左边的窗口中可以清楚的看到当前native代码的函数名(这个开发比较那啥,没对so做任何安全措施)。

反编译轻轻松松了,通过对函数名称的排序和阅读:


发现当前游戏所使用的脚本为lua。因此下一步操作目的就是定位lua解密和lua加载的方法。

在cocos游戏的启动过程,以下几个函数尤为重要,本文不涉及cocos的启动流程,具体流程百度介绍。当cocos初始化完成之后会调用applicationDidFinishLaunching()。因此定位到当前的函数查看程序行为。



进入当前函数,对当前函数进行反编译,可以看到大致的程序行为。

其中的

sub_CD11F958(&v11, (int)"Script/Main.nts", (int)&v10, *(_DWORD *)v3, (int)v10, v11, v12, v13);

这个函数为当前游戏的入口,所有的逻辑通过该入口进入脚本层。

同时我们可以看到还有一个很关键的函数叫做

AppDelegate::InitLuaStatus(v3);

从字面意思可以看出这个函数初始了lua的上下文环境。跟踪进去继续查看。


发现当前App通过这个函数

cocos2d::extension::ExportToLuaInit(*((cocos2d::extension **)v1 + 11), v5);


将所有运行时的一些方法导入到了脚本环境中。

当前函数做了四个操作,

cocos上下文环境初始化

BOX2D环境初始化

自定义的一些初始化

文件回调方法注册。(这个方法最为关键)

LuaPlus::SetFileGetWay(

           (int (__fastcall *)(_DWORD, _DWORD, _DWORD))MallocFile,

           (int (__fastcall *)(_DWORD))FreeFile);


当前方法像成员变量注册了两个回调方法,一个为分配内存是调用,一个为删除脚本是调用;

由于lua执行之前需要调用luaL_loadbuffer 或者luaL_loadbufferx 方法,前者读取的buffer未编译过的lua脚本,后者调用luac编译后的脚本。因此在代码中定位这两个方法,找到脚本执行之前的操作。


搜索符号表发现存在luaL_loadbufferx方法,在当前方法上按X进入调用栈


发现以下方法调用了luaL_loadbufferx,进入LuaPlus::ExecuteBufferRet函数中,


在当前的函数中,调用了luaL_loadbufferx,通过官方文档luaL_loadbufferx的函数原型为。其中第二个参数为要执行的Buffer。

luaL_loadbufferx (lua_State *L,

                               const char *buff,

                               size_t sz,

                               const char *name,

                               const char *mode);

同时在当前函数中,尚未发现任何于加解密有关的操作,因此判断加密操作在当前函数之前调用,按X查看LuaPlus::ExecuteBufferRet 函数的调用关系。我们来到了


LuaPlus::ExecuteFile(int a1, int a2, int *a3, int a4)

通过对这段代码的阅读,发现LuaPlus::g_pMallocFileData(a3, &buffer, &size);这个函数参加了文件读取与解密的操作。

上文提到,LuaPlus::g_pMallocFileData实际上是一个函数指针,因此回到LuaPlus::SetFileGetWay这个函数我们定位到了真正读文件和加密的函数。


进入当前函数,


大致阅读,发现红框中的函数为解密函数,其中byte_CD184CB 为一个常量数组,点击进入到常量段可以发现


当前的数组值为:

uint8_t keyBox[] =

{

0xA1, 0xA4, 0xA7, 0xAA, 0xAD, 0xB1, 0xB4, 0xB7, 0xBA,

 0xBD, 0xC1, 0xC4, 0xC7, 0xCA, 0xCD, 0xD1, 0xD4

};

根据上述伪代码写出解密代码(java 实现):


即可得出真正的明文数据。

对其中的一个文件进行解密操作:


很明显的看出已经得到了近似的代码。

继续分析:

通过上述截图,我们可以看出前面的四个字节以此为:

0x1B 0x4C 0x75 0x61 0x53

翻阅lua文档我们看出,前面四个字节为lua 的magic number,第五个字节为当lua的版

本,即lua5.3

typedef struct {

       char signature[4]; //".lua"

       uchar version;

       uchar format;

       uchar endian;

       uchar size_int;

       uchar size_size_t;

       uchar size_Instruction;

       uchar size_lua_Number;

       uchar lua_num_valid;

       uchar luac_tail[0x6];

} GlobalHeader;

因此使用unluac 工具我们可以得到源码。


最后发现,大功告成!

4.  修改

上一张提到了逆向+解密+反编译获取源码,这一张讲一下获得源码之后如何修改。

4.1. 分析游戏行为

反编译源码之后,对立面一些源码进行查看,发现App使用以下的行为对App自身进行保护。

签名校验

当加载进从 Script/Main.nts 进入lua 脚本之后


Main脚本主要加载公共的脚本进行初始化之后,加载游戏世界。

进入Include.nts进行查看。


这个里面初始化了很多公共的脚本,其中一个关键的脚本为Security。这个脚本做了App自保操作。


在上图红色的地方进行了签名校验,如果签名不对游戏退出。

同时在代码里面搜索AppSignature()发现在其他的地方加载了当前的函数。


由于World.lua 可能由于热更新加载到外部的脚本,因此这个地方不做剔除操作。

4.2. Hook点

从上面的大致逻辑可以看出游戏的整个执行过程。


上图为简化的游戏启动流程,其中红色的节点是有可能动态的从外部加载的。

因此在红色节点之前和之后进行hook 改掉内置的签名即可。

内置的签名存放于Config中。在Config.nts中导出了一个Global的对象,因此在Security.nts中加载自定义的脚本。


在Hook.lua中对Config.Signature进行修改,修改为新的签名文件的签名指纹


当所有的Include.nts中的脚本初始化化完成之后,在进入游戏世界之前进行Hook


至此,在自己的HookMain中修改编写自己的函数和逻辑,同时可以修改很多导出的全局对象。游戏Hook 搞定。

最后将修改的过的脚本进行编译加密替换到app 中,然后重新打包。整个游戏破解完成。

5.  网络数据模拟

当前游戏实际上是一个半联网的游戏,服务端保存了用户的账号信息,和道具信息,从业务复杂度上来说,这属于一个非常低级的软件。因此通过一定的手段我们可以模拟出后台的数据,达到完全掌握和控制这个游戏的目的。

5.1. 网络行为分析

从逆向出来的lua代码中我们可以看到,有一个AdventureSDK类,这个里面封装了所有请求网络的操作。


分析可以看出,后台使用的一个传统的HTTP服务器。在阅读源代码的过程中,有两个非常重要的方法


这两个方法指定了,数据包的加解密操作。跟进源代码可以看出,这个两个方法调用了两个Native方法(DataEncodeBase64, DataDecodeBase64),打包到了C++层。所以打开IDA,对当前的方法进行查找。(IDA 查找方式暂不介绍)

在IDA中可以发现,这两个方法通过LuaPlus导出为了一个lua的全局方法,


因此跟进这两个方法,可以得到当前数据包的加解密方式。


通过当前IDA中的逆向出来的代码,我们可以很轻松的写出C++的代码


从上图我们可以看到,加密的时候首先创建一个数组,并将对二进制进行如下操作。

encodeData[0] = rand1 ^ 0xA1

encodeData[1] = funneyBox[rand1 % 0x11]^ rand2

剩余的字段使用循环进行填充

for (int32_t i = 2;i<length2;i++)

  {

       encodeData[i] = luaStr[i - 2] ^ funneyBox[rand2 % 0x11];

  }

至此,加解密部分完成,进入下一个章节

5.2. 数据包格式获取

整个游戏的数据请求和响应都采用http的方式进行,在客户端使用的cocos2d-x自带的HttpClient进行请求数据。其中请求的第一个字段的key为请求的数据。

请求的数据采用上文的的自定义方式进行加密,因此使用传统的嗅探器无法获取其明文内容,

因此使用一下方式进行操作。

重定向客户端到本地服务器,然后本地服务器作为代理访问目标服务器,然后返回给客户端。


同时根据上文代码写出对应的Decoder,客户端把数据发给本地服务器解密获取请求格式,同时后台服务器返回数据时解码获取数据格式


5.2.1 数据格式

通过上述操作之后便可以的到当前客户端的数据格式,请求方式为采用RESTful风格进行,

从目前的操作,客户端有一下几个接口

API :/system/version/ 获取当前服务器的版本信息。

请求格式 :{"version":105003}

响应格式:

{

"code": 400800,

"data": {

"database": [

"insert.into.LevelExp{serial = 10000,level = 1,exp = 15,}",

这里可以插入自定义数据长度

"insert.into.LotteryItem{serial = 10000,item_id = 2001,amount = 1,type = 'body',extra = 0,}",

]

},

"mask": 2052972415

}

API :/system/snapshoot 上传截图

请求格式 :unknown

API :/account/authorize 登录

请求格式:{"name":"account","password":"SHA1(pwd)","time":1526291705,"version":105003}

响应格式:{

"code": 0,

"data": [

"insert.into.Account{serial = 10058,name = '421525157@qq.com',password = '17330F18BDC8483700B1A55E10248899BF1DF243',invitation = 'RAv31s1',sex = 1,gm = false,ip = '183.56.156.202',version = 100003,ctime = 1520178329,sign_time = 1526256000,last_time = 1526289910,ban = false,ban_time = 0,check_code = '4be8d05a2da2ff48ce6b4ba56375ea7bf5ef2b05',}",

"insert.into.Userinfo{serial = 10062,name = '\u53c1\u6c1b\u58ca\u6c17',belong = 10058,hp = 3992,mp = 420,exp = 0,body = 2000,hair = 35550,face = 20035,eardrop = 1032021,cap = 1004472,longcoat = 1053116,shoes = 1072189,glove = 1082685,cape = 1102801,weapon = 1702170,job = 100,map = 100000000,rmap = 100000100,ctime = 0,mtime = 1526039913,check_code = '26e29d525b6dba77177f0e30be10f2e554542830',}",

"insert.into.Userinfo{serial = 10115,name = '\u56db\u6c1b\u58ca\u6c17',belong = 10058,hp = 727,mp = 85,exp = 61793,body = 2000,hair = 35280,face = 20000,eardrop = 1032003,cap = 1003847,coat = 1040041,pants = 1060060,shoes = 1072007,glove = 1082001,weapon = 1432013,job = 530,map = 105070300,ctime = 0,mtime = 1522595861,check_code = '26ebe5dfdddb3b918264b906ccd881733dc947a3',}",

"insert.into.Integral{serial = 10025, belong = 10058, quantity = 2910, limit = 0, provenance = 'system', check_code = '0044bd555a0551f169b88fd280c45a6866e4f8e0'}",

"insert.into.Integral{serial = 10026, belong = 10058, quantity = 8772, limit = 1, provenance = 'system', check_code = '17b025264b0684c42dbbd27791ce99c5c3cc19e2'}",

"update.config.value{SystemExpMultiple = 0, SystemDropMultiple = 0}"

],

"mask": 1368326100

}

API :/account/data 获取角色列表

请求格式:{"token":"0ca9d29e5790e925cc2656415ea4fee4f929131c"}

{

"code": 0,

"data": [

"insert.into.Account{serial = 10058,name = '421525157@qq.com',password = '17330F18BDC8483700B1A55E10248899BF1DF243',invitation = 'RAv31s1',sex = 1,gm = false,ip = '183.56.156.202',version = 100003,ctime = 1520178329,sign_time = 1526256000,last_time = 1526289910,ban = false,ban_time = 0,check_code = '4be8d05a2da2ff48ce6b4ba56375ea7bf5ef2b05',}",

"insert.into.Userinfo{serial = 10062,name = '\u53c1\u6c1b\u58ca\u6c17',belong = 10058,hp = 3992,mp = 420,exp = 0,body = 2000,hair = 35550,face = 20035,eardrop = 1032021,cap = 1004472,longcoat = 1053116,shoes = 1072189,glove = 1082685,cape = 1102801,weapon = 1702170,job = 100,map = 100000000,rmap = 100000100,ctime = 0,mtime = 1526039913,check_code = '26e29d525b6dba77177f0e30be10f2e554542830',}",

"insert.into.Userinfo{serial = 10115,name = '\u56db\u6c1b\u58ca\u6c17',belong = 10058,hp = 727,mp = 85,exp = 61793,body = 2000,hair = 35280,face = 20000,eardrop = 1032003,cap = 1003847,coat = 1040041,pants = 1060060,shoes = 1072007,glove = 1082001,weapon = 1432013,job = 530,map = 105070300,ctime = 0,mtime = 1522595861,check_code = '26ebe5dfdddb3b918264b906ccd881733dc947a3',}",

"insert.into.Integral{serial = 10025, belong = 10058, quantity = 2910, limit = 0, provenance = 'system', check_code = '0044bd555a0551f169b88fd280c45a6866e4f8e0'}",

"insert.into.Integral{serial = 10026, belong = 10058, quantity = 8772, limit = 1, provenance = 'system', check_code = '17b025264b0684c42dbbd27791ce99c5c3cc19e2'}",

"update.config.value{SystemExpMultiple = 0, SystemDropMultiple = 0}"

],

"mask": 1368326100

}

API:/account/switch 切换角色 进入游戏

请求格式:{"token":"091a4ada3d999d32b38cae4de091542633124782","user":10115}

{

"code": 0,

"data": [

"insert.into.Possession{serial = 11795, belong = 10062, quantity = 53506100, limit = 0, provenance = 'system', check_code = '34bf8892162e48c60d1abfcca6645e50958b91bc'}",

"insert.into.Possession{serial = 11796, belong = 10062, quantity = 4728043, limit = 1, provenance = 'system', check_code = '3719769c236b8e567e15edf3ce696f17ba3563a4'}",

"insert.into.Levels{serial = 10079, belong = 10062, level = 80, check_code = 'd5cc47c5dcf6a4f4b5ff3aa88660e03d23d28c46'}",

"update.increment.Items \"11832\"",

"insert.into.Items{identify = 10558,serial = 10003,belong = 10062,item_id = 1142079,amount = 1,site = 'equips',level = 0,slevel = 0,provenance = 'cdkey',comment = 'Z0F0VAKYiE',ctime = 0,mtime = 1526013658,check_code = 'f0abd03df42a21f373d03644879a3b45b8a429a9',}"

"insert.into.Setting{serial = 10000,belong = 10062,name = 'consume',data = {[1] = 2000002, [2] = 2001001},check_code = '1f3d9a1f63f01576fc83337cdf47aa31a6691fca',}",

"insert.into.Setting{serial = 10001,belong = 10062,name = 'mexp',data = 1525399951,check_code = 'ac16a184a71570e1ac4595385eab6cdb58031fd4',}"

],

"mask": 1100030688

}

5.2.2 后台设计

后台使用SpringMVC + Spring 实现 使用RESTful接口进行封装。具体的逻辑参考Github实现。

https://github.com/haha02hah02/tech_talk/

6.总结

整个游戏的破解从分析游戏结构入手,得到当前游戏使用cocos for lua 进行编写,找到对应的lua脚本之后发现脚本进行一次自定加密的操作。通过IDA 进行逆向静态分析得到了加密的密钥,为解密提供方法。

分析当前的游戏行为,发现整个游戏的所有逻辑都是基于lua脚本进行,虽然是个所谓联网的游戏,不过只是个骗钱的噱头而已,在这种游戏架构下根本无法实现真正的联网操作。

在游戏中,怪物的爆率,商店的道具、抽奖等都是基于本地化的操作,在这种情况下,反编译lua脚本可以完全实现获取自定义道具的功能。

最后HTTP联网实际上只是保存了用户的一个状态而已,分析这个游戏联网通信的接口不超过15个,并且在得到了数据包加解密操作之后,写出后台也只是时间问题。

以上,从做游戏角度来看这是一个失败的作品,游戏的安全性不高,游戏体验差,玩法少,充分体现了这个开发者的无能。因此开发产品时,如何提高产品的安全性,如何提高游戏的玩法是游戏开发中最要思考的问题。


注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功