Cocos2dx-js 逆向分析乘凉

admin 2023年10月31日10:10:18评论132 views字数 10422阅读34分44秒阅读模式

最近有两个 cocos2dx-js 的apk逆向需求, 而且正好是两种不同形式的 jsc 文件,所以总结总结分享一下。

瞎写写,可能比较乱,凑合看吧。。

什么是 cocos2dx-js

Cocos2dx 是一款游戏开发引擎,并且提供对C#Javascriptlua等多款编程语言的支持,其中 cocos2dx-js 就是使用 Javascript 来编写游戏的逻辑代码。

作为脚本语言,一般来说 js 都是直接文本可读的代码文件,但在分析 cocos2dx-js 应用的过程中,总是会遇到 jsc 文件,里面不再是明文代码,那么本文将介绍两种情况下的 jsc 分析方法。

js加密 -> jsc

按照惯例,cocos2dx 打包的 app 都会将代码和资源文件放在 assets 里面, 这里我在 assets/src 目录下面发现了我们的目标 jsc 文件:

Cocos2dx-js 逆向分析乘凉

010editor 打开看,都是杂乱无章的字节,应该是被加密过:

Cocos2dx-js 逆向分析乘凉

cocos2dx 引擎自带了一个默认的源文件加密,可以通过 xxtea 算法对 js 文件内容进行加密,然后生成为 jsc 文件。这个网上一搜 "jsc解密" 就能找到很多相关内容,比如:https://github.com/OEDx/cocos-jsc-endecryptor。

主要的问题是,如何分析出 xxtea 算法的密钥。

查找密钥

cocos2dx-js 打包生成的 app,会有一个由游戏引擎代码编译而成的 libcocos2djs.so

同时,大部分情况下,开发者会在这个 so 里自定义自己的密钥甚至自定义算法或数据结构,那么只要对这个so进行逆向分析,就可以得到解密的密钥。通过前人栽的树,我直接在函数表里搜索 xxtea,于是乎:

Cocos2dx-js 逆向分析乘凉

Cocos2dx-js 逆向分析乘凉

成功找到密钥。然后将密钥直接带入解密,如果解密出来是个 gzip,那么就再解压一次即可,最后即可得到明文的 js,也可以参考我抄的代码: https://github.com/hluwa/Cocos2dHunter/blob/master/decrypt.py。

结果:

Cocos2dx-js 逆向分析乘凉

Cocos2dx-js 逆向分析乘凉

js编译 -> jsc

cocos2dx-js 自带的加密确实是弱鸡,实在难以顶住那些偷代码的,所以又有了另外一种方法,就是将源码预编译为字节码,然后运行时直接解释执行。

cocos2dx-js 使用的js引擎叫做 spidermonkey,是火狐开源的一个js运行环境,那么编译js文件的工作,也是交由它来完成的。cocos2dx 修改版的 spidermonkey 源代码位于:https://github.com/cocos2d/Spidermonkey。

而关于 jsc 字节码反编译的文章,也有非虫师傅帮我们栽好树了:《jsc反编译工具编写探索之路》https://zhuanlan.zhihu.com/p/42403161。

只不过,非虫师傅当时看的是v33版本的代码,而我遇到的这个已经升级到v52版本了,具体细节有些不一样。 

不过思路基本是一致的: 

  1. 编译 SpiderMonkey

  2. 调用 SpiderMonkeydisassemble 的相关代码对 jsc 进行反汇编

关于为什么无法做反编译的问题,虫哥的文章中已经说的很清楚了,SpiderMonkey 中的 Decompile 原理只是将编译时放在jsc中的源码提取出来,而若编译时指定不存源码,那么就无法调用它来进行反编译。

编译 SpiderMonkey

此处是在 Mac 下面进行的操作

首先,拉取 SpiderMonkey 的源码, 并且指定 v52 版本的分支。

git clone https://github.com/cocos2d/Spidermonkey -b v52

安装一下编译环境的依赖:

brew install yasm mercurial gawk ccache python
brew install [email protected]
brew link --overwrite [email protected]

配置一下编译环境

