首页
社区
课程
招聘
cocos2d逆向入门和某捕鱼游戏分析
发表于: 2021-7-20 12:05 26709

cocos2d逆向入门和某捕鱼游戏分析

2021-7-20 12:05
26709

先clone到本地

git clone https://github.com/cocos2d/cocos2d-x.git

Cocos2d-x是一个开源的移动2D游戏框架,底层支持各种平台,核心用c++封装了各种库,外面给了lua和c++的接口,所以关键代码可能在lua中,很多安卓游戏的逻辑也基本都在lua脚本里,盗用官网这张图

lua虚拟机相关代码在cocos2d-x\cocos\scripting\lua-bindings\manual里

CCLuaEngine.h lua引擎相关

CCLuaStack.h lua栈相关

进入虚拟机

cocos2d-x\templates\lua-template-default\frameworks\runtime-src\Classes\AppDelegate.cpp\AppDelegate::applicationDidFinishLaunching

应用结束加载中进入lua虚拟机

这句engine->executeScriptFile("main.lua")调用了cocos2d-x\cocos\scripting\lua-bindings\manual\CCLuaEngine.cpp的executeScriptFile调用了CCLuaStack.cpp的executeScriptFile

luaLoadBuffer里调用xxtea_decrypt解密了lua脚本,然后调用luaL_loadbuffer加载解密后的脚本,所以直接hook 这个函数luaL_loadbuffer把(char*)content这个字符dump出来就得到解密过的lua脚本了

而luaL_loadbuffer的源码没有,只有编译过的库cocos2d-x\external\lua\luajit\prebuilt\android\armeabi-v7a\libluajit.a,

要找它的实现需要下载luajit源代码分析了,这就完全进入了lua虚拟机的实现

a Just-In-Time Compiler for Lua. 采用C语言写的Lua的解释器的代码

1.从c++进入lua世界的调用逻辑

2.加密算法为xxtea,如果没有修改,sign为XXTEA,KEY为2dxlua,如果有修改,可以ida打开libxxluaxx.so在applicationDidFinishLaunching里找到

3.无论是否加密,解密后都会调用luaL_loadbuffer函数,所以直接hook这个函数把(char*)content这个字符dump出来就是解密过的lua脚本了,缺点是要把游戏运行一遍,只能搞出执行过的代码

4.cocos2d-x\external\xxtea\xxtea.cpp里有完整的加密解密算法,逻辑清晰,可以写个python脚本直接本地解密,也可以在这里hook获取key和sign或者解密后脚本

某捕鱼游戏,下载安装apk后,再其内部内置了捕鱼、麻将等十几款小游戏,嘿嘿,懂得都懂这是干啥的,直接点击捕鱼游戏下载,

下载后的游戏源码在/data/data/com.q8wdw6.gyll9spfo.nycatp9/files/download/里

adb pull 出来,files文件夹里面已经暴露很多信息了,配置,下载的游戏,更新等等

进入files\download\107\res\就可以看到luac,很明显crash里面存储了所有碰撞,model_path_crash和path里存储的是移动路径,我们可以看到所有的鱼的移动路径都是tm设定好的,models里面存储着游戏逻辑,views里面存储着界面显示逻辑

随机打开一个luac加密了,开头的ZX_CS@56#D~d@dud明显就是加密sign

根据之前对cocos2d引擎的分析,找到apk下面带有lua也是最大的一个so libqpry_lua.so,用ida直接打开,定位到AppDelegate::applicationDidFinishLaunching函数,直接拖到最下面,看到这句

(*(v7 + 1) + 116))((v7 + 1), "ZX_01RdsF~@!R8", 14, "ZX_CS@56#D~d@dud", 16);很明显是调用了stack->setXXTEAKeyAndSign

对比源码stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA"));

解密key为ZX_01RdsF~@!R8,sign为ZX_CS@56#D~d@dud

也可以直接搜索字符串,因为key和sign在一个函数调用,所以一般离的很近,base/src/main.lua这个字符串进一步验证了正确性

