cocos逆向工程汇总

2023/11/21 sec 共 5298 字,约 16 分钟

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》《捕鱼达人》《大掌门》《刀塔传奇》《放开那三国》《全民飞机大战》《欢乐斗地主》《开心消消乐》《保卫萝卜》《梦幻西游》《大话西游》《神武》《问道》《征途》《列王的纷争》《热血传奇》《传奇世界》《剑与家园》《乱世王者》《传奇霸业》等。

Download Cocos2d-x

Cocos2dx-JS

解密

常规在libcocos2djs.so文件中搜索Ascil字符串Cocos Gamemain.jsjsb-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中

  1. 第一种方法是libcocos2dlua.so使用IDA打开string窗口,全局查找加密sign。点击进入查找结果,在该结果的上方3行能够发现加密key。
  2. 第二种方法,用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]));
  }
});

参考

文档信息

Search

    Table of Contents