cd SpiderMonkey/js/src
autoconf213
mkdir build
cd build
CXXFLAGS="-stdlib=libc++ -mmacosx-version-min=10.15" CC=clang CXX=clang++ ../configure --disable-optimize --enable-debug

如果 check 不过的话, 可以尝试安装 clang7 然后重试一遍

brew install llvm@7
brew link --force llvm@7

最后, 开始编译

make -j8

如果编译顺利,基本上就离成功只有一小步了。

分析源码

通过搜索源码,最后找到 disassembly 相关的函数存在于 shell/js.cpp 中, 编译完最终会生成一个叫 js 的可执行文件,这是一个相当于 Javascript Console 的封装。而 shell 里提供了 disfile 这样的函数,只不过里面加载文件的时候,采用的是对js文件的加载方式,我们还得改一改让他加载jsc文件,然后再进行反汇编操作。

这里我也直接给出我的 patch,基本上也是抄代码, 核心逻辑是先加载 jsc 文件,然后调用 DecodeScript 函数来解析脚本数据,调用 shell 里的 DisassembleScript 得到反汇编结果,最后输出文件。

From 26a9454fff054d52e92c899a769331578db8b0d7 Mon Sep 17 00:00:00 2001
From: hluwa <[email protected]>
Date: Sat, 27 Jun 2020 00:47:54 +0800
Subject: [PATCH] jsc disassemably

---
 js/public/RootingAPI.h            |   2 +-
 js/src/gc/RootMarking.cpp         |   2 +-
 js/src/jscntxt.cpp                |   8 +-
 js/src/shell/js.cpp               |   2 +-
 js/src/shell/jscdisasm.cpp        | 185 ++++++++++++++++++++++++++++++
 js/src/shell/moz.build            |   1 +
 mozglue/misc/TimeStamp_darwin.cpp |   2 +-
 7 files changed, 194 insertions(+), 8 deletions(-)
 create mode 100644 js/src/shell/jscdisasm.cpp

diff --git a/js/public/RootingAPI.h b/js/public/RootingAPI.h
index a99ac4ec8..6f2077b86 100644
--- a/js/public/RootingAPI.h
+++ b/js/public/RootingAPI.h
@@ -784,7 +784,7 @@ class MOZ_RAII Rooted : public js::RootedBase<T>
     }
 
     ~Rooted() {
-        MOZ_ASSERT(*stack == reinterpret_cast<Rooted<void*>*>(this));
+        //MOZ_ASSERT(*stack == reinterpret_cast<Rooted<void*>*>(this));
         *stack = prev;
     }
 