有了key和sign就可以直接解密luac脚本了,照着写,很简单,可以看到sign的作用就是被忽略,只有长度有用,尴尬,解密用的主要是key

或者直接pip install xxtea-py

然后直接python,自己写个循环实现批量解密吧

下面开始分析lua,这个最简单,我们想分析子弹打到鱼的逻辑,直接找src\views\layer\BulletLayer.luac

直接找子弹打到鱼的函数,很清晰,第一个参数为子弹对象,第二个为鱼列表,这里看到只有master_id == self_player_id才会调用

on_self_bullet_crash_fish当自己的子弹击中了鱼最后会调用加金币等,否则只调用on_bullet_crash_fish显示效果,这里可以也改成on_self_bullet_crash_fish就可以加金币了

在定位一下加钱的函数,fish_gold是鱼的钱,fish_odds是鱼的剩余 ,这句 local one_add_gold = math.floor(fish_gold/12)就是一个鱼加的钱,我们把这个/12改为*12就可以修改倍数了

入口代码在AnimationLayer.lua里,直接看接收到玩家捕到鱼的函数,分为网或者炸弹,里面还有一网打尽和大转盘之类的特效,这里可以把它全部改成高倍的特效

src\models\DataCenter.lua里存储着鱼和子弹等参数,可以看到子弹最高9级,odds_min与odds_max应该就代表威力,改这里可以改子弹威力

self.fish_client存储不同鱼的参数,不同id对应不同鱼,mulriple_min与mulriple_max存储着金币的倍数最大与最小值,一般鱼都是2-3倍,大章鱼为300倍,改这里可以直接改鱼的倍数

其他的功能逻辑获得源码后都很清晰,改倍数改鱼改子弹什么都很简单,改完之后重新用sign和key加密之后push到相应文件夹下面就实现了破解

如何实现cocos2d反逆向,我的一些不成熟想法,由浅到深分层如下

1.修改xxtea的key和sign,就像这个游戏,需要分析so才能找到key

2.直接修改xxtea算法为其他可逆算法,这样逆向的时候还得逆加密算法

3.在进一步,修改luajit源码改动lua虚拟机,比如改变字节码指令顺序,或者改变数据读取顺序等,这样逆向的时候必须分析lua虚拟机的改动

4.把key,加密算法,虚拟机改动等的关键函数封装放到其他cpp或者其他so里,在加密一层,调用的时候在解密,这样需要先动态调试先分析调用逻辑

5.关键代码加入ollvm再编译或者直接vm掉,这样需要先去掉ollvm混淆或者分析vm

cocos2d 3.3 lua 代码加密 luac

