吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8825|回复: 41
收起左侧

[Android 原创] cocos2d逆向入门和某捕鱼游戏分析

  [复制链接]
L剑仙 发表于 2021-7-19 23:24
本帖最后由 L剑仙 于 2021-7-20 21:07 编辑

[TOC]

初步认识cocos2d-x

先clone到本地

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

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

img

framework_architecture_v4.png

从c++进入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

applicationDidFinishLaunching函数

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

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;
}

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

LuaStack::executeScriptFile

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;
}

LuaStack::luaLoadBuffer

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

  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;
}

而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世界的调用逻辑

AppDelegate::applicationDidFinishLaunching

{

  setXXTEAKeyAndSign

  executeScriptFile

  {

​    getDataFromFile

​    luaLoadBuffer

​    {

​      xxtea_decrypt

​      luaL_loadbuffer

​      {

​        luajit

​       }

​     }

​    executeFunction

  }

}

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文件夹里面已经暴露很多信息了,配置,下载的游戏,更新等等

QQ图片20210720113613.png

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

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

QQ图片20210719213107.png

根据之前对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这个字符串进一步验证了正确性

QQ图片20210719214220.png

int __fastcall AppDelegate::applicationDidFinishLaunching(AppDelegate *this)
{

//前面是一堆初始化啥的没用的

....

(**(v7 + 1) + 116))(*(v7 + 1), "ZX_01RdsF~@!R8", 14, "ZX_CS@56#D~d@dud", 16);
if ( (*(*v7 + 28))(v7, "base/src/main.lua") )
return 0;
v26 = GetMCKernel();
if ( !v26 )
return 0;
v27 = *(this + 145); if ( v27 ) v27 += 580; v28 = (*(*v26 + 20))(v26, v27);
v29 = *(cocos2d::Director::getInstance(v28) + 152);
v32[0] = AppDelegate::GlobalUpdate;
v32[1] = -8;
cocos2d::Scheduler::schedule(v29, AppDelegate::GlobalUpdate, -8, this + 4, 0.0, 0xFFFFFFFE, 0.0, 0);
return v25;
}

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

unsigned char* result = xxtea_decrypt((unsigned char*)chunk + _xxteaSignLen,
                                          (xxtea_long)chunkSize - _xxteaSignLen,
                                          (unsigned char*)_xxteaKey,
                                          (xxtea_long)_xxteaKeyLen,
                                          &len);

unsigned char *xxtea_decrypt(unsigned char *data, xxtea_long data_len, unsigned char *key, xxtea_long key_len, xxtea_long *ret_length)
{
    unsigned char *result;

    *ret_length = 0;

    if (key_len < 16) {
        unsigned char *key2 = fix_key_length(key, key_len);
        result = do_xxtea_decrypt(data, data_len, key2, ret_length);
        free(key2);
    }
    else
    {
        result = do_xxtea_decrypt(data, data_len, key, ret_length);
    }

    return result;
}

或者直接pip install xxtea-py

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

import xxtea
import os
   orig_path=“”//初始路径
   new_path=“”//存储路径
   xxtea_sign=“”
   xxtea_key=“”
   orig_file = open(orig_path, "rb")
   encrypt_bytes = orig_file.read()
   orig_file.close()
   decrypt_bytes = xxtea.decrypt(encrypt_bytes[len(xxtea_sign):], xxtea_key)
   new_file = open(new_path, "wb")
   new_file.write(decrypt_bytes)
   new_file.close()

下面开始分析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就可以加金币了

function BulletLayer:on_bullet_crash_fish(bullet_obj, t_fish_list)
    local scene = self:get_scene()
    local master_id = bullet_obj:get_master_id()
    local bullet_uid = bullet_obj:get_bullet_uid()
    local self_player_id = self:get_self_player_id()
    local num = #t_fish_list
    if(num >= 2) then
        local fish_layer = self:get_fish_layer()
        table.sort(t_fish_list, function(uid1, uid2)
        local fish_obj1 = fish_layer:get_fish_by_uid(uid1);local fish_obj2 = fish_layer:get_fish_by_uid(uid2)
            local zorder1 = fish_obj1:getLocalZOrder();local zorder2 = fish_obj2:getLocalZOrder()
            if(zorder1 > zorder2) then return true end
            end)
    end
    local fish_uid = t_fish_list[1]
    if(master_id == self_player_id) then
        scene:on_self_bullet_crash_fish(master_id, bullet_uid, fish_uid)
        --scene:on_self_bullet_crash_fish_test(master_id, bullet_uid, fish_uid)
    end
    scene:on_bullet_crash_fish(master_id, bullet_uid, t_fish_list)
end 

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

function AnimationLayer:play_catch_fish_earn_money_self(fish_gold, fish_odds)
    local earn_money_node = self:reuse_new_earn_money_view_node('zhuanqianle_buyu', self, 0)
    local pos = cc.p(sizeVisble.width/2, sizeVisble.height/2)
    local one_add_gold = math.floor(fish_gold/12)
    local min = self:get_earn_money_one_add_gold_min()
    if(one_add_gold < min) then one_add_gold = min end
    earn_money_node:setPosition(pos);  earn_money_node:set_gain_gold(fish_gold)
    earn_money_node:set_one_add_gold(one_add_gold); earn_money_node:set_cur_gold(0)
    local delay_time = cc.DelayTime:create(4)
    local call_back = cc.CallFunc:create(handler(self, self.call_back_reuse_obj))
    local seq = cc.Sequence:create(delay_time, call_back);
    earn_money_node:setScale(1)
    earn_money_node:runAction(seq);
    earn_money_node:play_ani()
end

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

