吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 10721|回复: 13
上一主题 下一主题
收起左侧

[Android 原创] cocos2dx游戏逆向实战

  [复制链接]
跳转到指定楼层
楼主
BeneficialWeb 发表于 2022-5-10 13:04 回帖奖励
本帖最后由 BeneficialWeb 于 2022-5-11 12:10 编辑

Cocos2dx游戏逆向实战

搞过cocos2dx游戏逆向的都知道lua的解密函数是下面这个:

int luaL_loadbuffer (lua_State *L,
                     const char *buff,
                     size_t sz,
                     const char *name);

网上有很多帖子,大多讲解了如何去解密lua代码和资源,然后回编译,重新签名打包apk,但是本文将采用一种新思路—通过对上述函数的hook,动态加载我们自己编写的lua脚步,实现劫持lua加载,这样就不需要回编译和打包apk的操作,实现外挂功能。目前cocos2dx的脚本类型主要有两种,一种是luac,另外一种是luajit.。本文讲解的是第一种luac,游戏的例子是这个:https://jltx.175game.com/ 可直接去官网下载apk。

Hook使用到的技术是Xposed hook 游戏so库加载 libgame.so。对于Android 9, xposed的核心代码如下所示:

@Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.qtz.game.jltx"))
            return;

        // It uses the class loader of the caller, which is usually the app, but would be Xposed as soon as you hook it. Please hook Runtime.loadLibrary() instead.
        // loadLibrary0(VMStack.getCallingClassLoader(), libname); Android 9
        XposedHelpers.findAndHookMethod("java.lang.Runtime", lpparam.classLoader,
                "loadLibrary0", ClassLoader.class,String.class, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                        super.beforeHookedMethod(param);
                    }

                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        super.afterHookedMethod(param);
                        ClassLoader fromClass = (ClassLoader) param.args[0];
                        String soName = (String) param.args[1];
                        XposedBridge.log("afterHookedMethod System.loadLibrary0("+soName+")");
                        // 不可hook多次,so可能会加载多次
                        // https://www.jianshu.com/p/93828be3ff58
                        if(soName.contains("game")){
                            // 将hook-lib.so拷贝到应用程序库目录
                            // cp /data/app/com.example.hellondk-mE2vo3RWkDDrlWW-T8gywA\=\=/lib/arm64/libhook-lib.so /data/app/com.qtz.game.jltx-NJFXwyKPB3COyuej66PaOQ\=\=/lib/arm64/
                            XposedBridge.log("find "+soName);
                            Object[] newArgs = new Object[]{fromClass, "hook-lib"};
                            XposedBridge.invokeOriginalMethod(param.method, param.thisObject, newArgs);
                        }
                    }
                });
    }

逻辑就是游戏刚加载完so库时是我们比较好的hook时机。此时加载我们的hook-lib.so实现对luaL_loadbuffer() 的动态hook。

hook框架使用了Dobby,一个安全大佬写的hook库。接下来是hook-lib.so的核心代码:

bool saveFile(const void* addr,int len,const char*outFileName) {
    bool success = false;
    FILE* file = fopen(outFileName,"wb+");
    if(file!=nullptr){
        fwrite(addr,len,1,file);
        fflush(file);
        fclose(file);
        success=true;
        chmod(outFileName,S_IRWXU|S_IRWXG|S_IRWXO);
    }else{
        MY_LOG_ERROR("[%s] fopen failed,error: %s.",__FUNCTION__ ,dlerror());
    }

    return success;
}

/*
int luaL_loadbuffer (lua_State *L,
                     const char *buff,
                     size_t sz,
                     const char *name);
*/

// original function copy
int (*luaL_loadbuffer_orig) (void* L,const char *buff,int size,const char* name) =nullptr;

// local function
int lual_loadbuffer_mod(void* L,const char* buff,int size,const char* name) {
    //MY_LOG_INFO("[dumplua] loadL_loadbuffer name: %s",name);
    std::string path = "/storage/emulated/0/Download/jltx";
    std::string modPath = "/storage/emulated/0/Download/jltx_modified";
    if(name[0]!='/'){
        path+='/';
        modPath+='/';
    }
    path+=name;
    modPath+=name;
    // get the file name
    std::string fileName = path;
    std::string modFileName = modPath;
    // get the path
    int pos = path.rfind("/");
    path = path.substr(0,pos);
    pos = modPath.rfind("/");
    modPath = modPath.substr(0,pos);
    //MY_LOG_INFO("[dumplua] path: %s",path.c_str());
    // create the directories
    std::__fs::filesystem::create_directories(path);
    std::__fs::filesystem::create_directories(modPath);

    // judgement the file's existence ,if not exist , save it
    bool isExist = std::__fs::filesystem::is_regular_file(fileName);
    if (!isExist)
        saveFile(buff,size,fileName.c_str());
    else{
        MY_LOG_INFO("[dumplua] %s has saved.",fileName.c_str());
    }

    isExist = std::__fs::filesystem::is_regular_file(modFileName);
    if(isExist){
        FILE* fp = fopen(modFileName.c_str(),"rb");
        MY_LOG_INFO("[hook] hijack the %s.",fileName.c_str());
        if(fp!=nullptr){
            fseek(fp,0,SEEK_END);
            long fileSize = ftell(fp);
            unsigned char* buffer = (unsigned char*)malloc(fileSize);
            if(buffer!=nullptr){
                fseek(fp,0,SEEK_SET);
                int readBytes = fread(buffer,fileSize,1,fp);
                if(readBytes>0){
                    int ret = luaL_loadbuffer_orig(L,(const char*)buffer,fileSize,modFileName.c_str());
                    free(buffer);
                    MY_LOG_INFO("[hook] ret: %d.",ret);
                    return ret;
                }
            }
        }

    }

    return luaL_loadbuffer_orig(L,buff,size,name);
}

