apk分析工具 浅析 Android 手游 lua 脚本的加密与解密

0. 前言

这篇文章是本人在学习 Android 手游安全时总结的一篇关于 lua 的文章,不足之处欢迎指正,也欢迎大牛前来交流。本文目录如下:

0. 前言

1. lua脚本在手游中的现状

2. lua、luac、luaJIT三种文件的关系

3. lua脚本的保护

3.1 普通的对称加密,在加载脚本之前解密

3.2 将lua脚本编译成luaJIT字节码并且加密打包

3.3 修改lua虚拟机中opcode的顺序

4. 获取lua代码的一般方法

4.1 静态分析so解密方法

4.2 动态调试:ida + idc + dump

4.3 hook so

4.4 分析lua虚拟机的opcode的顺序

5. 三个游戏的lua脚本解密过程

5.1 54捕鱼

5.2 捕鱼达人4

5.3 梦幻西游手游

6. 总结

参考文章

主要用到的工具和环境:

1. win7 系统一枚

2. quick-cocos2d-x 的开发环境(弄一个开发环境方便学习,而且大部分 lua 手游都是用的 cocos2d-x 框架,还有一个好处,可以查看源码关键函数中的特征字符串,然后在 IDA 定位到关键函数,非常方便)

3. IDA6.8(分析so文件+动态调试so)

4. vs2015(编写解密代码)这里建议用vs2013来编译运行cocos2d-x,vs2015太多坑要填了…..

5. AndroidKiller 1.3.1(反编译apk,其中apktool.exe是最新版)

6. luadec51(反编译 luac)

7. luajit-decomp(反编译 luaJIT)

等等…

1. lua 脚本在手游中的现状

略。

2. lua、luac、luaJIT 三种文件的关系

在学习 lua 手游过程中,本人遇到的 lua 文件大部分是这 3 种。其中 lua 是明文代码,直接用记事本就能打开,luac 是 lua 编译后的字节码,文件头为 0x1B 0x4C 0x75 0x61 0x51,lua 虚拟机能够直接解析 lua 和 luac 脚本文件,而 luaJIT 是另一个 lua 的实现版本(不是原作者写的),JIT 是指Just-In-Time(即时解析运行),luaJIT 相比 lua 和 luac 更加高效,文件头是 0x1B 0x4C 0x4A。

luac:

luajit:

3. lua脚本的保护

一般有安全意识的游戏厂商都不会直接把 lua 源码脚本打包到 APK 中发布,所以一般对 lua 脚本的保护有下面3种:

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. 获取 lua 源码的一般方法

这里主要介绍 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 文件apk分析工具,如果是 hook 的话,就不需要那么麻烦,直接玩一遍游戏,全部 lua 脚本就已经保存好了。

4.4 分析 lua 虚拟机的 opcode 的顺序

这里主要是 opcode 的顺序被修改了apk分析工具,需要用 ida 定位到虚拟机执行luac字节码的地方,然后对比原来 lua 虚拟机的执行过程,获取修改后的 opcode 顺序,最后还原 lua 脚本。

5. 三个游戏的 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。

apk分析工具_apk解包打包工具_apk反编译工具 2016

一直向上回溯(交叉引用 ),来到下图,发现解密的密钥和签名,其中xiaoxian为密钥,XXFISH为签名。

apk反编译工具 2016_apk分析工具_apk解包打包工具

进去函数里面看看,其实会发现调用了XXTea算法,这里我们也可以直接分析loadChunksFromZIP函数的源码(所以配置一个cocos2d的开发环境还是非常有必要的)。查看源码里的lua_loadChunksFromZIP函数的原型:

intCCLuaStack::lua_loadChunksFromZIP(lua_State*L)

{

if(lua_gettop(L)m_xxteaSignLen,

(xxtea_long)size-(xxtea_long)stack->m_xxteaSignLen,

(unsignedchar*)stack->m_xxteaKey,

(xxtea_long)stack->m_xxteaKeyLen,

&len);

delete[]zipFileData;

zipFileData=NULL;

zip=CCZipFile::createWithBuffer(buffer,len);

}

}

接下来直接写解密函数(在cocos2d-x项目里面写的解密函数,很多工具直接可以调用)

voiddecryptZipFile_54BY(stringstrZipFilePath)

{

CCFileUtils*utils=CCFileUtils::sharedFileUtils();

unsignedlonglZipFileSize=0;

unsignedchar*szBuffer=NULL;

unsignedchar*zipFileData=utils->getFileData(strZipFilePath.c_str(),”rb”,&lZipFileSize);

xxtea_longxxBufferLen=0;

szBuffer=xxtea_decrypt(zipFileData+6,//6为签名XXFISH的长度

(xxtea_long)lZipFileSize-(xxtea_long)6,//减去签名的长度

(unsignedchar*)”xiaoxian”,//xiaoxian为密钥

(xxtea_long)8,//密钥的长度

&xxBufferLen);

//获取zip里面的所有文件

CCZipFile*zipFile=CCZipFile::createWithBuffer(szBuffer,xxBufferLen);

intcount=0;

stringstrFileName=zipFile->getFirstFilename();

while(strFileName.length())

{

cout

———END———
限 时 特 惠:本站每日持续更新海量各大内部创业教程,一年会员只需128元,全站资源免费下载点击查看详情
站 长 微 信:jiumai99

滚动至顶部