[color=]- 2023年9月28日 更新:此方法支持最新版 v2.3.1
[color=]- 2024年1月12日 更新:
[color=]支持最新版
[color=]v2.5.3
1.前言
本人小白, 而且在此之前未曾使用过此软件, 在果核上偶然看见了此软件的更新, 详情请查看果核推荐页面, 然后评论区有提到是否有修改版, 然后就下载软件, 开始了逆向之路
1.1 软件下载&安装
从官网下载软件, 软件100多M, 猜想是Electron实现
使用解压软件打开安装包, 果不其然, 证实了猜想
对于这类(Electron)软件, 我个人基本都是直接解压app-64.7z这个压缩文件到对应的目录, 省去安装的步骤, 但缺点就是没法关联打开文件
解压后得到如下结构
此时先不急打开, 因为此类(Electron)软件, 直接打开exe, 通常会在C盘建立一个目录, 并存放一些缓存和配置
如果不喜欢的话, 可以将Mindbox.exe创建一个快捷方式, 并添加参数--user-data-dir=D:\PortablePrograms\Mindbox\data, 这样就能将C盘的缓存目录重定向到user-data-dir这个目录上
但一旦直接打开了exe, 就会在c盘创建目录
1.2.打开软件
软件的基础版已经完全够用, 诚意还是够的, 但作为一名论坛混子, 虽然能力不行, 但想法还是要有的
2.常规分析
2.1 解包
对于此类软件, 代码基本都在resources/app.asar里头, asar可以看成一个压缩包, 但和常规的压缩类型不同, 所以不能以压缩软件打开, 需要安装asar模块进行解压和打包
// 安装asar
npm install -g asar
// 解压app.asar, 解压到app就可以直接运行程序, 不必重新打包再测试
asar extract app.asar ./app
至此, 在resources/app下边就有解压出来的资源文件
将原有的app.asar备份删除, 或者修改为别的名字
2.2 分析
用WebStorm打开app目录
vscode也行, 但webstorm格式化效果比较好
回到软件界面上, 点击标题栏上的皇冠图标, 第一次打开是登录框, 注册账号并登录后, 会弹出订阅套餐界面
这就有两个方向, 一个是处理购买订阅后的结果, 第二个是使用下方的恢复订阅
emmm 此处选择第二种方式
点击按钮后会出现恢复购买失败的提示
在WebStorm中搜索 恢复购买失败, 可以发现只有两个地方出现了, 而且并不是出现在js逻辑代码里头, 他们对的英文属性名也是一致的resumePurchaseFailed
因此转而搜索resumePurchaseFailed, 可以看到多出了一个搜索结果
那这个结果所在的函数, 就是我们重点要关注的地方, 将函数单独复制出来, 并格式化看看大致逻辑
查找到的js文件可以使用ctrl+alt+l格式化
async function getProFromFocusNote(G = !1) {
// 定义U=恢复购买失败提示信息
let U = getI18nText("resumePurchaseFailed");
// 检查Pro许可
const W = await checkProTransfer();
// if判断可直接删除
if (W) {
// emmm..., 虽然不清楚是干什么的,但我们可以直接暴力跳过判断
const K = await ProMigrationModal(G);
// if判断可直接删除
if (K === 0) {
// 更新用户信息
const X = await refreshUserInfo();
// 设置用户信息
window.electron.ipcRenderer.invoke("setUserInfo", X),
U = getI18nText("resumePurchaseSuccess")
} K !== -1 && Message({ message: U })
}
else G && Message({ message: U });
return W
}
大致逻辑就是通过一些判断, 要么提示恢复购买失败, 要么更新用户信息, 并提示恢复购买成功, 所以这些判断都可以直接去掉
async function getProFromFocusNote(G = !1) {
// 更新用户信息
const X = await refreshUserInfo();
// 设置用户信息
window.electron.ipcRenderer.invoke("setUserInfo", X);
return 1
}
那么关键函数就是这个refreshUserInfo, 只要它能得到一个包含PRO许可的结果, 软件就会刷新用户信息
async function refreshUserInfo() {
// 获取用户token
const G = await requestApi({ type: "get", url: "/auth/account/refresh-token" });
if (G.success) {
// 获取用户购买的产品清单
const U = await getUserProductList({ headers: { [G.data.tokenHead]: G.data.token } });
return {
...G.data,
// 这个U就是关键, 要如何构造出U的报文结构
purchaseList: U
}
}
}
查看getUserProductList函数
async function getUserProductList({headers: G} = {}) {
let U = {
type: "get",
url: "/auth/user-product/list",
// 查询用户购买的产品所使用的请求参数
// appId应为此软件的ID, productType即产品版本, showExpired是否显示过期(订阅)产品
params: {appId: 5, productType: "PRO", showExpired: !1},
headers: {"content-type": "application/x-www-form-urlencoded"}
};
if (G) for (let K in G) U.headers[K] = G[K];
const W = await requestApi(U);
return W.success ? W.data : []
}
根据上述信息, 即可尝试修改返回结果, 使其包含productType: "PRO"
因此直接将此函数改为如下内容
async function getUserProductList({headers: G} = {}) {
return [{productType: "PRO"}]
}
注意, 需把 getProFromFocusNote相关判断去掉, 否则程序到不了refreshUserInfo这一步, 也就到不了getUserProductList
保存后重新启动程序, 并点击 恢复购买, 左上角查看账户信息, 已经更新为PRO, 但日期是失效的
到目前为止, 软件打开已经显示为PRO版本, 但重新打开可能会变为基础版, 需要重新点击恢复购买
2.3 修复
方法和上边类似, 仍然按照关键字搜索, 在IDE中搜索您的订阅时长有效期至
再搜索英文属性pro_duration, 即可找到关键位置
代码格式化后查看更好分析
关键代码
// 字段定义, 在u函数上方
const {t} = q(), r = re(), c = se(), i = E(() => c.userInfo), d = E(() => r.useSync), h = E(() => c.isPro), k = Xe();
async function u() {
// i.value是用户信息, h.value是c.isPro
// 在IDE中可以按下ctrl键+鼠标左键,进入属性或方法, 比如此处的i和h
if (i.value) if (h.value) {
// 循环用户信息中的purchaseList, 过滤出productType = "PRO"的数据
// 此处也找到了另一个关键属性: expireTime, 这个字段就表示过期时间
const $ = Math.max(...i.value.purchaseList.filter(S => S.productType === "PRO").map(S => S.expireTime)),
// 如果超过50年即为永久
x = ge($).diff(ge(), "year") > 50;
await ee({
// 产品类型
title: t("proType"),
// 过期时间
message: t("pro_duration") + (x ? t("permanent") : ge($).format("YYYY/M/D")) + "。",
// 取消按钮
cancelText: t("cancel"),
// 续费按钮
okText: t("renew")
}) && C()
} else C()
}
综上所述, 只需要在getUserProductList函数中增加expireTime即可, 值类型为时间戳
async function getUserProductList({headers: G} = {}) {
// 2099-08-04 01:51:24
return [{productType: "PRO", expireTime: 4089462684000}]
}
注意: 如果重新打开界面不是PRO版本, 点击恢复购买即可, 或者在js中主动调用一次getProFromFocusNote(1);, 这样每次启动都会去刷新用户的信息
2.4 打包
修改上述文件均在app目录下操作js, Electron默认会加载app目录下的文件, 所以打包也不是必要的
// 打包命令
asar pack ./ app.asar
// 移动到上一层
move app.asar ../
2.5大功告成
3.更简单的方法
在上述分析过程中, 我们找到了关键的productType和expireTime的处理逻辑, 而进入这个逻辑的判断条件有两个
const {t} = q(), r = re(), c = se(), i = E(() => c.userInfo), d = E(() => r.useSync), h = E(() => c.isPro), k = Xe();
async function u() {
// i.value是用户信息, h.value是c.isPro
// 在IDE中可以按下ctrl键+鼠标左键,进入属性或方法, 比如此处的i和h
if (i.value) if (h.value) {}
}
这里我们直接查找h的定义(按Ctrl+鼠标左键), 可以发现 h = c.isPro, c = se(), 我们再进入se方法, 可以发现如下定义
// se()
function useUserStore() {
return useStore$2(store)
}
const store = createPinia(), useStore$2 = defineStore({
id: "user",
state: () => ({userInfo: null, receiptList: []}),
actions: {
async getUserInfo() {
this.userInfo = await window.electron.ipcRenderer.invoke("getUserInfo")
}, async getReceiptList() {
this.receiptList = await window.electron.ipcRenderer.invoke("getAppData", {key: "receiptList", orNot: []})
}
},
// isPro就是根据用户的购买的清单来获取的
getters: {isPro: G => G.userInfo && G.userInfo.purchaseList ? G.userInfo.purchaseList.findIndex(U => U.productType === "PRO") !== -1 : !!(G.receiptList.length > 0 && checkReceiptValid(G.receiptList))}
});
到这里就能找打isPro的定义, 既然这样, 何不那样, 对吧 :)
{
...
// 直接返回1
getters: {isPro:G=>1}
}
所以更简的方式就是, 解压app.asar后, 直接找到函数useStore$2的定义, 修改函数G的返回结果, 这样哪怕不登陆, 也能用PRO的功能
当然这样日期会显示无效日期, 强迫症可以根据前边的分修改日期的显示
// 过期时间显示定义: 您的订阅时长有效期至:+ 永久有效/具体到期日期 + 。
message: t("pro_duration") + (x ? t("permanent") : ge($).format("YYYY/M/D")) + "。"
// 如修改为永久, 并加上相应提示
message: t("pro_duration") + t("permanent") + " by 52pj。"
4.总结
重新梳理一遍分析流程, 对整个流程有了更多的认识, 也找到了更简洁的方法
软件的这个版本还没有复杂的加密, 解密之类的处理, 可以用于练练手
开发不易, 希望大家尊重软件开发团队的成果, 不要传播破解版本, 以免引起不必要的纠纷
附件:
123盘: https://www.123pan.com/s/3RzA-GF9Fv.html
本项目只做个人学习研究之用,不得用于商业用途!
若资金允许,请点击链接购买正版,谢谢合作!