void Hook(){
    //DobbyHook((void*)&Java_com_example_hellondk_MainActivity_print,(void*)new_print,(void**)&orig_print);
    MY_LOG_INFO("[dumplua] hook begin");
    void* handle = dlopen("libgame.so",RTLD_NOW);
    if(handle == nullptr){
        MY_LOG_INFO("[dumplua] dlopen err: %s.",dlerror());
        return;
    }else{
        MY_LOG_INFO("[dumplua] libgame.so dlopen OK!");
    }

    void *pluaL_loadbuffer = dlsym(handle,"luaL_loadbuffer");
    if(pluaL_loadbuffer == nullptr) {
        MY_LOG_ERROR("[dumplua] lua_loadbuffer not found!");
        MY_LOG_ERROR("[dumplua] dlsym err: %s.", dlerror());
    }else{
        MY_LOG_DEBUG("[dumplua] lual_loadbuffer found!");
        DobbyHook(pluaL_loadbuffer,(void*)&lual_loadbuffer_mod,(void**)&luaL_loadbuffer_orig);
    }
}

JNIEXPORT void JNICALL Java_com_example_hellondk_MainActivity_Unhook(JNIEnv* env,jobject thiz) {
    //DobbyDestroy((void*)&Java_com_example_hellondk_MainActivity_print);
}

}

jint JNI_OnLoad(JavaVM* vm,void* reserved){
    MY_LOG_INFO("Start Hook");

    Hook();

    return JNI_VERSION_1_6;
}

上面的代码逻辑就是在JNI_OnLoad时完成hook操作。同时我们备份原始的luac脚本到Download目录。然后通过LuacGUI.exe(这个工具可以在网上下载到)反编译成lua脚步源码。

在拿到源码后,我们可以进行修改,分析,阅读相关游戏逻辑。

在阅读上面的相关模块后,我们进行一定的修改,再使用luac.exe,回编译,放到jltx_modified的目录下,就能实现劫持加,实现作弊检测的移除,游戏对象的修改等等。

.\luac.exe -o K:\Reverse\Android\jltx_decrypt\data\user\0\com.qtz.game.jltx\files\q2.game.qtz.com\scripts\module\login\login.lua.out K:\Reverse\Android\jltx_decrypt\data\user\0\com.qtz.game.jltx\files\q2.game.qtz.com\scripts\module\login\login.lua

以下是一些作弊检测分析结果:

-- 字符串特征码
--\230\136\152\230\150\151\231\187\147\230\158\156
--module\battle\battle.lua 1287
--

-- 发送视频回放
  battle.sendBattleRecord()
-- 
  battle.sendResult(battleConst.STATE_OVER)
  server.rpc_server_fight_end(battle.battleType, effectMax, effectLv)
  MissionData.sendCachedRpcRequest()
  if battle.verifyStatus == battleConst.VERIFY_STATUS_CHEAT then
    tellme.show(T("\230\136\152\230\150\151\231\187\147\230\158\156\230\160\161\233\170\140\229\164\177\232\180\165!!!"))
  end

battle.sendResult 里有检测代码

function battle.checkCheat()
    battle.sendVerifyInfo()
    battle.sendImpeachInfo()
    battle.verifyBattleData()
    if battle.cheatInfo then
      battle.sendCheatInfo(battle.cheatInfo)
      battle.cheatInfo = nil
    end
end

最终统计后发送这个
server.rpc_server_fight_verify(tostring(checksum))

local infoStr = json.encode(info)
server.rpc_server_fight_impeach(infoStr)

rpc_server_fight_result
rpc_server_pve_fight_result

触发作弊事件
gEvent.trigEvent(gEvent.BATTLE_CHEAT, verifyInfo)

发送作弊信息
battle.sendCheatInfo(info)

server.rpc_server_client_error(infoStr)

battle.onCheat

fight_obj.lua

699
function Obj:recordVerifyInfo(info)
    self.cheatInfo = info
    gEvent.trigEvent(gEvent.BATTLE_CHEAT, info)
