MindBox(Neatify笔记)简单破解

查看 151|回复 10
作者:Braycep   
MindBox(Neatify笔记 v2.0.0)简单破解
[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
    本项目只做个人学习研究之用,不得用于商业用途!
    若资金允许,请点击链接购买正版,谢谢合作!

    软件, 用户信息

  • ╭你独1无2╮   

    增加一些分析过程吧,不然这个和发成品也没什么太大的区别了。
    AiniWang   

    非常感谢分享,写的很详细,对于小白很友好,经测试最新版V2.5.1也是有效的。就是最后打包的时候,测试有问题,命令行可能是  asar p app/ app.asar  。
    GaryZong   

    按照教程已成功,官方最新版2.0.1-353也支持
    补图
    Shadow1005   

    謝謝分享,學習一下
    aimzhangyuting   

    謝謝分享,學習一下
    Braycep
    OP
      

    感谢楼主分享 好人长命百岁
    无痕567   


    Hmily 发表于 2023-8-3 12:51
    增加一些分析过程吧,不然这个和发成品也没什么太大的区别了。

    好嘞, 回头整理一下
    雪莱鸟   

    大佬 你好 我按照你的方法处理完之后  启动之后 纯白屏   根据网上的解决办法 https://zhuanlan.zhihu.com/p/391380229  也不行
    Braycep
    OP
      


    无痕567 发表于 2023-8-3 17:36
    大佬 你好 我按照你的方法处理完之后  启动之后 纯白屏   根据网上的解决办法 https://zhuanlan.zhihu.com/ ...

    我也是纯白屏,求解
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部