diff --git a/js/src/gc/RootMarking.cpp b/js/src/gc/RootMarking.cpp
index 93264084b..fa0419b9c 100644
--- a/js/src/gc/RootMarking.cpp
+++ b/js/src/gc/RootMarking.cpp
@@ -395,7 +395,7 @@ js::gc::GCRuntime::traceRuntimeCommon(JSTracer* trc, TraceOrMarkRuntime traceOrM
 class AssertNoRootsTracer : public JS::CallbackTracer
 {
     void onChild(const JS::GCCellPtr& thing) override {
-        MOZ_CRASH("There should not be any roots after finishRoots");
+        //MOZ_CRASH("There should not be any roots after finishRoots");
     }
 
   public:
diff --git a/js/src/jscntxt.cpp b/js/src/jscntxt.cpp
index be5d51aa7..c3cf60ccc 100644
--- a/js/src/jscntxt.cpp
+++ b/js/src/jscntxt.cpp
@@ -139,10 +139,10 @@ js::DestroyContext(JSContext* cx)
 
 void
 RootLists::checkNoGCRooters() {
-#ifdef DEBUG
-    for (auto const& stackRootPtr : stackRoots_)
-        MOZ_ASSERT(stackRootPtr == nullptr);
-#endif
+//#ifdef DEBUG
+//    for (auto const& stackRootPtr : stackRoots_)
+//        MOZ_ASSERT(stackRootPtr == nullptr);
+//#endif
 }
 
 bool
diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp
index e2d1199e8..210923bcf 100644
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -7626,7 +7626,7 @@ PreInit()
 }
 
 int
-main(int argc, char** argv, char** envp)
+main1(int argc, char** argv, char** envp)
 {
     PreInit();
 
diff --git a/js/src/shell/jscdisasm.cpp b/js/src/shell/jscdisasm.cpp
new file mode 100644
index 000000000..b5fcdd788
--- /dev/null
+++ b/js/src/shell/jscdisasm.cpp
@@ -0,0 +1,185 @@
+#include <iostream>
+#include <sstream>
+#include <fstream>
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+#include "jsapi.h"
+#include "js/Initialization.h"
+
+/* Use the fastest available getc. */
+#if defined(HAVE_GETC_UNLOCKED)
+# define fast_getc getc_unlocked
+#elif defined(HAVE__GETC_NOLOCK)
+# define fast_getc _getc_nolock
+#else
+# define fast_getc getc
+#endif
+
+static MOZ_MUST_USE bool DisassembleScript(JSContext* cx, HandleScript script, HandleFunction fun, bool lines, bool recursive, bool sourceNotes, Sprinter* sp);
+
+static bool
+GetBuildId(JS::BuildIdCharVector* buildId)
+{
+    const char buildid[] = "cocos_xdr";
+    bool ok = buildId->append(buildid, strlen(buildid));
+    return ok;
+}
+static const JSClassOps g_classOps = {
+    nullptr, nullptr, nullptr, nullptr,
+    nullptr, nullptr, nullptr,
+    nullptr,
+    nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook
+};
+static const JSClass g_class = {
+    "global",
+    JSCLASS_GLOBAL_FLAGS,
+    &g_classOps
+};
+
+bool ReadFile(JSContext* cx, const std::string &filePath, JS::TranscodeBuffer& buffer)
+{
+    FILE *fp = fopen(filePath.c_str(), "rb");
+    if (!fp) {
+        return false;
+    }
+    /* Get the complete length of the file, if possible. */
+    struct stat st;
+    int ok = fstat(fileno(fp), &st);
+    if (ok != 0)
+        return false;
+    if (st.st_size > 0) {
+        if (!buffer.reserve(st.st_size))
+            return false;
+    }
+    for (;;) {
+        int c = fast_getc(fp);
+        if (c == EOF)
+            break;
+        if (!buffer.append(c))
+            return false;
+    }
+
+    return true;
+}
+
+bool DecompileFile(const char *inputFilePath, const char* outputFilePath, JSContext *cx) {
+    JS::CompileOptions op(cx);
+    op.setUTF8(true);
+    op.setSourceIsLazy(true);
+    op.setFileAndLine(inputFilePath, 1);
+
+    std::cout << "Input file: " << inputFilePath << std::endl;
+
+    std::cout << "Loading ..." << std::endl;
+
+    JS::RootedScript script(cx);
+    JS::TranscodeBuffer loadBuffer;
+    if(!ReadFile(cx,inputFilePath,loadBuffer)){
+        std::cout << "Loading fails!" << std::endl;
+        return false;
+    }
+    JS::TranscodeResult decodeResult = JS::DecodeScript(cx, loadBuffer, &script);
+    if (decodeResult != JS::TranscodeResult::TranscodeResult_Ok)
+    {
+        std::cout << "Decoding fails!" << std::endl;
+        return false;
+    }
+    Sprinter sprinter(cx);
+    if (!sprinter.init())
+        return false;
+    bool ok = DisassembleScript(cx, script, nullptr, falsetruefalse, &sprinter);
+
+    if (ok)
+    {
+        const char* dis = sprinter.string();
+        FILE* fd = fopen(outputFilePath, "wb");
+        fwrite(dis, strlen(dis), 1, fd);
+        fclose(fd);
+        std::cout << "Disassemable to: " << outputFilePath << std::endl;
+    }
+        
+    if (!ok){
+        std::cout << "Disassemable failed." << std::endl;
+        return false;
+    }
+        
+
+    return true;
+
+}
+
+int main(int argc, char** argv, char** envp)
+{
+
+    if(argc < 2){
+        printf("Usage: js <jscfile> [outfile]n");
+        return false;
+    }
+
+
+    if (!JS_Init())
+    {
+        return false;
+    }
+
+    JSContext *cx = JS_NewContext(JS::DefaultHeapMaxBytes);
+    if (nullptr == cx)
+    {
+        return false;
+    }
+
+    JS_SetGCParameter(cx, JSGC_MAX_BYTES, 0xffffffff);
+    JS_SetGCParameter(cx, JSGC_MODE, JSGC_MODE_INCREMENTAL);
+    JS_SetNativeStackQuota(cx, 500000);
+    JS_SetFutexCanWait(cx);
+    JS_SetDefaultLocale(cx, "UTF-8");
+
+    if (!JS::InitSelfHostedCode(cx))
+    {
+        return false;
+    }
+
+    JS_BeginRequest(cx);
+
+    JS::CompartmentOptions options;
+    options.behaviors().setVersion(JSVERSION_LATEST);
+    options.creationOptions().setSharedMemoryAndAtomicsEnabled(true);
+
+    JS::ContextOptionsRef(cx)
+        .setIon(true)
+        .setBaseline(true)
+        .setAsmJS(true)
+        .setNativeRegExp(true);
+
+
+    JS::RootedObject global(cx, JS_NewGlobalObject(cx, &g_class, nullptr, JS::DontFireOnNewGlobalHook, options));
+
+    JSCompartment *oldCompartment = JS_EnterCompartment(cx, global);
+
+    if (!JS_InitStandardClasses(cx, global)) {
+        std::cout << "JS_InitStandardClasses failed! " << std::endl;
+    }
+
+    JS_FireOnNewGlobalObject(cx, global);
+
+    JS::SetBuildIdOp(cx, GetBuildId);
+
+
+    if(argc == 3){
+        DecompileFile(argv[1], argv[2],cx);
+    }
+    else{
+        DecompileFile(argv[1],"./disassemble.jasm",cx);
+    }
+    
+
+    if (cx) {
+        JS_LeaveCompartment(cx, oldCompartment);
+        JS_EndRequest(cx);
+        JS_DestroyContext(cx);
+        JS_ShutDown();
+        cx = nullptr;
+    }
+}
 No newline at end of file
diff --git a/js/src/shell/moz.build b/js/src/shell/moz.build
index 72ea8145c..c1f76d4e3 100644
--- a/js/src/shell/moz.build
+++ b/js/src/shell/moz.build
@@ -12,6 +12,7 @@ if CONFIG['JS_SHELL_NAME']:
 
 UNIFIED_SOURCES += [
     'js.cpp',
+    'jscdisasm.cpp',
     'jsoptparse.cpp',
     'jsshell.cpp',
     'OSObject.cpp'
diff --git a/mozglue/misc/TimeStamp_darwin.cpp b/mozglue/misc/TimeStamp_darwin.cpp
index f30bc9846..41e12540f 100644
--- a/mozglue/misc/TimeStamp_darwin.cpp
+++ b/mozglue/misc/TimeStamp_darwin.cpp
@@ -101,7 +101,7 @@ BaseTimeDurationPlatformUtils::ToSecondsSigDigits(int64_t aTicks)
 int64_t
 BaseTimeDurationPlatformUtils::TicksFromMilliseconds(double aMilliseconds)
 {
-  MOZ_ASSERT(gInitialized, "calling TimeDuration too early");
+  //MOZ_ASSERT(gInitialized, "calling TimeDuration too early");
   double result = (aMilliseconds * kNsPerMsd) / sNsPerTick;
   if (result > INT64_MAX) {
     return INT64_MAX;
-- 
2.24.3 (Apple Git-128)

收工

最后再编译一下,执行 js/src/shell/js xxx.jsc

Cocos2dx-js 逆向分析乘凉

Cocos2dx-js 逆向分析乘凉

关机下班,底薪到手。

参考资料

https://zhuanlan.zhihu.com/p/42403161 

https://github.com/irelance/js-binding-mozjs52-training 

https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Build_Documentation

https://blog.csdn.net/u013647453/article/details/82597751


Cocos2dx-js 逆向分析乘凉


原文始发于微信公众号(秃头的逆向痴想):Cocos2dx-js 逆向分析乘凉

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年10月31日10:10:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Cocos2dx-js 逆向分析乘凉http://cn-sec.com/archives/799782.html

发表评论

匿名网友 填写信息