最近有两个 cocos2dx-js
的apk逆向需求, 而且正好是两种不同形式的 jsc
文件,所以总结总结分享一下。
瞎写写,可能比较乱,凑合看吧。。
什么是 cocos2dx-js
Cocos2dx
是一款游戏开发引擎,并且提供对C#
、Javascript
、lua
等多款编程语言的支持,其中 cocos2dx-js
就是使用 Javascript
来编写游戏的逻辑代码。
作为脚本语言,一般来说 js
都是直接文本可读的代码文件,但在分析 cocos2dx-js
应用的过程中,总是会遇到 jsc
文件,里面不再是明文代码,那么本文将介绍两种情况下的 jsc
分析方法。
js加密 -> jsc
按照惯例,cocos2dx
打包的 app
都会将代码和资源文件放在 assets
里面, 这里我在 assets/src
目录下面发现了我们的目标 jsc
文件:
用 010editor
打开看,都是杂乱无章的字节,应该是被加密过:
cocos2dx
引擎自带了一个默认的源文件加密,可以通过 xxtea
算法对 js
文件内容进行加密,然后生成为 jsc
文件。这个网上一搜 "jsc解密" 就能找到很多相关内容,比如:https://github.com/OEDx/cocos-jsc-endecryptor。
主要的问题是,如何分析出 xxtea
算法的密钥。
查找密钥
cocos2dx-js
打包生成的 app
,会有一个由游戏引擎代码编译而成的 libcocos2djs.so
。
同时,大部分情况下,开发者会在这个 so
里自定义自己的密钥甚至自定义算法或数据结构,那么只要对这个so进行逆向分析,就可以得到解密的密钥。通过前人栽的树,我直接在函数表里搜索 xxtea
,于是乎:
成功找到密钥。然后将密钥直接带入解密,如果解密出来是个 gzip
,那么就再解压一次即可,最后即可得到明文的 js
,也可以参考我抄的代码: https://github.com/hluwa/Cocos2dHunter/blob/master/decrypt.py。
结果:
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版本了,具体细节有些不一样。
不过思路基本是一致的:
-
编译
SpiderMonkey
-
调用
SpiderMonkey
中disassemble
的相关代码对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, false, true, false, &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
关机下班,底薪到手。
参考资料
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 逆向分析乘凉
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论