安卓逆向之Luac解密反编译

 
 
 
 
 
 
 
bool AppDelegate::applicationDidFinishLaunching()
{
    // set default FPS
    Director::getInstance()->setAnimationInterval(1.0 / 60.0f);//设置fps刷新率
 
    // register lua module
    auto engine = LuaEngine::getInstance();//创建lua虚拟机引擎
    ScriptEngineManager::getInstance()->setScriptEngine(engine);//设置脚本引擎为lua引擎
    lua_State* L = engine->getLuaStack()->getLuaState();//创建lua虚拟机环境lua_State
    lua_module_register(L);  //分配网络,控制台,ui界面等相关联的寄存器
 
    register_all_packages();
 
    LuaStack* stack = engine->getLuaStack();
    stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA"));
    //这一步很关键,获取栈结构并调用setXXTEAKeyAndSign设置加密算法为xxtea,sign为XXTEA,KEY为2dxlua,很多游戏lua脚本都用的默认的sign和key
    //如果没有用默认的,ida打开libxxxluaxxx.so直接搜索applicationDidFinishLaunching导出函数也基本都能直接找到
 
    //register custom function
    //LuaStack* stack = engine->getLuaStack();
    //register_custom_function(stack->getLuaState());
 
#if CC_64BITS
    FileUtils::getInstance()->addSearchPath("src/64bit");
#endif
    FileUtils::getInstance()->addSearchPath("src");  //lua源码在src文件夹,资源在res文件夹
    FileUtils::getInstance()->addSearchPath("res");
    if (engine->executeScriptFile("main.lua"))   //直接通过lua引擎调用main.lua进入lua的世界
    {
        return false;
    }
 
    return true;
}
bool AppDelegate::applicationDidFinishLaunching()
{
    // set default FPS
    Director::getInstance()->setAnimationInterval(1.0 / 60.0f);//设置fps刷新率
 
    // register lua module
    auto engine = LuaEngine::getInstance();//创建lua虚拟机引擎
    ScriptEngineManager::getInstance()->setScriptEngine(engine);//设置脚本引擎为lua引擎
    lua_State* L = engine->getLuaStack()->getLuaState();//创建lua虚拟机环境lua_State
    lua_module_register(L);  //分配网络,控制台,ui界面等相关联的寄存器
 
    register_all_packages();
 
    LuaStack* stack = engine->getLuaStack();
    stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA"));
    //这一步很关键,获取栈结构并调用setXXTEAKeyAndSign设置加密算法为xxtea,sign为XXTEA,KEY为2dxlua,很多游戏lua脚本都用的默认的sign和key
    //如果没有用默认的,ida打开libxxxluaxxx.so直接搜索applicationDidFinishLaunching导出函数也基本都能直接找到
 
    //register custom function
    //LuaStack* stack = engine->getLuaStack();
    //register_custom_function(stack->getLuaState());
 
#if CC_64BITS
    FileUtils::getInstance()->addSearchPath("src/64bit");
#endif
    FileUtils::getInstance()->addSearchPath("src");  //lua源码在src文件夹,资源在res文件夹
    FileUtils::getInstance()->addSearchPath("res");
    if (engine->executeScriptFile("main.lua"))   //直接通过lua引擎调用main.lua进入lua的世界
    {
        return false;
    }
 
    return true;
}
int LuaStack::executeScriptFile(const char* filename)
{
    CCAssert(filename, "CCLuaStack::executeScriptFile() - invalid filename");
 
    std::string buf(filename);
    //
    // remove .lua or .luac
    //
    size_t pos = buf.rfind(BYTECODE_FILE_EXT);//BYTECODE_FILE_EXT就是lua字节码,NOT_BYTECODE_FILE_EXT就是lua脚本源码
    //static const std::string BYTECODE_FILE_EXT    = ".luac";
    if (pos != std::string::npos)
    {
        buf = buf.substr(0, pos);//截取前缀
    }
    else
    {
        pos = buf.rfind(NOT_BYTECODE_FILE_EXT);
        if (pos == buf.length() - NOT_BYTECODE_FILE_EXT.length())
        {
            buf = buf.substr(0, pos);
        }
    }
 
    FileUtils *utils = FileUtils::getInstance();
 
    //
    // 1. check .luac suffix
    // 2. check .lua suffix
    //
    std::string tmpfilename = buf + BYTECODE_FILE_EXT;
    if (utils->isFileExist(tmpfilename))
    {
        buf = tmpfilename;
    }
    else
    {
        tmpfilename = buf + NOT_BYTECODE_FILE_EXT;
        if (utils->isFileExist(tmpfilename))
        {
            buf = tmpfilename;
        }
    }
 
    std::string fullPath = utils->fullPathForFilename(buf);//获取绝对路径
    Data data = utils->getDataFromFile(fullPath);//通过getDataFromFile读取lua文件到data
    int rn = 0;
    if (!data.isNull())
    {
        if (luaLoadBuffer(_state, (const char*)data.getBytes(), (int)data.getSize(), fullPath.c_str()) == 0)//通过luaLoadBuffer加载data
        {
            rn = executeFunction(0);
        }
    }
    return rn;
}
int LuaStack::executeScriptFile(const char* filename)
{
    CCAssert(filename, "CCLuaStack::executeScriptFile() - invalid filename");
 
    std::string buf(filename);
    //
    // remove .lua or .luac
    //
    size_t pos = buf.rfind(BYTECODE_FILE_EXT);//BYTECODE_FILE_EXT就是lua字节码,NOT_BYTECODE_FILE_EXT就是lua脚本源码
    //static const std::string BYTECODE_FILE_EXT    = ".luac";
    if (pos != std::string::npos)
    {
        buf = buf.substr(0, pos);//截取前缀
    }
    else
    {
        pos = buf.rfind(NOT_BYTECODE_FILE_EXT);
        if (pos == buf.length() - NOT_BYTECODE_FILE_EXT.length())
        {
            buf = buf.substr(0, pos);
        }
    }
 
    FileUtils *utils = FileUtils::getInstance();
 
    //
    // 1. check .luac suffix
    // 2. check .lua suffix
    //
    std::string tmpfilename = buf + BYTECODE_FILE_EXT;
    if (utils->isFileExist(tmpfilename))
    {
        buf = tmpfilename;
    }
    else
    {
        tmpfilename = buf + NOT_BYTECODE_FILE_EXT;
        if (utils->isFileExist(tmpfilename))
        {
            buf = tmpfilename;
        }
    }
 
    std::string fullPath = utils->fullPathForFilename(buf);//获取绝对路径
    Data data = utils->getDataFromFile(fullPath);//通过getDataFromFile读取lua文件到data
    int rn = 0;
    if (!data.isNull())
    {
        if (luaLoadBuffer(_state, (const char*)data.getBytes(), (int)data.getSize(), fullPath.c_str()) == 0)//通过luaLoadBuffer加载data
        {
            rn = executeFunction(0);
        }
    }
    return rn;
}
int LuaStack::luaLoadBuffer(lua_State *L, const char *chunk, int chunkSize, const char *chunkName)
{
    int r = 0;
 
    if (_xxteaEnabled && strncmp(chunk, _xxteaSign, _xxteaSignLen) == 0)//这里判断是否开启xxtea加密,如果开启就需要解密
    {
        // decrypt XXTEA
        xxtea_long len = 0;
        unsigned char* result = xxtea_decrypt((unsigned char*)chunk + _xxteaSignLen,
                                              (xxtea_long)chunkSize - _xxteaSignLen,
                                              (unsigned char*)_xxteaKey,
                                              (xxtea_long)_xxteaKeyLen,
                                              &len);//调用xxtea_decrypt解密脚本,这个函数在cocos2d-x\external\xxtea\xxtea.cpp里,加解密都在这个cpp里
        unsigned char* content = result;
        xxtea_long contentSize = len;
        skipBOM((const char*&)content, (int&)contentSize);//忽略utf8的bom
        r = luaL_loadbuffer(L, (char*)content, contentSize, chunkName);//无论是否加密,解密后都会调用luaL_loadbuffer函数,所以直接hook这个函数把(char*)content这个字符dump出来就是解密过的lua脚本了
        free(result);
    }
    else
    {
        skipBOM(chunk, chunkSize);
        r = luaL_loadbuffer(L, chunk, chunkSize, chunkName);//这个返回值r会反映加载失败的类型,在下面的switch中打印出来
    }
 
#if defined(COCOS2D_DEBUG) && COCOS2D_DEBUG > 0
    if (r)
    {
        switch (r)
        {
            case LUA_ERRSYNTAX:
                CCLOG("[LUA ERROR] load \"%s\", error: syntax error during pre-compilation.", chunkName);
                break;
 
            case LUA_ERRMEM:
                CCLOG("[LUA ERROR] load \"%s\", error: memory allocation error.", chunkName);
                break;
 
            case LUA_ERRFILE:
                CCLOG("[LUA ERROR] load \"%s\", error: cannot open/read file.", chunkName);
                break;
 
            default:
                CCLOG("[LUA ERROR] load \"%s\", error: unknown.", chunkName);
        }
    }
#endif
    return r;
}
int LuaStack::luaLoadBuffer(lua_State *L, const char *chunk, int chunkSize, const char *chunkName)
{
    int r = 0;
 
    if (_xxteaEnabled && strncmp(chunk, _xxteaSign, _xxteaSignLen) == 0)//这里判断是否开启xxtea加密,如果开启就需要解密
    {
        // decrypt XXTEA
        xxtea_long len = 0;
        unsigned char* result = xxtea_decrypt((unsigned char*)chunk + _xxteaSignLen,
                                              (xxtea_long)chunkSize - _xxteaSignLen,
                                              (unsigned char*)_xxteaKey,
                                              (xxtea_long)_xxteaKeyLen,
                                              &len);//调用xxtea_decrypt解密脚本,这个函数在cocos2d-x\external\xxtea\xxtea.cpp里,加解密都在这个cpp里
        unsigned char* content = result;
        xxtea_long contentSize = len;
        skipBOM((const char*&)content, (int&)contentSize);//忽略utf8的bom
        r = luaL_loadbuffer(L, (char*)content, contentSize, chunkName);//无论是否加密,解密后都会调用luaL_loadbuffer函数,所以直接hook这个函数把(char*)content这个字符dump出来就是解密过的lua脚本了
        free(result);
    }
    else
    {
        skipBOM(chunk, chunkSize);
        r = luaL_loadbuffer(L, chunk, chunkSize, chunkName);//这个返回值r会反映加载失败的类型,在下面的switch中打印出来
    }
 
#if defined(COCOS2D_DEBUG) && COCOS2D_DEBUG > 0
    if (r)
    {
        switch (r)
        {
            case LUA_ERRSYNTAX:
                CCLOG("[LUA ERROR] load \"%s\", error: syntax error during pre-compilation.", chunkName);
                break;
 
            case LUA_ERRMEM:
                CCLOG("[LUA ERROR] load \"%s\", error: memory allocation error.", chunkName);
                break;
 
            case LUA_ERRFILE:
                CCLOG("[LUA ERROR] load \"%s\", error: cannot open/read file.", chunkName);
                break;
 
            default:
                CCLOG("[LUA ERROR] load \"%s\", error: unknown.", chunkName);
        }
    }
#endif
    return r;
}
 
 
AppDelegate::applicationDidFinishLaunching
 
