淺談Cocos2djs逆向
前言
簡單聊一下cocos2djs手遊的逆向,有任何相關想法歡迎和我討論^^
一些概念
列出一些個人認為比較有用的概念:
- Cocos遊戲的兩大開發工具分別是
CocosCreator和CocosStudio,區別是前者是cocos2djs專用的開發工具,後者則是cocos2d-lua、cocos2d-cpp那些。

- 使用
Cocos Creator 2開發的手遊,生成的關鍵so默認名稱是libcocos2djs.so - 使用
Cocos Creator 3開發的手遊,生成的關鍵so默認名稱是libcocos.so( 入口函數非applicationDidFinishLaunching) - Cocos Creator在構建時可以選擇是否對
.js腳本進行加密&壓縮,而加密算法固定是xxtea,還可以選擇是否使用Zip壓縮

libcocos2djs.so裡的AppDelegate::applicationDidFinishLaunching是入口函數,可以從這裡開始進行分析- Cocos2djs是Cocos2d-x的一個分支,因此https://github.com/cocos2d/cocos2d-x源碼同樣適用於Cocos2djs
自己寫一個Demo
自己寫一個Demo來分析的好處是能夠快速地判斷某個錯誤是由於被檢測到?還是本來就會如此?
版本信息
嘗試過2.4.2、2.4.6兩個版本,都構建失敗,最終成功的版本信息如下:
- 編輯器版本:
Creator 2.4.13( 2系列裡的最高版本,低版本在AS編譯時會報一堆錯誤 ) - ndk版本:
23.1.7779620 project/build.gradle:classpath 'com.android.tools.build:gradle:8.0.2'project/gradle/gradle-wrapper.properties:distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
Cocos Creator基礎用法
由於本人不懂cocos遊戲開發,只好直接用官方的Hello World模板。

首先要設置SDK和NDK路徑

然後構建的參數設置如下,主要需要設置以下兩點:
- 加密腳本:全都勾上,密鑰用默認的
- Source Map:保留符號,這樣IDA在打開時才能看到函數名

我使用Cocos Creator能順利構建,但無法編譯,只好改用Android Studio來編譯。
使用Android Studio打開build\jsb-link\frameworks\runtime-src\proj.android-studio,然後就可以按正常AS流程進行編譯
Demo如下所示,在中心輸出了Hello, World!。

jsc腳本解密
上述Demo構建中有一個選項是【加密腳本】,它會將js腳本通過xxtea算法加密成.jsc。
而遊戲的一些功能就會通過js腳本來實現,因此cocos2djs逆向首要事件就是將.jsc解密,通常.jsc會存放在apk內的assets目錄下

獲取解密key
方法一:從applicationDidFinishLaunching入手

方法二:HOOK
- hook
set_xxtea_key
// soName: libcocos2djs.so
function hook_jsb_set_xxtea_key(soName) {let set_xxtea_key = Module.findExportByName(soName, "_Z17jsb_set_xxtea_keyRKNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE");Interceptor.attach(set_xxtea_key,{onEnter(args){console.log("xxtea key: ", args[0].readCString())},onLeave(retval){}})
}
- hook
xxtea_decrypt
function hook_xxtea_decrypt(soName) {let set_xxtea_key = Module.findExportByName(soName, "xxtea_decrypt");Interceptor.attach(set_xxtea_key,{onEnter(args){console.log("xxtea key: ", args[2].readCString())},onLeave(retval){}})
}
python加解密腳本
一次性解密output_dir目錄下所有.jsc,並在input_dir生成與output_dir同樣的目錄結構。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
|
jsc文件的2種讀取方式
為實現對遊戲正常功能的干涉,顯然需要修改遊戲執行的js腳本。而替換.jsc文件是其中一種思路,前提是要找到讀取.jsc文件的地方。
方式一:從apk裡讀取
我自己編譯的Demo就是以這種方式讀取/data/app/XXX/base.apk裡assets目錄內的.jsc文件。
cocos引擎默認使用xxtea算法來對.jsc等腳本進行加密,因此讀取.jsc的操作定然在xxtea_decrypt之前。
跟cocos2d-x源碼,找使用xxtea_decrypt的地方,可以定位到LuaStack::luaLoadChunksFromZIP

