最近闲来无事,花了5个小时的时间学习破解了1款基于cocos lua写的android游戏,最终得到了这个游戏的源代码。第一次尽然轻松成功了,经验教训分享一下。
阅读当前教程,你需要掌握一下的一些技术和方法。
首先拿到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 工具我们可以得到源码。
最后发现,大功告成!
上一张提到了逆向+解密+反编译获取源码,这一张讲一下获得源码之后如何修改。
反编译源码之后,对立面。一些源码进行查看,发现App使用以下的行为对App自身进行保护。
签名校验
当加载进从 Script/Main.nts 进入lua 脚本之后
Main脚本主要加载公共的脚本进行初始化之后,加载游戏世界。
进入Include.nts进行查看。
这个里面初始化了很多公共的脚本,其中一个关键的脚本为Security。这个脚本做了App自保操作。
在上图红色的地方进行了签名校验,如果签名不对游戏退出。
同时在代码里面搜索AppSignature()发现在其他的地方加载了当前的函数。
由于World.lua 可能由于热更新加载到外部的脚本,因此这个地方不做剔除操作。
从上面的大致逻辑可以看出游戏的整个执行过程。
上图为简化的游戏启动流程,其中红色的节点是有可能动态的从外部加载的。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2018-6-15 18:05
被法老王ms编辑
,原因: