最近需要逆向学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 文件,可能包含一些特殊的优化或安全措施。
下面是这段代码的详细解析:
首先,它导入了一些 Node.js 的内置模块,比如
fs
(文件系统)、v8
(V8 JavaScript 引擎)、path
(路径处理)和Module
(模块系统)。然后,根据当前的平台(
process.platform
),它导入了不同的CompileExt
模块。如果当前平台是 macOS("darwin"
),则导入../mac/CompileExt
;否则,导入../win/CompileExt
。这个CompileExt
模块可能包含了一些平台特定的编译或解析.jscx
文件的代码。接下来,它设置了 V8 JavaScript 引擎的一些标志,这里是
--no-lazy
,这可能是为了优化 JavaScript 的编译。最后,它为 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的,但是折腾一阵发现
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加密回去。
try { const robotTranslate = require('./test/robotTranslate'); } catch (e) { console.error(e); }
确保文件会被加载就可以看到对应key的值了。