Skip to content

最近需要逆向学X通一些参数的加密过程,但相应功能只在客户端上才有,所以分析网页 JS 行不通。APP用了梆梆企业版加固,在网上找了一圈没找到什么脱壳教程遂放弃。PC端浓浓的 Electron 味,有戏!

Electron 味哪里来?

学X通PC客户端是使用 electron + nodejs 框架,用 javascript 开发的跨平台的桌面程序。

目录结构

通常使用Electron构建的程序目录都类似以下这样

最显眼的就是 locales 和 resources,两个文件夹,其中 locales 里面是放的应用程序的本地化(或国际化)资源。resources 里面是程序的相应的主体代码。

页面风格

常见的软件比如:Visual Studio Code,Postman,新版网易云音乐,QQNT,你可以在 Electron 官网底部看到更多。

就连我现在使用的这个 Gridea 都是😂

分析程序结构

通常程序的主代码都在 resources 文件夹下,打包为 app.asar 。需要打包/解压工具 (npm i -g asar)

但是学X通并没有打包,也就省去了解包的步骤。

需要解密的文件是位于 cxstudyinhouse\1.1.8.6\resources\app\electron\renderer 目录下的 MainWindowHelper.jscx,打开是一大串无规则的字符串。

经过查找解密步骤的实现在 cxstudyinhouse\1.1.8.6\resources\app\module\compile 文件夹下。其中 lib 文件夹内 CompileUtil.js 作用是用于导出模块中的 getDbKey 函数,与解密 jscx 文件关系不大。

主要解密步骤在 Jscx.js 文件中

ChatGPT 如是说道:

这段代码的主要功能是为 Electron 应用程序添加对一个新的文件扩展名 .jscx 的支持。这个新的文件扩展名看起来像是一种预编译的 JavaScript 文件,可能包含一些特殊的优化或安全措施。

下面是这段代码的详细解析:

  1. 首先,它导入了一些 Node.js 的内置模块,比如 fs(文件系统)、v8(V8 JavaScript 引擎)、path(路径处理)和 Module(模块系统)。

  2. 然后,根据当前的平台(process.platform),它导入了不同的 CompileExt 模块。如果当前平台是 macOS("darwin"),则导入 ../mac/CompileExt;否则,导入 ../win/CompileExt。这个 CompileExt 模块可能包含了一些平台特定的编译或解析 .jscx 文件的代码。

  3. 接下来,它设置了 V8 JavaScript 引擎的一些标志,这里是 --no-lazy,这可能是为了优化 JavaScript 的编译。

  4. 最后,它为 Node.js 的模块系统添加了一个新的文件扩展名处理器。这个处理器定义了当 Node.js 遇到一个 .jscx 文件时应该如何处理。在这个处理器中,它创建了一个新的 require 函数,设置了一些属性,然后调用了 compileExt.RunFile 函数来处理 .jscx 文件。

总的来说,这段代码的主要目的是为 Electron 应用程序添加对 .jscx 文件的支持,这可能是为了实现一些特殊的优化或安全措施。


总的逻辑就是将 jscx 文件丢给 ../win/CompileExt 解析运行。

CompileExt.node 是node.js Addon:通过 C++ 编写的扩展模块,用于扩展 Node.js 的功能。 还需要知道有:N-API(Node-API)是一组稳定的 C 语言 API,用于编写跨平台的 Node.js Addon。N-API 提供了一套抽象层,使得开发者可以更轻松地编写可移植的扩展模块,而无需担心不同版本的 Node.js 或不同平台之间的兼容性问题。

通过对几个 jscx 文件的观察,内容很像是 Base64 编码过的,但是使用Base64解码又不成功,那么大概率是 AES 或 DES 加密。

既然 CompileExt.node 可以解析 jscx 文件,那么应该是将加密过的 jscx 文件在内部用 key 解密转回 js 文件后再执行。

IDA 启动!

CompileExt.node 既然是C++编译来的那就反编译查看他的解密逻辑。

通过搜索字符串 crypto 定位到相关代码段,F5 反编译为C代码

大胆假设这段就是解密方法!

可以看到相关的解密代码,是调用了 crypto.createDecipheriv 函数,加密方法也和前面的猜想一样是使用了 aes-128-ecb,第二个参数是key也就是解密所需的密钥,第三个参数iv是空,可以忽略掉。

重点转移到 key 的值是如何来的

查看Node.js的文档

大胆假设一下,既然需要将 key 传递到 js 代码中那就需要创建一个相应的 js 的 key 字符串,那么v20 很可能就是 key ,又 v20 = &a10; 而 a10 又是作为参数传递进来的

那么就要看是谁调用了这个函数,给这个函数改个名,点击IDA菜单 View->Open subviews->Function calls

可以看到这个函数被调用了三次,先点进第一个看看,可以看到在61行调用了解密函数,a10对应的参数也就是我另起名的key

在第59行可以看到将key的地址传递给了getKey函数(这是我后修改的名),继续跟进

Assignment 函数的逻辑是将第二个参数以第三个参数的长度赋值给第一个参数

举个例子:在第62行的逻辑就是从v4+2,即v33+ 2,开始往后v3即3个字符,赋值给Block。

以下的几行差不多都是类似的逻辑

但是他最后是将 *v23 什么的值赋值给 this ,但是我并不知道 *v23的值是怎么来的,他前面也没有过赋值😓

不过知道了key是在什么地方生成的,那么只需要相应地方打下断点,动态调试一下就知道具体的值了!

经过之后的测试发现,第一个调用是将前面那一大长串用 getKey 生成的 key 进行解密。

第二次和第三次调用都是在同一个函数内,区别为第二次才是真的将 jscx 文件解密 ,而第三次同样是将前面的一串字符解密运行。

解密 jscx 文件要在以下地址下断点

52AD37A9 | E8 E2F7FFFF | call compileext.52AD2F90 | 调用解密函数

xdbg 启动!

打开 xdbg,运行学X通客户端,随便点几下确保 CompileExt.node 相关代码已经运行,接着搜索字符串 HmoBjviHurIH+fe..... ,定位到对应的代码行,对比一下

call compileext.7B6A2F90 即为 call jiemi

在对应位置下断点,运行程序,当程序运行到断点处时查看堆栈区即有对应的 key 值。

拿去尝试解密,确定就是密钥没错了。

后记

最初我是想通过开启 Devtools 调试出key的,但是折腾一阵发现

js
mainWindow.webContents.openDevTools()

找不到他具体的mainWindow是什么,new BrowserWindow 这条代码也是打包在 exe 中的,其他js文件中都没有。debugtron 这个工具连学X通都识别不到。

另一个思路是在 js 层 hook api,修改他 crypto 库的文件,console.log(key),但是 crypto 库也打包进了 exe 里,也只能放弃。

花了两天,简单的学了下 IDA 和 xdbg 通过动态调试可算把key找到了。从头到尾花了四天,还不错😇


再更.....

发现有部分文件在断点处没有 key 的值,解决方法是将 main.jscx 解密后插入对应的代码,再用同一个key加密回去。

js

try { const robotTranslate = require('./test/robotTranslate'); } catch (e) { console.error(e); }

确保文件会被加载就可以看到对应key的值了。

Released under the MIT License.