向上跟會發現它的bytes數據是由getDataFromFile函數獲取

繼續跟getDataFromFile的邏輯,它會調用getContents,而getContents裡是調用fopen來打開,但奇怪的是hook fopen卻沒有發現它有打開任何.jsc文件


後來發現調用的並非FileUtils::getContents,而是FileUtilsAndroid::getContents。
它其中一個分支是調用libandroid.so的AAsset_read來讀取.jsc數據,調用AAssetManager_open來打開.jsc文件。

繼續對AAssetManager_open進行深入分析( 在線源碼 ),目的是找到能夠IO重定向的點:
AAssetManager_open裡調用了AssetManager::open函數
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
AssetManager::open調用openNonAssetInPathLocked
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
AssetManager::openNonAssetInPathLocked先判斷assets是位於.gz還是.zip內,而.apk與.zip基本等價,因此理應會走else分支。
| 1 |
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
嘗試繼續跟剛剛hook失敗的AssetManager::getZipFileLocked,它調用的是AssetManager::ZipSet::getZip。
| 1 |
|
| 1 2 3 4 5 6 7 |
|
ZipSet::getZip會調用SharedZip::getZip,後者直接返回mZipFile。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
尋找mZipFile賦值的地方,最終會找到是由ZipFileRO::open(mPath.string())賦值。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
| 1 |
|
方式二:從應用的數據目錄裡讀取
無論是方式一還是方式二,.jsc數據都是通過getDataFromFile獲取。而getDataFromFile裡調用了getContents。
| 1 |
|
在方式一中,我一開始看的是FileUtils::getContents,但其實是FileUtilsAndroid::getContents才對。
只有當fullPath[0] == '/'時才會調用FileUtils::getContents,而FileUtils::getContents會調用fopen來打開.jsc
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
替換思路
正常來說有以下幾種替換腳本的思路:
-
找到讀取
.jsc文件的地方進行IO重定向。 -
直接進行字節替換,即替換
xxtea_decypt解密前的.jsc字節數據,或者替換xxtea_decypt解密後的明文.js腳本。這裡的替換是指開闢一片新內存,將新的數據放到這片內存,然後替換指針的指向。
-
直接替換apk裡的
.jsc,然後重打包apk。 -
替換js明文,不是像
2那樣開闢一片新內存,而是直接修改原本內存的明文js數據。
經測試後發現只有1、3、4是可行的,2會導致APP卡死( 原因不明??? )。
思路一實現
從上述可知第一種.jsc讀取方式會先調用ZipFileRO::open(mPath.string())來打開apk,之後再通過AAssetManager_open來獲取.jsc。
hook ZipFileRO::open看看傳入的參數是什麼。
function hook_ZipFile_open(flag) {let ZipFile_open = Module.getExportByName("libandroidfw.so", "_ZN7android9ZipFileRO4openEPKc"); console.log("ZipFile_open: ", ZipFile_open)return Interceptor.attach(ZipFile_open,{onEnter: function (args) {console.log("arg0: ", args[0].readCString());},onLeave: function (retval) {}});
}
可以看到其中一條是當前APK的路徑,顯然assets也是從這裡取的,因此這裡是一個可以嘗試重定向點,先需構造一個fake.apk push 到/data/app/XXX/下,然後hook IO重定向到fake.apk實現替換。
![]()
對我自己編譯的Demo而言,無論是以apktool解包&重打包的方式,還是直接解壓縮&重壓縮&手動命名的方式來構建fake.apk都是可行的,但要記得賦予fake.apk最低644的權限。
以下是我使用上述方法在我的Demo中實踐的效果,成功修改中心的字符串。

