MindBox(Neatify笔记)简单PJ

MindBox(Neatify笔记 v2.0.0)简单破解

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.更简单的方法

​        在上述分析过程中, 我们找到了关键的productTypeexpireTime的处理逻辑, 而进入这个逻辑的判断条件有两个

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.总结

直接修改文件 app/dist/electron/renderer/assets/index-yqoE38iR.js方法 即可破解

async function getUserProductList({headers: G} = {}) {
    return [{productType: "PRO"}]
} 
  • 重新梳理一遍分析流程, 对整个流程有了更多的认识, 也找到了更简洁的方法
  • 软件的这个版本还没有复杂的加密, 解密之类的处理, 可以用于练练手
  • 开发不易, 希望大家尊重软件开发团队的成果, 不要传播破解版本, 以免引起不必要的纠纷

本项目只做个人学习研究之用,不得用于商业用途!
若资金允许,请点击链接购买正版,谢谢合作

转载自   https://www.52pojie.cn/thread-1816569-1-1.html

版权声明:
作者:超级管理员
链接: https://apecloud.ltd/article/detail.html?id=183
来源:猿码云个人技术站
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
/static/admin/img/weixin.jpg/static/admin/img/zfb.jpg
<<上一篇>
typora简单PJ
下一篇>>