首页
社区
课程
招聘
[原创]Cocos Creator游戏过加密
发表于: 1天前 589

[原创]Cocos Creator游戏过加密

1天前
589

最近在看一个Cocos Creator 类型游戏的逻辑时,遇到了游戏界某厂商的加固(到处都是这家的身影,懂的都懂),看了下应该是免费版,还是比较简单的。

1. 原理

Cocos Creator 是一个游戏引擎。使用 JavaScript/TypeScript 写游戏逻辑,引擎负责渲染、物理、事件。在代码打包时通过选择某加固,就会将main.js加密,在运行时解密交由V8引擎执行。


2. 判断

解压apk后,在assets中找到main.js,这个是游戏运行时的主要代码逻辑文件。

通过运行

xxd apk/extracted/assets/main.js | head -1

可以很清晰看到某加固厂商的标识。

6e 65 74 65 61 73 65


3. 分析

3.1 V8 引擎是怎么执行JS代码的

V8 执行一段 JS 源码,分三步:

  1.  拿到源码字符串(一段文本,比如 "function foo(){...}")。

  2. 编译:V8 把源码解析、编译成字节码 bytecode

  3. 执行字节码

第 2 步编译完成后,第 1 步那段"源码文本"就没用了,会被释放掉——V8 只留字节码。所以通过dump内存是不可行(已踩坑)。通过查看V8引擎的源码,发现V8 提供了一个接口来接收"源码文本":

v8::String::NewExternalOneByte(Isolate*, ExternalOneByteStringResource* resource)
  • Cocos 解密出明文后,不是把它拷进 V8,而是把明文 buffer 的指针包成一个 ExternalOneByteStringResource 对象,通过 NewExternalOneByte 交给 V8。

  • 在这个函数被调用的一瞬间,参数里就装着"解密后明文的指针 + 长度"。这就是可以Hook的点。

3.2 需要注意的点

这个点需要在源码"被交给 V8、还没编译"的那一刻拦截。而这一刻发生在启动后 1~2 秒内,所以必须冷启动(让进程挂起启动、先装好钩子再放行)


4. 破解

4.1  环境与工具

环境: Ubuntu 22.04

工具: Florida 17.10.1(frida去特征版)

手机:Pixel 6 自编译AOSP 15

4.2 找目标函数的符号名

v8 引擎是在 libcocos.so,所以先找到对应的符号名

readelf -W --dyn-syms libcocos.so | grep NewExternalOneByte

4.3 HOOK脚本

// 输出到 App 私有目录
var OUT = '/data/data/xx.xx.xx/js_all.txt';
var f = null, cnt = 0, installed = false;
function openFile(){ if(!f) f = new File(OUT, 'wb'); }

// 只保留"register/function/exports"的字符串,过滤噪声
function dumpIf(s, tag){
  if (s && s.length > 1200 && (s.indexOf('register')>=0 || s.indexOf('function')>=0 || s.indexOf('exports')>=0)){
    openFile();
    f.write('\n=====' + tag + '#' + (cnt++) + ' len=' + s.length + '=====\n');
    f.write(s); f.flush();
  }
}

function exp(mod, name){
  try { return Process.getModuleByName(mod).getExportByName(name); } catch(e){}
  try { return Module.getGlobalExportByName(name); } catch(e){}
  return null;
}

function install(){
  if (installed) return true;
  var oneByte = exp('libcocos.so', '_ZN2v86String18NewExternalOneByteEPNS_7IsolateEPNS0_29ExternalOneByteStringResourceE');
  if (!oneByte) return false;                 // 库没加载/符号没解析到 → 先返回,等 dlopen 回调再试

  Interceptor.attach(oneByte, { onEnter: function(a){
    try {
      // a[1] = 第2个参数 = ExternalOneByteStringResource* 对象指针
      // 该对象内存布局: [+0 vtable指针][+8 data指针][+16 length]
      var res = a[1];
      var dataPtr = res.add(Process.pointerSize).readPointer();        
      var len     = res.add(Process.pointerSize*2).readU64().toNumber();
      var s = (len>0 && len<30000000) ? dataPtr.readUtf8String(len) : dataPtr.readCString();
      dumpIf(s, 'EXT1');                        // 拿到明文
    } catch(e){}
  }});
  installed = true;
  send('[*] V8 string hook installed');
  return true;
}

if (!install()){
  var dl = exp('libc.so', 'android_dlopen_ext') || exp('libdl.so', 'android_dlopen_ext');
  if (dl){
    Interceptor.attach(dl, {
      onEnter: function(a){ try { this.p = a[0].readCString(); } catch(e){} },
      onLeave: function(){ if (this.p && this.p.indexOf('libcocos')>=0){ install(); } }
    });
    send('[*] waiting for libcocos via dlopen');
  } else send('[!] no dlopen');
}

4.4 导出代码


至此整个代码已被解密,可分析逻辑。


[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

收藏
免费 0
打赏
分享
最新回复 (2)
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
一般用evalString符号,可以在args一次性拿到buf,文件名,长度,不用扣布局
9小时前
0
雪    币: 303
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
Zepp7289 一般用evalString符号,可以在args一次性拿到buf,文件名,长度,不用扣布局
evalString是更好,可以拿到源码树,只需要多一步计算偏移就好
8小时前
0
游客
登录 | 注册 方可回帖
返回