但感覺這種方式的實用性較低( 什至不如直接重打包… )
思路二嘗試(失敗)
連這樣僅替換指針指向都會導致APP卡死??
function hook_xxtea_decrypt() {Interceptor.attach(Module.findExportByName("libcocos2djs.so", "xxtea_decrypt"), {onEnter(args) {let jsc_data = args[0];let size = args[1].toInt32();let key = args[2].readCString();let key_len = args[3].toInt32();this.arg4 = args[4];let target_list = [0x15, 0x43, 0x73];let flag = true;for (let i = 0; i < target_list.length; i++) {if (target_list[i] != Memory.readU8(jsc_data.add(i))) {flag = false;}}this.flag = flag;if (flag) {let new_size = size;let newAddress = Memory.alloc(new_size);Memory.protect(newAddress, new_size, "rwx")Memory.protect(args[0], new_size, "rwx")Memory.writeByteArray(newAddress, jsc_data.readByteArray(new_size))args[0] = newAddress;}},onLeave(retval) {}})}
思路四實現
參考這位大佬的文章可知cocos2djs內置的v8引擎最終通過evalString來執行.jsc解密後的js代碼。
在正式替換前,最好先通過hook evalString的方式保存一份目標js( 因為遊戲的熱更新策略等原因,可能導致evalString執行的js代碼與你從apk裡手動解密.jsc得到的js腳本有所不同 )。
function saveJscode(jscode, path) {var fopenPtr = Module.findExportByName("libc.so", "fopen");var fopen = new NativeFunction(fopenPtr, 'pointer', ['pointer', 'pointer']);var fclosePtr = Module.findExportByName("libc.so", "fclose");var fclose = new NativeFunction(fclosePtr, 'int', ['pointer']);var fseekPtr = Module.findExportByName("libc.so", "fseek");var fseek = new NativeFunction(fseekPtr, 'int', ['pointer', 'int', 'int']);var ftellPtr = Module.findExportByName("libc.so", "ftell");var ftell = new NativeFunction(ftellPtr, 'int', ['pointer']);var freadPtr = Module.findExportByName("libc.so", "fread");var fread = new NativeFunction(freadPtr, 'int', ['pointer', 'int', 'int', 'pointer']);var fwritePtr = Module.findExportByName("libc.so", "fwrite");var fwrite = new NativeFunction(fwritePtr, 'int', ['pointer', 'int', 'int', 'pointer']);let newPath = Memory.allocUtf8String(path);let openMode = Memory.allocUtf8String('w');let str = Memory.allocUtf8String(jscode);let file = fopen(newPath, openMode);if (file != null) {fwrite(str, jscode.length, 1, file)fclose(file);}return null;
}function hook_evalString() {Interceptor.attach(Module.findExportByName("libcocos2djs.so", "_ZN2se12ScriptEngine10evalStringEPKclPNS_5ValueES2_"), {onEnter(args) {let path = args[4].readCString();path = path == null ? "" : path;let jscode = args[1];let size = args[2].toInt32();if (path.indexOf("assets/script/index.jsc") != -1) {saveJscode(jscode.readCString(), "/data/data/XXXXXXX/test.js");}}})
}
利用Memory.scan來找到修改的位置
function findReplaceAddr(startAddr, size, pattern) {Memory.scan(startAddr, size, pattern, {onMatch(address, size) {console.log("target offset: ", ptr(address - startAddr))return 'stop';},onComplete() {console.log('Memory.scan() complete');}});
}function hook_evalString() {Interceptor.attach(Module.findExportByName("libcocos2djs.so", "_ZN2se12ScriptEngine10evalStringEPKclPNS_5ValueES2_"), {onEnter(args) {let path = args[4].readCString();path = path == null ? "" : path;let jscode = args[1];let size = args[2].toInt32();if (path.indexOf("assets/script/index.jsc") != -1) {let pattern = "76 61 72 20 65 20 3D 20 64 2E 50 6C 61 79 65 72 41 74 74 72 69 62 75 74 65 43 6F 6E 66 69 67 2E 67 65 74 44 72 65 61 6D 48 6C 70 65 49 74 65 6D 44 72 6F 70 28 29 2C";findReplaceAddr(jscode, size, pattern);}}})
}
最後以Memory.writeU8來逐字節修改,不用Memory.writeUtf8String的原因是它默認會在最終添加'\0'而導致報錯。
function replaceEvalString(jscode, offset, replaceStr) {for (let i = 0; i < replaceStr.length; i++) {Memory.writeU8(jscode.add(offset + i), replaceStr.charCodeAt(i))}
}// 例:
function cheatAutoChopTree(jscode) {let replaceStr = 'true || " "';replaceEvalString(jscode, 0x3861f6, replaceStr)
}
某砍樹手遊實踐
以某款砍樹遊戲來進行簡單的實踐。
遊戲有自動砍樹的功能,但需要符合一定條件
![]()
如何找到對應的邏輯在哪個.jsc中?直接搜字符串就可以。
利用上述替換思路4來修改對應的js判斷邏輯,最終效果:

結語
思路4那種替換手段有大小限制,不能隨意地修改,暫時還未找到能隨意修改的手段,有知道的大佬還請不嗇賜教,有任何想法也歡迎交流^^
後記
在評論區的一位大佬指點下,終於是找到一種更優的替換方案,相比起思路4來說要方便太多了。
最開始時我其實也嘗試過這種直接的js明文替換,但APP會卡死/閃退,現在才發現是frida的api所致,那時在開辟內存空間時使用了Memory.alloc、Memory.allocUtf8String,改成使用libc.so的malloc就不會閃退了,具體為什麼會這樣我也不清楚,看看以後有沒有機會研究下frida的源碼吧^^
相关文章:
淺談Cocos2djs逆向
前言 簡單聊一下cocos2djs手遊的逆向,有任何相關想法歡迎和我討論^^ 一些概念 列出一些個人認為比較有用的概念: Cocos遊戲的兩大開發工具分別是CocosCreator和CocosStudio,區別是前者是cocos2djs專用的開發工具,後者則是coco…...
【ROS2】RViz2加载URDF模型文件
1、RViz2加载URDF模型文件 1)运行RViz2 rviz22)添加组件:RobotModel 3)选择通过文件添加 4)选择URDF文件,此时会报错,需要修改Fixed Frame为map即可 5)因为没有坐标转换,依然会报错,下面尝试解决 2、运行坐标转换节点 1)运行ROS节点:robot_state_publishe...
Unity导入特效,混合模式无效问题
检查spine导出设置与Unity导入设置是否一致 检查Blend Mode Materials是否勾选 检查是否使用导入时产生的对应混合模式的材质,混合模式不适用默认材质 这里选导入时生成的材质...
el-table自定义按钮控制扩展expand
需求:自定义按钮实现表格扩展内容的展开和收起,实现如下: 将type“expand”的表格列的宽度设置为width"1",让该操作列不展示出来,然后通过ref动态调用组件的内部方法toggleRowExpansion(row, row.expanded)控…...
opencv CV_TM_SQDIFF未定义标识符
opencv CV_TM_SQDIFF未定义标识符 opencv4部分命名发生变换,将CV_WINDOW_AUTOSIZE改为WINDOW_AUTOSIZE;CV_TM_SQDIFF_NORMED改为TM_SQDIFF_NORMED。...
2024acl论文体悟
总结分析归纳 模型架构与训练方法:一些论文关注于改进大语言模型的架构和训练方法,以提高其性能和效率。例如,“Quantized Side Tuning: Fast and Memory-Efficient Tuning of Quantized Large Language Models”提出了一种量化侧调优方法&a…...
【Git原理与使用】版本回退reset 详细介绍、撤销修改、删除文件
目录 一、版本回退 reset 1.1 指令: 1.2 参数说明: 1.3 演示: 二、撤销修改 情况一:对于工作区的代码,还没有 add 情况二:已经 add ,但没有 commit 情况三:已经 add &…...
反规范化带来的数据不一致问题的解决方案
在数据库设计中,规范化(Normalization)和反规范化(Denormalization)是两个相互对立但又不可或缺的概念。规范化旨在消除数据冗余,确保数据的一致性和准确性,但可能会降低查询效率。相反…...
【Android】直接使用binder的transact来代替aidl接口
aidl提供了binder调用的封装,有的时候,比如: 1. 懒得使用aidl生成的接口文件(确实是懒,Android studio中aidl生成接口文件很方便) 2. 服务端的提供者只公开了部分接口出来,只给了调用编号和参…...
Python机器学习笔记(十八、交互特征与多项式特征)
添加原始数据的交互特征(interaction feature)和多项式特征(polynomial feature)可以丰富特征表示,特别是对于线性模型。这种特征工程可以用统计建模和许多实际的机器学习应用中。 上一次学习:线性模型对w…...
《跟我学Spring Boot开发》系列文章索引❤(2025.01.09更新)
章节文章名备注第1节Spring Boot(1)基于Eclipse搭建Spring Boot开发环境环境搭建第2节Spring Boot(2)解决Maven下载依赖缓慢的问题给火车头提提速第3节Spring Boot(3)教你手工搭建Spring Boot项目纯手工玩法…...
【AI进化论】 如何让AI帮我们写一个项目系列:将Mysql生成md文档
一、python脚本 下面给出一个简易 Python 脚本示例,演示如何自动获取所有表的结构,并生成一份 Markdown 文件。你可根据自己的需求做修改或使用其他编程语言。 import mysql.connector# ------------------------ # 1. 连接数据库 # -----------------…...
(已开源-AAAI25) RCTrans:雷达相机融合3D目标检测模型
在雷达相机融合三维目标检测中,雷达点云稀疏、噪声较大,在相机雷达融合过程中提出了很多挑战。为了解决这个问题,我们引入了一种新的基于query的检测方法 Radar-Camera Transformer (RCTrans)。具体来说: 首先设计了一个雷达稠密…...
Elasticsearch:在 HNSW 中提前终止以实现更快的近似 KNN 搜索
作者:来自 Elastic Tommaso Teofili 了解如何使用智能提前终止策略让 HNSW 加快 KNN 搜索速度。 在高维空间中高效地找到最近邻的挑战是向量搜索中最重要的挑战之一,特别是当数据集规模增长时。正如我们之前的博客文章中所讨论的,当数据集规模…...
unittest VS pytest
以下是 unittest 和 pytest 框架的对比表格: 特性unittestpytest设计理念基于类的设计,类似于 Java 的 JUnit更简洁,基于函数式编程设计,支持类和函数两种方式测试编写需要继承 unittest.TestCase 类,方法以 test_ 开…...
Tableau数据可视化与仪表盘搭建-基础图表制作
目录 对比分析:比大小 柱状图 条形图 数据钻取 筛选器 热力图 气泡图 变化分析:看趋势 折线图 预测 面积图 关系分布:看位置 散点图 直方图 地图 构成分析:看占比 饼图 树地图 堆积图 对比分析:比大…...
Center Loss 和 ArcFace Loss 笔记
一、Center Loss 1. 定义 Center Loss 旨在最小化类内特征的离散程度,通过约束样本特征与其类别中心之间的距离,提高类内特征的聚合性。 2. 公式 对于样本 xi 和其类别yi,Center Loss 的公式为: xi: 当前样本的特征向量&…...
3125: 【入门】求1/1+1/2+2/3+3/5+5/8+8/13+13/21……的前n项的和
文章目录 题目描述输入输出样例输入样例输出 题目描述 求1/11/22/33/55/88/1313/2121/34……的前n项的和。 输入 第1行:一个整数n(1 < n < 30 )。 输出 一行:一个小数,即前n项之和(保留3位小数&…...
如何确保获取的淘宝详情页数据的准确性和时效性?
要确保获取的淘宝详情页数据的准确性和时效性,可从以下几个方面着手: 合法合规获取数据 遵守平台规则:在获取淘宝详情页数据之前,务必仔细阅读并严格遵守淘宝平台的使用协议和相关规定。明确哪些数据可以获取、以何种方式获取以及…...
云计算是如何帮助企业实现高可用性的
想象一下,你正在享受一个悠闲的周末,突然接到同事的电话:公司的核心系统宕机了!这个场景对很多IT从业者来说并不陌生。但在云计算时代,这样的噩梦正在逐渐远去。 一位前辈告诉我:"在技术世界里&#…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...
大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...
招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...