{
 
  setXXTEAKeyAndSign
 
  executeScriptFile
 
  {
 
​    getDataFromFile
 
​    luaLoadBuffer
 
​    {
 
​      xxtea_decrypt
 
​      luaL_loadbuffer
 
​      {
 
​        luajit
 
​       }
 
​     }
 
​    executeFunction
 
  }
 
}
AppDelegate::applicationDidFinishLaunching
 
{
 
  setXXTEAKeyAndSign
 
  executeScriptFile
 
  {
 
​    getDataFromFile
 
​    luaLoadBuffer
 
​    {
 
​      xxtea_decrypt
 
​      luaL_loadbuffer
 
​      {
 
​        luajit
 
​       }

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2021-7-20 20:58 被挤蹭菌衣编辑 ,原因: 图挂了
收藏
免费 10
支持
分享
最新回复 (22)
雪    币: 4953
活跃值: (19055)
能力值: ( LV13,RANK:317 )
在线值:
发帖
回帖
粉丝
2
感谢分享,马上学习!
2021-7-20 20:56
0
雪    币: 5235
活跃值: (3260)
能力值: ( LV10,RANK:175 )
在线值:
发帖
回帖
粉丝
3
0x指纹 感谢分享,马上学习!
感谢指纹大佬支持
2021-7-20 20:59
0
雪    币: 986
活跃值: (6167)
能力值: ( LV7,RANK:115 )
在线值:
发帖
回帖
粉丝
4
太强了
2021-7-20 21:44
0
雪    币: 6
活跃值: (906)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
这个游戏连符号都不去
2021-7-21 08:53
0
雪    币: 5330
活跃值: (5479)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
6
mark
2021-7-21 14:16
0
雪    币: 1342
活跃值: (1070)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
7
从代码层讲得很详细了,温故知新。
2021-7-24 20:30
1
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8

大佬,可以帮我解答下问题吗?我发到收费问答板块了,具体内容大致就是,so层分析的时候,有段逻辑我不知道分析方法,介于本人目前是个只会静态分析的小白。希望大神可以帮忙讲解下

2021-7-27 13:32
0
雪    币: 5235
活跃值: (3260)
能力值: ( LV10,RANK:175 )
在线值:
发帖
回帖
粉丝
9
mb_mfdpqgiv 大佬,可以帮我解答下问题吗?我发到收费问答板块了,具体内容大致就是,so层分析的时候,有段逻辑我不知道分析方法,介于本人目前是个只会静态分析的小白。希望大神可以帮忙讲解下
你直接用tea-py库算出来不对吗
2021-7-28 22:20
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
额 前面看着感觉很不错 看到最后的个人想法 心里不禁一沉
2021-7-29 00:50
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
挤蹭菌衣 你直接用tea-py库算出来不对吗
算出来不对,然后我仔细看了so文件里面的逻辑,发现有原理是差不多,但是运算公式不对~正在学习如何hook这个函数,用的frida框架,却总是报错
2021-7-29 08:57
0
雪    币: 5235
活跃值: (3260)
能力值: ( LV10,RANK:175 )
在线值:
发帖
回帖
粉丝
12
mb_mfdpqgiv 算出来不对,然后我仔细看了so文件里面的逻辑,发现有原理是差不多,但是运算公式不对~正在学习如何hook这个函数,用的frida框架,却总是报错
你直接把tea-py的源码复制出来,然后对着ida找算法变化的地方改,py库里的解析肯定是对的
2021-7-29 10:54
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
挤蹭菌衣 你直接把tea-py的源码复制出来,然后对着ida找算法变化的地方改,py库里的解析肯定是对的
我确实是这样做的,最好结果仍然不正确~~所以我正在学习用frida框架去hook下输入输出~可是,可是~~~我全都照着论坛上其他大佬的做的,然而一运行就返回message的默认值~~目前正在debug找原因,您有没有什么好的建议?
2021-7-29 11:43
0
雪    币: 5235
活跃值: (3260)
能力值: ( LV10,RANK:175 )
在线值:
发帖
回帖
粉丝
14
mb_mfdpqgiv 我确实是这样做的,最好结果仍然不正确~~所以我正在学习用frida框架去hook下输入输出~可是,可是~~~我全都照着论坛上其他大佬的做的,然而一运行就返回message的默认值~~目前正在debug ...
这。。。不如去找肉丝大佬搞个虚拟机环境 
2021-7-29 23:47
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
挤蹭菌衣 这。。。不如去找肉丝大佬搞个虚拟机环境
肉丝大佬是??我虚拟环境应该是配置好了~~但是又遇到新的问题了~逆向路不好走阿
2021-7-30 14:11
0
雪    币: 5235
活跃值: (3260)
能力值: ( LV10,RANK:175 )
在线值:
发帖
回帖
粉丝
16
mb_mfdpqgiv 肉丝大佬是??我虚拟环境应该是配置好了~~但是又遇到新的问题了~逆向路不好走阿
ios区版主  最近出了一本书讲frida的
2021-7-30 14:31
0
雪    币: 147
活跃值: (462)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
可以来一期入门视频教程么
2021-7-30 16:06
0
雪    币: 2363
活跃值: (2280)
能力值: (RANK:400 )
在线值:
发帖
回帖
粉丝
19
学习了~
2021-8-13 11:15
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
支持一下
2023-3-26 21:37
0
雪    币: 39
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
21
完全看不懂
2023-3-28 07:48
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
22
这是要逆向破解嘛大哥?
2023-5-8 04:58
0
游客
登录 | 注册 方可回帖
返回
//