正文
3.1 普通的对称加密,在加载脚本之前解密
这种情况是指打包在APK中的lua代码是加密过的,程序在加载lua脚本时解密(关键函数luaL_loadbuffer ),解密后就能够获取lua源码。如果解密后获取的是luac字节码的话,也可以通过反编译得到lua源码,反编译主要用的工具有unluac和luadec51,后面会具体分析。
3.2 将 lua 脚本编译成luaJIT字节码并且加密打包
因为反编译的结果并不容易查看,所以这种情况能够较好的保护 lua 源码。这个情况主要是先解密后反编译,反编译主要是通过 luajit-decomp 项目,它能够将 luajit 字节码反编译成伪 lua 代码。
3.3 修改 lua 虚拟机中 opcode 的顺序
这种情况主要是修改 lua 虚拟机源码,再通过修改过的虚拟机将lua脚本编译成 luac 字节码,达到保护的目的。这种情况如果直接用上面的反编译工具是不能将 luac 反编译的,需要在程序中分析出相对应的opcode,然后修改lua项目的 opcode 的顺序并重新编译生成反编译工具,就能反编译了,后面会具体分析。
一般上面的情况都会交叉遇到。
这里主要介绍 4 种方法,都会在第 5 节中用实例说明。
4.1 静态分析 so 解密方法
这种方法需要把解密的过程全部分析出来,比较费时费力,主要是通过 ida 定位到 luaL_loadbuffer 函数,然后往上回溯,分析出解密的过程。
4.2 动态调试:ida + idc + dump
这里主要通过 ida 动态调试 so 文件,然后是定位到 luaL_loadbuffer 地址,游戏会在启动的时候通过调用 luaL_loadbuffer 函数加载必要的 lua 脚本,通过在 luaL_loadbuffer 下断点 ,断下后就可以运行 idc 脚本将lua代码导出(程序调用一次 luaL_loadbuffer 加载一个lua脚本,不写 idc 脚本的话需要手动导N多遍.....)。
4.3 hook so
跟 4.2 原理一样,就是通过 hook 函数 luaL_loadbuffer 地址,将代码保存,相比4.2的好处是有些 lua 脚本需要在玩游戏的过程中才加载,如果用了 4.2 的方法,游戏过程中 中断一次就需要手动运行一次 idc 脚本,而且往往每次只加载一个 lua 文件,如果是 hook 的话,就不需要那么麻烦,直接玩一遍游戏,全部 lua 脚本就已经保存好了。
4.4 分析 lua 虚拟机的 opcode 的顺序
这里主要是 opcode 的顺序被修改了,需要用 ida 定位到虚拟机执行luac字节码的地方,然后对比原来 lua 虚拟机的执行过程,获取修改后的 opcode 顺序,最后还原 lua 脚本。
好了,下面用3个例子来说明上面的情况。
5.1 54捕鱼
首先用AndroidKiller 加载,然后查看lib目录下的so文件,发现libcocos2dlua.so文件,基本可以确定是lua脚本编写的了。这里有个小技巧,当有很多so文件的时候,一般最大的文件是我们的目标(文件大是因为集成了lua引擎)。既然有lua引擎,肯定有lua脚本了,接着找lua脚本。资源文件和lua脚本文件都是在assets目录下。发现游戏的资源文件和配置文件都是明文,这里直接修改游戏的配置文件就可以作弊(比如修改升级炮台所需的金币和钻石,就可以达到快速升级炮台的目的),然后并没有发现类似lua脚本的文件。
顺手解压了一下res目录下的liveupdate_precompiled.zip,发现解压失败,看来是加密了(看名字就知道是更新游戏的代码)这里说明一下,一般遇到xxxx_precompiled.zip的这种文件,都是quick-cocos2d-x框架(quick简单来说就是对lua的拓展实现),在quick-cocos2d-x框架下可以用compile_scripts命令将lua文件加密打包成xxxx_precompiled.zip,游戏运行时再解密加载。注意,这种方式打包的lua脚本一般都会被编译成luaJIT,加载的关键函数是loadChunksFromZIP,可以在IDA中直接搜索该函数,如果找不到可以搜索字符串luaLoadChunksFromZIP来定位到函数
OK,了解了原理接下来开始动手分析,将libcocos2dlua.so拖到IDA中加载,函数中直接搜索loadChunksFromZIP,定位后F5。
一直向上回溯(交叉引用 ),来到下图,发现解密的密钥和签名,其中xiaoxian为密钥,XXFISH为签名。
进去函数里面看看,其实会发现调用了XXTea算法,这里我们也可以直接分析loadChunksFromZIP函数的源码(所以配置一个cocos2d的开发环境还是非常有必要的)。查看源码里的lua_loadChunksFromZIP函数的原型:
int CCLuaStack::lua_loadChunksFromZIP(lua_State *L)
{
if (lua_gettop(L)
{ // 这里可以发现用字符串也可以定位到目标函数
CCLOG("lua_loadChunksFromZIP() - invalid arguments");
return 0;
}
...
if (isXXTEA)
{
// decrypt XXTEA