end

function Obj:verifyAttr(k, v, randomKey)
    local clones = self.fightAttrClones
    for i, clone in ipairs(clones) do
      if not clone[k] then
        return nil
      end
      local cv
      if i == 1 then
        cv = randomKey - clone[k] / 373
      elseif i == 2 then
        cv = clone[k] - randomKey
        cv = cv - math_floor(v / 20)
        cv = cv <= 1 and cv >= -1 and v or v + 2
      else
        cv = clone[k] / i
      end
      local diff = v - cv
      if not (diff > 0) or not diff then
        diff = -diff
      end
      return nil
      -- if diff > 1 then
      --   logger.warn("It is cheating!!!", i, self:getName(), self:getFightObjId(), diff)
      --   print("It is cheating!!!", self:getName(), self:getFightObjId(), diff)
      --   return {
      --     k = k,
      --     v = v,
      --     cv = cv,
      --     diff = diff,
      --     name = self:getName(),
      --     clone1 = clones[1][k],
      --     clone2 = clones[2][k],
      --     id = self:getFightObjId(),
      --     rkey = randomKey
      --   }
      -- end
    end
    return nil
  end

  function Obj:isVerify()
    return false
  end

  function Obj:isVerify()
    return true
  end
  local VERIFY_ATTRS = {
    [FAConst.FA_HP] = true,
    [FAConst.FA_DAMAGE] = true,
    [FAConst.FA_ATTACK_SPEED] = true
  }

  检测血量,伤害,攻击速度

  tellme.show 显示文字的代码
检测特征码 rpc_server_fingerprint
checkTool

特征码 \232\167\166\229\143\145\229\164\150\230\140\130\230\163\128\230\159\165
checkTool

battle.sendCheatInfo(info)

battle.checkCheat()

self:recordVerifyInfo(cheatInfo)

针对游戏对象的检测原则上过这个事件就ok了
触发的就是这个事件
function Obj:recordVerifyInfo(info)
  self.cheatInfo = info
  --gEvent.trigEvent(gEvent.BATTLE_CHEAT, info)
end

在阅读源码的过程中,发现游戏采用了UTF8编码字符串,

为了方便分析特意写了一个转换工具。

以下是代码涉及的仓库地址:

https://github.com/BeneficialCode/utf8_tool

https://github.com/BeneficialCode/XposedDev

https://github.com/BeneficialCode/HelloNDK

解密后的大部分lua源码地址

https://github.com/BeneficialCode/jltx_decrypt

已通知官方客服,由于研究游戏安全的过程中,被其他用户举报,导致账户被封。反馈给客服后,不予解封。对方客服以“如果对我们游戏造成恶劣影响或者损失,我们会保留相应的法律权利并进行追责行为。”搪塞,以上内容仅供学习,勿用于非法用途!

免费评分

参与人数 11吾爱币 +12 热心值 +10 收起 理由
graffee + 1 + 1 谢谢@Thanks!
/│\云。 + 1 + 1 谢谢@Thanks!
笙若 + 1 + 1 谢谢@Thanks!
chinawolf2000 + 1 + 1 热心回复!
xhever + 1 我很赞同!
ZJevon + 2 + 1 我很赞同!
viply + 1 + 1 盘他呀
gunxsword + 1 + 1 谢谢@Thanks!
a1046830 + 1 + 1 用心讨论,共获提升!
积积 + 1 + 1 我很赞同!
loo1221ool + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

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

推荐
积积 发表于 2022-5-10 14:23
未授权的行为小心被请喝茶
推荐
seahai9195 发表于 2024-8-15 13:44
最近一个面试,就遇到一个逆向游戏 用到Cocos2dx  还不知道如何下手。直接用jadx反编译apk,发现代码缺失太多。
完全看不到游戏的运行逻辑  看到这个 有一些思路了。感谢大佬
3#
张海洋 发表于 2022-5-11 09:04
4#
ZJevon 发表于 2022-5-11 09:32
律师函警告&#9888;
5#
呱呱生 发表于 2022-5-11 11:33
好像很好玩
6#
 楼主| BeneficialWeb 发表于 2022-5-11 11:45 |楼主
关于今天早上接到广东广州派出所电话号码02038343497,让我删jltx_decrypt库保平安的事,坛友们怎么看?
7#
/│\云。 发表于 2022-5-12 08:52
BeneficialWeb 发表于 2022-5-11 11:45
关于今天早上接到广东广州派出所电话号码02038343497,让我删jltx_decrypt库保平安的事,坛友们怎么看?

删归删 存脑袋的还能删不成
8#
Bruce_HD 发表于 2022-5-12 09:59
转一转,看一看。
9#
metoo2 发表于 2022-5-12 14:28
有点意思,厉害了,楼主
10#
chigangcxk 发表于 2023-3-15 23:49
楼主 能加下qq请教一下吗
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2024-11-24 10:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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