Cocos2d-x 是一款国产的开源的手机游戏开发框架,基于MIT许可证发布。引擎核心采用C++编写,提供C++、Lua、JavaScript 三种编程语言接口,跨平台支持 iOS、Android 等智能手机,Windows、Mac 等桌面操作系统,以及 Chrome, Safari, IE 等 HTML5 浏览器。
Cocos2d-x 降低了手机游戏的技术从业门槛,在全球范围得到广泛使用和认可。腾讯、网易、盛大、掌趣等国内游戏大厂,以及任天堂、Square Enix、Gamevil、DeNA、LINE等国际大厂均已使用cocos2d-x引擎开发并推出了自己的手游产品。使用cocos2d-x引擎的历年代表作有《我叫MT Online》《捕鱼达人》《大掌门》《刀塔传奇》《放开那三国》《全民飞机大战》《欢乐斗地主》《开心消消乐》《保卫萝卜》《梦幻西游》《大话西游》《神武》《问道》《征途》《列王的纷争》《热血传奇》《传奇世界》《剑与家园》《乱世王者》《传奇霸业》等。
Cocos2dx-JS
解密
常规在libcocos2djs.so
文件中搜索Ascil字符串Cocos Game
、main.js
、jsb-adapter/jsb-builtin.js
等一些常规的普遍关键词来尝试定位Key。如果没找到,用IDA看下so文件有没有做过加密混淆,没有的话就结合applicationDidFinishLaunching
函数等来寻找明文的Key值,或者hook关键函数来打印Key值。如果游戏做了混淆或其他安全手段,需要分析处理。
一般来说,文本方式打开cocos
引擎的so文件,搜索特征字符串:Cocos Game
,在后面紧接着的明文字符串就是密钥。
cocos2dx-js
解密,coco2dx
生成的jsc
并不是真正意义上的编译出来的字节码,只是做一层压缩和xxtea
加密,因此解密过程就是先做xxtea
解密和解压缩。网上有一个解密的python
脚本:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
##运行需求
##pip install cffi
##pip install xxtea-py
import os
import xxtea
import zlib
##获取当前目录下所有jsc文件
def getFileList():
fs=[]
dirpath='./'
for root,dirs,files in os.walk(dirpath):
for file in files:
if(file.endswith('.jsc')):
fs.append(os.path.join(root,file))
return fs
def Fix(path,key):
f1=open(path,'rb').read()
print("正在解密:%s"%(path))
d1=xxtea.decrypt(f1,key)
d1=zlib.decompress(d1,16+zlib.MAX_WBITS)
print("解密完成:%s"%(path))
f2=open(path.replace('.jsc','.js'),'wb')
f2.write(d1)
def run(key):
for f in getFileList():
#print(f)
Fix(f,key)
key = "xxxxxxx-xxxx-xx"
run(key)
密钥分析过程
IDA分析 libcocos2djs.so
,查找如下函数分析上下文关联寻找线索:
jsb_set_xxtea_key
applicationDidFinishLaunching
xxtea_decrypt
do_xxtea_decrypt
Interceptor.attach(Module.findBaseAddress("libcocos2djs.so").add(0x22E5CC), {
onEnter: function(args) {
console.log(Memory.readUtf8String(args[2]));
},
onLeave: function(retval) {
}
});
重建
Cocos2dx-js引擎做的游戏在运行时会先检测内存里面有没有js文件,有的话就直接运行js文件,没有的话就从jsc转换出js文件,所以解密后的js文件直接丢入原包就行(除了一些做了文件验证形式的安全手段的游戏)。jsc解密后,还得在同目录下的index.json(config.json)文件把encrypted
改成flase
,不然会打不开。
Cocos2dx-Lua
AppDelegate
.cpp源码:
bool AppDelegate::applicationDidFinishLaunching() {
// register lua engine
LuaEngine* pEngine = LuaEngine::getInstance();
ScriptEngineManager::getInstance()->setScriptEngine(pEngine);
LuaStack* stack = pEngine->getLuaStack();
stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA"));
lua_State* L = stack->getLuaState();
lua_module_register(L);
lua_getglobal(L, "_G");
if (lua_istable(L,-1))//stack:...,_G,
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 || CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID ||CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_MAC)
register_assetsmanager_test_sample(L);
#endif
register_test_binding(L);
}
lua_pop(L, 1);
#if CC_64BITS
FileUtils::getInstance()->addSearchPath("src/64bit");
#endif
FileUtils::getInstance()->addSearchPath("src");
FileUtils::getInstance()->addSearchPath("res");
pEngine->executeScriptFile("controller.lua");
return true;
}
luaLoadBuffer
里调用xxtea_decrypt
解密了lua脚本,然后调用luaL_loadbuffer
加载解密后的脚本,所以直接hook 函数luaL_loadbuffer
就可以dump出解密过的lua脚本了。
AppDelegate::applicationDidFinishLaunching {
setXXTEAKeyAndSign
executeScriptFile {
getDataFromFile
luaLoadBuffer {
xxtea_decrypt
luaL_loadbuffer {
luajit
}
}
executeFunction
}
}
密key的找寻方法
key在打包后的cocos的lib库的libcocos2dlua.so中
- 第一种方法是
libcocos2dlua.so
使用IDA打开string窗口,全局查找加密sign。点击进入查找结果,在该结果的上方3行能够发现加密key。 - 第二种方法,用
strings
工具查找字符串。终端运行strings -a libcocos2dlua.so
,查找sign
,观察sign
上方的字符串,即为key。
反编译
使用unluac.jar
:
java -jar unluac.jar ./StoreItemDlg.luac > ./StoreItemDlg.luac.lua
抓包
抓游戏客户端与服务器的通信数据:
//函数定义 void LuaWebSocket::onMessage(WebSocket* ws, const WebSocket::Data& data)
// 这种方式可以精确的hook某个函数但需要自行查找函数调用地址,动态调试需要自行查找偏移地址。
// 用 nm -DC libcocos2dlua.so | grep -i LuaWebSocket::onMessage 可以找到so内静态的调用地址。
//var func = Module.findBaseAddress("libcocos2dlua.so").add(0x8244b4);
// 当函数是全局唯一时可以用这种方式,如果存在多个函数名则hook无效。
var func = Module.findExportByName("libcocos2dlua.so" , "LuaWebSocket::onMessage");
var Log = Java.use("android.util.Log");
Interceptor.attach(func, {
onEnter: function (args) {
// 在不知道数据类型前先这样看看hook后是否有数据,有数据再用对应数据类型的读函数或转换函数。数据类型不对会导致hook失败。
Log.e("frida-HOOK", "ws:"+args[1]);
Log.e("frida-HOOK", "data:"+args[2]);
}
});
Dump lua文件的frida脚本, 脚本文件放置路径为/data/local/tmp/frida_script.js:
var func = Module.findBaseAddress("libcocos2dlua.so").add(0x93ad2d);
//var func = Module.findBaseAddress("libcocos2dlua.so").add(0x93ad0d);
Interceptor.attach(func, {
onEnter: function (args) {
this.fileout = "/sdcard/lua/" + Memory.readCString(args[3]).split("/").join(".");
console.log("read file from: "+this.fileout);
var tmp = Memory.readByteArray(args[1], args[2].toInt32());
var file = new File(this.fileout, "w");
file.write(tmp);
file.flush();
file.close();
}
});
获取sign和key的frida脚本, 脚本文件放置路径为/data/local/tmp/frida_script.js
var func = Module.findBaseAddress("libcocos2dlua.so").add(0x6ea6d4);
//var func = Module.findExportByName("libcocos2dlua.so" , "cocos2d::LuaStack::setXXTEAKeyAndSign");
var Log = Java.use("android.util.Log");
Interceptor.attach(func, {
onEnter: function (args) {
Log.e("frida-HOOK", "key:"+Memory.readCString(args[1]));
Log.e("frida-HOOK", "sign:"+Memory.readCString(args[3]));
}
});
参考
- Cocos2DX-JS 加密逆向探究解密app实战
- 关于Cocos2dx-js游戏的jsc文件解密(二)
- Cocos2d-lua工程运行流程的理解
- 浅谈Cocos2d-x下Lua文件的保护方式
- cocos2dx-Lua引擎游戏脚本及图片资源解密与DUMP_luajit解密
- cocos2dx lua 反编译
- cocos2dx游戏逆向实战
- cocos2d-x官方引用的xxtea加密解密算法源码
- XXTEA 可逆加密解密算法 C++ C#兼容版本
- 安卓逆向之Luac解密反编译
- Cocos2d-x与LuaJIT汇编的初步解密
- Dr-MTN/luajit-decompiler
- luajit-lang-toolkit
- Lua绑定的Socket通信源码
- Lua绑定的XMLHttpRequest通信源码(http请求会用到)
- Lua绑定的Downloader源码(下载文件用到)
- frida内存检索svc指令查找sendto和recvfrom进行hook抓包
文档信息
- 本文作者:zhupite
- 本文链接:https://zhupite.com/sec/cocos-decompile.html
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)