function AnimationLayer:on_recv_player_catch_fish(player_id, fish_uid, fish_gold)
    local fish_layer = self:get_fish_layer()
    local fish_obj = fish_layer:get_fish_by_uid(fish_uid)
    if(fish_obj == nil) then return end  
    local data_center = self:get_data_center()
    local fish_id = fish_obj:get_fish_id()
    local fish_info = data_center:get_fish_info(fish_id)
    if(fish_info == nil) then return end 
    local fish_type = fish_info.type
    if(fish_type == GameDefine.fish_type.wang) then
        local other_fish_obj_list = self:get_other_fish_yi_wang_da_jin_list(fish_obj, GameDefine.fish_type.wang)
        local num = #other_fish_obj_list
        if(num > 0) then
            local fish_odds = self:get_catch_fish_odds(fish_obj, other_fish_obj_list)
            self:on_player_catch_fish_yi_wang_da_jin(player_id, fish_obj, fish_gold, fish_odds, other_fish_obj_list)
        end
        self:on_player_catch_fish_drop_fish_gold(player_id, fish_obj, 0, 0)
        return
    end
    if(fish_type == GameDefine.fish_type.bomb) then
        play_drop_gold = 0
        local other_fish_list = self:get_bomb_fish_effect_fish_list()
        local num = #other_fish_list
        if(num > 0) then
           local fish_odds = self:get_catch_fish_odds(fish_obj, other_fish_list)
            self:on_player_catch_fish_bomb(player_id, fish_obj, fish_gold, fish_odds, other_fish_list)
        end
        self:on_player_catch_fish_drop_fish_gold(player_id, fish_obj, 0, 0)
        return
    end
    local fish_odds = fish_info.mulriple_max;local fish_name = fish_info.name
    self:on_player_cath_fish_da_zhuan_pan(player_id, fish_type, fish_gold, fish_name)
    self:on_player_catch_fish_drop_fish_gold(player_id, fish_obj, fish_gold, fish_odds)
end

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

    {
       room_style= 3,
       cannon_id= 109,
       odds_min= 7,
       odds_max= 10,
       level= 3,
       res= "pao3_buyu",
       bullet= "zd9",
       net= "yuwang3_buyu",
       time= 200,
       sound= 109
    }

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

          {
             id= 302,
             name= "60倍组合鱼",
             packet= "4lian_buyu",
             crash_model= 101,
             mulriple_min= 60,
             mulriple_max= 60,
             type= 101,
             zoder= 21,
             die_sound= "fish14_1",
             die_type= 4,
             copy_num= 4
          },
          {
             id= 402,
             name= "大章鱼",
             packet= "yu22_buyu",
             crash_model= 16,
             mulriple_min= 300,
             mulriple_max= 300,
             type= 2,
             zoder= 30,
             bron_sound= "f_wb_3",
             die_sound= "fish33_1",
             die_type= 5,
             copy_num= 4
          },

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

[md]# 个人想法

如何实现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](https://www.cnblogs.com/lxjshuju/p/7028393.html)

[安卓逆向之Luac解密反编译](https://www.yuanrenxue.com/app-crawl/luac.html)[/md]

免费评分

参与人数 16吾爱币 +16 热心值 +16 收起 理由
Nattevak + 1 + 1 我很赞同!
梦想遥不可及 + 1 + 1 楼主有些地方不解希望能请教一下,怎么联系你
kakaxi007 + 1 + 1 我很赞同!
xiaolong5944 + 1 我很赞同!
lyl610abc + 3 + 1 我很赞同!
maiming123 + 1 我很赞同!
jlwmq + 1 + 1 热心回复!
Nickie + 1 + 1 热心回复!
ForGot_227 + 1 + 1 用心讨论,共获提升!
_小白 + 1 + 1 我很赞同!
mafeixiangdj + 1 + 1 我很赞同!
yuan71058 + 1 + 1 谢谢@Thanks!
大瑜 + 1 + 1 用心讨论,共获提升!
gunxsword + 1 + 1 我很赞同!
努力加载中 + 1 + 1 热心回复!
huaishion + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

jiaozi282 发表于 2023-4-6 10:50
大神好, 之前认识同样一位大神, 他当时主要做网页、PC端捕鱼游戏,他做了一款辅助就是可以屏蔽(鱼种、其他玩家子弹、调整炮弹速度、左右攻击或者锁定等),感觉你们的思路很是接近,都很强, 不知道有没机会能够认识您一下, 我的V chy88880912   如果有幸能跟您学习 在下一定奉上丰厚的拜师费, 望大神翻牌~!
夜不负红颜 发表于 2022-8-16 17:12
大佬好,参考您的方式 修改完lua文件后,把修改好的文件更新原来的路径后,进不了游戏房间了 这个该怎么处理呢?  如果我整个捕鱼游戏 解密后在更新回原来的路径,直接提示下载游戏失败,望指教
闷骚小贱男 发表于 2021-7-20 07:47
{:1_908:图片上传的方法是不是不太对...
图片怎么都挂了
 楼主| L剑仙 发表于 2021-7-20 08:16
闷骚小贱男 发表于 2021-7-20 07:47
{:1_908:图片上传的方法是不是不太对...
图片怎么都挂了

有空我在编辑一下
芽衣 发表于 2021-7-20 08:34
为什么代码字体也那么大
zx2700 发表于 2021-7-20 08:38
看看,学习一下 lz
黄hsir 发表于 2021-7-20 08:43
想玩修改后的,还有图全挂了
ck07880506 发表于 2021-7-20 08:44
超级棒的文章,好好好
飘零星夜 发表于 2021-7-20 08:55
这种游戏 专门骗充值....  谢谢楼主的技术指导
Talrity 发表于 2021-7-20 09:13
超级棒的好文章
林瑾瑜 发表于 2021-7-20 09:52
感谢大神分享!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-27 01:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表