使用范围

理论可使用版本是23.07以下的23版本。

安装XMind

官网下载安装即可,或下载此链接:Download XMind 23.07

依赖

需要npm安装asar

npm install -g asar

需求的三个文件:

破解过程

1. 解压

进入到路径%USERPROFILE%/AppData/Local/Programs/Xmind/resources,使用asar解压:

asar extract .\app.asar ./dist/

2. 替换

使用VSCode打开dist文件夹,全局搜索:

String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,71,102,77,65,48,71,67,83,113,71,83,73,98,51,68,81,69,66,65,81,85,65,65,52,71,78,65,68,67,66,105,81,75,66,103,81,67,68,89,72,51,49,108,48,108,108,105,99,66,97,118,98,85,90,82,103,48,121,49,76,110,73,10,50,74,74,117,80,90,97,107,48,52,57,56,119,71,109,75,48,78,43,107,115,113,67,122,65,48,88,85,102,67,103,81,53,69,57,105,116,89,121,80,117,84,43,122,54,80,122,47,43,48,113,54,78,101,65,112,107,87,99,110,67,47,84,104,10,87,81,89,54,90,108,69,79,77,111,110,114,104,80,117,98,56,122,115,87,89,79,90,122,99,107,81,117,116,120,51,106,110,54,107,43,54,90,88,120,55,121,85,98,98,107,120,73,107,43,119,113,87,103,110,108,81,120,110,120,54,84,77,100,10,83,51,114,103,111,51,114,52,98,108,70,84,87,105,54,69,69,81,73,68,65,81,65,66,10,45,45,45,45,45,69,78,68,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)

将其替换为:

String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,73,66,73,106,65,78,66,103,107,113,104,107,105,71,57,119,48,66,65,81,69,70,65,65,79,67,65,81,56,65,77,73,73,66,67,103,75,67,65,81,69,65,113,73,71,102,103,104,120,115,47,115,99,104,106,105,100,43,109,72,108,75,10,65,81,104,87,72,109,49,122,49,117,80,88,47,67,87,114,50,84,66,72,80,99,103,51,80,68,109,70,56,118,86,105,121,104,117,120,112,107,86,101,52,47,88,52,84,122,57,104,78,57,66,71,65,43,104,55,116,111,72,85,119,54,114,75,10,122,50,77,107,53,77,53,112,101,71,53,73,100,52,68,86,76,65,68,117,86,100,112,99,98,106,111,48,89,112,99,48,109,79,100,68,68,84,74,116,108,99,50,84,56,113,49,48,114,100,71,89,68,48,69,114,112,101,82,57,83,117,57,105,10,97,74,120,68,87,77,79,76,108,78,122,112,109,87,88,112,103,75,81,87,106,82,117,122,111,73,114,79,105,105,72,118,71,122,65,105,83,114,67,77,75,116,54,109,43,47,109,43,83,118,114,53,67,81,72,119,43,47,74,120,49,105,65,119,10,121,77,90,73,77,119,117,120,56,103,115,103,97,119,86,116,85,49,117,54,77,109,73,66,57,112,120,52,74,110,99,70,101,112,115,103,51,70,100,83,69,98,113,100,89,90,76,51,77,101,69,120,68,84,55,80,80,104,50,71,81,99,98,83,10,102,99,108,49,103,89,84,114,67,103,74,70,85,90,85,114,50,74,66,79,83,86,73,111,73,118,71,65,84,72,55,86,73,77,89,66,87,97,110,116,98,65,105,81,103,71,113,107,74,115,116,88,98,56,85,110,103,69,77,52,104,114,115,88,10,117,81,73,68,65,81,65,66,10,45,45,45,45,45,69,78,68,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)

3. 本地Hook服务

上面提供的三个附件,hook.js放入到main目录中,然后在main目录中创建hook目录,将crypto.jselectron.js放到dist/main/hook目录下,并且在dist/main/main.js的开头添加:

require('./hook')

4. 重新打包

可以先将原本的app.asar重命名为app.asar.bak做备份,防止翻车

执行命令:

asar pack ./dist/ app.asar

使用登录

使用任意账号密码登录即可,由于存在本地Hook服务,都会认证通过。

Hook服务使用了本地的3000端口,如果该端口被占用,会导致认证出错。

原理探究

实际上是通过劫持的相关的API,使得请求直接走向了本地的Hook服务,然后返回指定内容,进而使得软件认为授权成功。

明白原理后开始着手写一下破解脚本简化破解过程,并且对这个破解方式做一下优化。

随机端口优化

不妨定义以下函数:

function getAvailablePort() {
  const net = require('net')
  return new Promise(res => {
    const srv = net.createServer()
    srv.listen(0, () => {
      const port = srv.address().port
      srv.close(() => res(port))
    })
  })
}

listen(0, () => {})就可以获取到一个随机的且未被占用的端口号进行监听,随后只需要获取其端口号就可以取到一个随机的、可用的端口号。

注册器

接下来编写注册器,参考上面的hook.js,其实可以将所有的东西都集成到一个文件里,这更有利于破解的便捷性,注册器代码不在此贴出,本质思路就是将所有文件的内容合并,然后通过修改随机端口监听使得在本地3000端口被占用后也可以正常启动软件。可以通过此链接跳转到注册器代码位置:XMind破解#注册器代码

破解脚本

首先给出常量定义:

const path = `${process.env.HOME||process.env.USERPROFILE}/AppData/Local/Programs/Xmind/resources`
const extratFolder = 'dist'

参照上面的过程,首先进行的是解压app.asar

function extractCode() {    
    if (!fs.existsSync(path)) {
        console.error('[!] Error: Could not found XMind Installation.')
        exit(-1)
    }
    console.log('[*] Info : Start extracting...')
    asar.extractAll(`${path}/app.asar`, `${path}/${extratFolder}/`)
    if (!(fs.existsSync(`${path}/${extratFolder}/main`) && fs.existsSync(`${path}/${extratFolder}/renderer`) && fs.existsSync(`${path}/${extratFolder}/static`) && fs.existsSync(`${path}/${extratFolder}/package.json`))){
        console.error('[!] Error: Extract error')
        exit(-1)
    }
    console.log('[*] Info : Extract finish')
}

这里简单判断了解压后的文件结构来判断是否解压成功。

随后是替换证书公钥:

function replaceCertificatePublicKey() {
    const searchString = Buffer.from('String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,71,102,77,65,48,71,67,83,113,71,83,73,98,51,68,81,69,66,65,81,85,65,65,52,71,78,65,68,67,66,105,81,75,66,103,81,67,68,89,72,51,49,108,48,108,108,105,99,66,97,118,98,85,90,82,103,48,121,49,76,110,73,10,50,74,74,117,80,90,97,107,48,52,57,56,119,71,109,75,48,78,43,107,115,113,67,122,65,48,88,85,102,67,103,81,53,69,57,105,116,89,121,80,117,84,43,122,54,80,122,47,43,48,113,54,78,101,65,112,107,87,99,110,67,47,84,104,10,87,81,89,54,90,108,69,79,77,111,110,114,104,80,117,98,56,122,115,87,89,79,90,122,99,107,81,117,116,120,51,106,110,54,107,43,54,90,88,120,55,121,85,98,98,107,120,73,107,43,119,113,87,103,110,108,81,120,110,120,54,84,77,100,10,83,51,114,103,111,51,114,52,98,108,70,84,87,105,54,69,69,81,73,68,65,81,65,66,10,45,45,45,45,45,69,78,68,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)')
    const replaceString = Buffer.from('String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,73,66,73,106,65,78,66,103,107,113,104,107,105,71,57,119,48,66,65,81,69,70,65,65,79,67,65,81,56,65,77,73,73,66,67,103,75,67,65,81,69,65,113,73,71,102,103,104,120,115,47,115,99,104,106,105,100,43,109,72,108,75,10,65,81,104,87,72,109,49,122,49,117,80,88,47,67,87,114,50,84,66,72,80,99,103,51,80,68,109,70,56,118,86,105,121,104,117,120,112,107,86,101,52,47,88,52,84,122,57,104,78,57,66,71,65,43,104,55,116,111,72,85,119,54,114,75,10,122,50,77,107,53,77,53,112,101,71,53,73,100,52,68,86,76,65,68,117,86,100,112,99,98,106,111,48,89,112,99,48,109,79,100,68,68,84,74,116,108,99,50,84,56,113,49,48,114,100,71,89,68,48,69,114,112,101,82,57,83,117,57,105,10,97,74,120,68,87,77,79,76,108,78,122,112,109,87,88,112,103,75,81,87,106,82,117,122,111,73,114,79,105,105,72,118,71,122,65,105,83,114,67,77,75,116,54,109,43,47,109,43,83,118,114,53,67,81,72,119,43,47,74,120,49,105,65,119,10,121,77,90,73,77,119,117,120,56,103,115,103,97,119,86,116,85,49,117,54,77,109,73,66,57,112,120,52,74,110,99,70,101,112,115,103,51,70,100,83,69,98,113,100,89,90,76,51,77,101,69,120,68,84,55,80,80,104,50,71,81,99,98,83,10,102,99,108,49,103,89,84,114,67,103,74,70,85,90,85,114,50,74,66,79,83,86,73,111,73,118,71,65,84,72,55,86,73,77,89,66,87,97,110,116,98,65,105,81,103,71,113,107,74,115,116,88,98,56,85,110,103,69,77,52,104,114,115,88,10,117,81,73,68,65,81,65,66,10,45,45,45,45,45,69,78,68,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)')
    function walkDir(dir) {
        let list = []
        fs.readdirSync(dir).forEach((item) => {
            list.push(item)
        })
        return list
    }
    const baseDir = `${path}/${extratFolder}/renderer`
    let jsFile = walkDir(baseDir).filter((name) => name.endsWith('.js'))
    console.log('[*] Info : Replace software authentic certificate...')
    let isHited = false
    for (let index = 0; index < jsFile.length; index++) {
        let content = fs.readFileSync(`${baseDir}/${jsFile[index]}`)
        let foundIndex = content.indexOf(searchString)
        if (foundIndex != -1) {
            console.log(`[*] Info : Target hit in file ${jsFile[index]}`)
            const finalContent = content.subarray(0, foundIndex) + replaceString + content.subarray(foundIndex + searchString.length)
            fs.writeFileSync(`${baseDir}/${jsFile[index]}`, finalContent)
            console.log('[*] Info : Replace certificate finish')
            isHited = true
            break
        }
    }
    if (!isHited) {
        console.log('[!] Error: Target not found, exited.')
        exit(-1)
    }
}

接下来是写入注册器,并且在main.js最上面添加代码,使注册器在最前面加载,等同上面的复制hook.js相关步骤:

function writeActivator() {
    console.log('[*] Info : Applying Activator...')
    if (fs.existsSync(`${__dirname}/activator.js`)) {
 
    } else {
        console.log('[!] Error: Activator lost')
        exit(-1)
    }
    fs.copyFileSync(`${__dirname}/activator.js`, `${path}/${extratFolder}/main/activator.js`)
    console.log('[*] Info : Apply activator finish')
    console.log('[*] Info : Register activator...')
    fs.writeFileSync(`${path}/${extratFolder}/main/main.js` ,Buffer.from(`require('./activator')\n`) + fs.readFileSync(`${path}/${extratFolder}/main/main.js`))
    console.log('[*] Info : Register finish')
}

最后就是重新打包为asar

function cleanup() {
    console.log('[*] Info : Deleting code dir...')
    fs.rmSync(`${path}/${extratFolder}`, {force: true, recursive: true})
    if (fs.existsSync(`${path}/${extratFolder}`)) {
        console.log(`[^] Warn : Cleanup failed, some file still in "${path}/${extratFolder}", delete it manually`)
    } else {
        console.log('[*] Info : Cleanup finish')
    }
}
 
function repack() {
    console.log('[*] Info : Back up official software')
    fs.renameSync(`${path}/app.asar`, `${path}/app.asar.bak`)
    console.log(`[*] Info : Your software backup is "${path}/app.asar.bak", if something wrong, rename it to app.asar`)
    console.log('[*] Info : Repacking software...')
    asar.createPackage(`${path}/${extratFolder}/`, `${path}/app.asar`).finally(() => {
        // cleanup
        cleanup()
        if (fs.existsSync(`${path}/app.asar`)) {
            console.log('[*] Info : Repack finish, now you could open XMind and input any account with any password to upgrade it with pro edition')
        } else {
            console.log('[!] Error: Repack failed, rollback...')
            fs.renameSync(`${path}/app.asar.bak`, `${path}/app.asar`)
            console.log('[*] Info : Rollback finish. Activation progress has failed, try it again.')
        }
    })
}

这样子,就完成了破解。

完整脚本可以通过这个链接跳转:XMind破解#破解脚本代码

附录

注册器代码

保存为activator.js

const http = require("http");
const url = require("url");
 
function getAvailablePort() {
  const net = require('net')
  return new Promise(res => {
    const srv = net.createServer()
    srv.listen(0, () => {
      const port = srv.address().port
      srv.close(() => res(port))
    })
  })
}
 
const hostname = "127.0.0.1";
let server_port = 0
getAvailablePort().then(port => {
  // persist port
  server_port = port
  const server = http.createServer((req, res) => {
    const parsedUrl = url.parse(req.url, true);
    const path = parsedUrl.pathname;
    const method = req.method;
 
    res.setHeader("Content-Type", "application/json; charset=utf-8");
 
    if (path === "/_res/session" && method === "GET") {
      res.statusCode = 200;
      res.end(
        JSON.stringify({
          uid: "_xmind_1234567890",
          group_name: "",
          phone: "18888888888",
          group_logo: "",
          user: "_xmind_1234567890",
          cloud_site: "cn",
          expireDate: 4093057076000,
          emailhash: "1234567890",
          userid: 1234567890,
          if_cxm: 0,
          _code: 200,
          token: "1234567890",
          limit: 0,
          primary_email: "crack@xmind",
          fullname: "crack@xmind",
          type: null,
        })
      );
    } else if (path === "/_api/check_vana_trial" && method === "POST") {
      res.statusCode = 200;
      res.end(JSON.stringify({ code: 200, _code: 200 }));
    } else if (path === "/_res/get-vana-price" && method === "GET") {
      res.statusCode = 200;
      res.end(
        JSON.stringify({
          products: [
            { month: 6, price: { cny: 0, usd: 0 }, type: "bundle" },
            { month: 12, price: { cny: 0, usd: 0 }, type: "bundle" },
          ],
          code: 200,
          _code: 200,
        })
      );
    } else if (path === "/_api/events" && method === "GET") {
      res.statusCode = 200;
      res.end(JSON.stringify({ code: 200, _code: 200 }));
    } else if (path === "/_res/user_sub_status" && method === "GET") {
      res.statusCode = 200;
      res.end(JSON.stringify({ _code: 200 }));
    } else if (path === "/piwik.php" && method === "POST") {
      res.statusCode = 200;
      res.end(JSON.stringify({ code: 200, _code: 200 }));
    } else if (path.startsWith("/_res/token/") && method === "POST") {
      res.statusCode = 200;
      res.end(
        JSON.stringify({
          uid: "_xmind_1234567890",
          group_name: "",
          phone: "18888888888",
          group_logo: "",
          user: "_xmind_1234567890",
          cloud_site: "cn",
          expireDate: 4093057076000,
          emailhash: "1234567890",
          userid: 1234567890,
          if_cxm: 0,
          _code: 200,
          token: "1234567890",
          limit: 0,
          primary_email: "xmind@cracked",
          fullname: "xmind@cracked",
          type: null,
        })
      );
    } else if (path === "/_res/devices" && method === "POST") {
      res.statusCode = 200;
      res.end(
        JSON.stringify({
          raw_data:
            "TiT1ul64lE+EMrH0ogOPWHZw5r3sE+jU1l2smjmRuvxmqN3v0NPklgJI9ZpGBt3MZ/mRXM+KmmlZy/bXopy74SH7VLeg3Y1aCATUoWsY2O0XXy1I0JtvLsIF+uM6G2oOx8F6f5Wz+Embhg6b9SIF19MBckmXOOfahd0zWJDaxzpAYthagLgakhbG8k7ynXrUmGIaVmxcktxg3hnRgxlwKvJfM56x5lxF+eLY/t4EFBKfk++omYQExwflUwTrwdeP4kbQvNTMGi9v5Nmyg8Nq7w47sfc1zfeg5opDhW47JTzu29EveGXXAxgV88pjQDZMWjL5c+v4PprDSzF+KJGSfA==",
          license: {
            status: "sub",
            expireTime: 4093057076000,
          },
          _code: 200,
        })
      );
    } else {
      res.statusCode = 404;
      res.end("Not Found");
    }
  });
  server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
  });
})
 
const crypto = require("crypto");
 
// save origin publicDecrypt
const originalPublicDecrypt = crypto.publicDecrypt;
 
/**
 * Offical decrypt
 * @param {string} message encrypted message
 * @returns
 */
const originalPublicDecryptEx = function (message) {
  let key = `-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCDYH31l0llicBavbUZRg0y1LnI\n2JJuPZak0498wGmK0N+ksqCzA0XUfCgQ5E9itYyPuT+z6Pz/+0q6NeApkWcnC/Th\nWQY6ZlEOMonrhPub8zsWYOZzckQutx3jn6k+6ZXx7yUbbkxIk+wqWgnlQxnx6TMd\nS3rgo3r4blFTWi6EEQIDAQAB\n-----END PUBLIC KEY-----`;
  const n = originalPublicDecrypt(
    {
      key: key,
      padding: 1,
    },
    message
  );
  return n;
};
 
// define it as getter, return new implemention
Object.defineProperty(crypto, "publicDecrypt", {
  get() {
    return function myPublicDecrypt(...args) {
      console.log("Activator decrypting...")
      args[0]["key"] =
        "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqIGfghxs/schjid+mHlK\nAQhWHm1z1uPX/CWr2TBHPcg3PDmF8vViyhuxpkVe4/X4Tz9hN9BGA+h7toHUw6rK\nz2Mk5M5peG5Id4DVLADuVdpcbjo0Ypc0mOdDDTJtlc2T8q10rdGYD0ErpeR9Su9i\naJxDWMOLlNzpmWXpgKQWjRuzoIrOiiHvGzAiSrCMKt6m+/m+Svr5CQHw+/Jx1iAw\nyMZIMwux8gsgawVtU1u6MmIB9px4JncFepsg3FdSEbqdYZL3MeExDT7PPh2GQcbS\nfcl1gYTrCgJFUZUr2JBOSVIoIvGATH7VIMYBWantbAiQgGqkJstXb8UngEM4hrsX\nuQIDAQAB\n-----END PUBLIC KEY-----";
      let result;
      try {
        result = originalPublicDecrypt.call(this, ...args);
        let data = JSON.parse(result.toString());
        result = Buffer.from(JSON.stringify(data));
        crypto.log("Decrypt by activator success")
      } catch (e) {
        crypto.log("Decrypt by activator failed, fallback to officail decrypt")
        result = null;
        let ori = originalPublicDecryptEx(args[1]);
        crypto.log(
          "Decrypt Error, ",
          args[1].toString("base64"),
          "\nOfficial decrypt result: ",
          ori,
          "\nDetail:\n",
          e
        );
        result = ori;
      }
      return result;
    };
  },
});
 
Object.defineProperty(crypto, "log", {
  get() {
    return function log(...args) {
      console.log(...args);
    };
  },
});
const electron = require("electron");
 
const originalNet = electron.net;
 
const originalRequest = originalNet.request;
 
Object.defineProperty(originalNet, "request", {
  get() {
    return function (options, callback) {
      let url = options["url"];
      if (url.indexOf("/_api/share/vana_map/?lang=zh") > -1) {
      } else url = url.replace("https://www.xmind.cn", `http://${hostname}:${server_port}`);
      options["url"] = url;
  
      const req = originalRequest(options, callback);
 
      // response event listener
      req.on("response", (response) => {
        let data = "";
        response.on(
          "data",
          function (chunk) {
            data += chunk;
            chunk = "FUCKING data";
            this.emit("continue", chunk);
          }.bind(response)
        );
        response.on(
          "end",
          function () {
            // persist data to cache
            // cache[options.url] = data;
            console.log(
              "===== Intercepting net.request with options:",
              options,
              "Response ----- ",
              data
            );
            this.emit("continue");
          }.bind(response)
        );
      });
      return req;
    };
  },
});
 

破解脚本代码

const asar = require('asar');
const fs = require('fs');
const { exit } = require('process');
 
const path = `${process.env.HOME||process.env.USERPROFILE}/AppData/Local/Programs/Xmind/resources`
const extratFolder = 'dist'
 
function extractCode() {    
    if (!fs.existsSync(path)) {
        console.error('[!] Error: Could not found XMind Installation.')
        exit(-1)
    }
    console.log('[*] Info : Start extracting...')
    asar.extractAll(`${path}/app.asar`, `${path}/${extratFolder}/`)
    if (!(fs.existsSync(`${path}/${extratFolder}/main`) && fs.existsSync(`${path}/${extratFolder}/renderer`) && fs.existsSync(`${path}/${extratFolder}/static`) && fs.existsSync(`${path}/${extratFolder}/package.json`))){
        console.error('[!] Error: Extract error')
        exit(-1)
    }
    console.log('[*] Info : Extract finish')
}
 
function replaceCertificatePublicKey() {
    const searchString = Buffer.from('String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,71,102,77,65,48,71,67,83,113,71,83,73,98,51,68,81,69,66,65,81,85,65,65,52,71,78,65,68,67,66,105,81,75,66,103,81,67,68,89,72,51,49,108,48,108,108,105,99,66,97,118,98,85,90,82,103,48,121,49,76,110,73,10,50,74,74,117,80,90,97,107,48,52,57,56,119,71,109,75,48,78,43,107,115,113,67,122,65,48,88,85,102,67,103,81,53,69,57,105,116,89,121,80,117,84,43,122,54,80,122,47,43,48,113,54,78,101,65,112,107,87,99,110,67,47,84,104,10,87,81,89,54,90,108,69,79,77,111,110,114,104,80,117,98,56,122,115,87,89,79,90,122,99,107,81,117,116,120,51,106,110,54,107,43,54,90,88,120,55,121,85,98,98,107,120,73,107,43,119,113,87,103,110,108,81,120,110,120,54,84,77,100,10,83,51,114,103,111,51,114,52,98,108,70,84,87,105,54,69,69,81,73,68,65,81,65,66,10,45,45,45,45,45,69,78,68,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)')
    const replaceString = Buffer.from('String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,73,66,73,106,65,78,66,103,107,113,104,107,105,71,57,119,48,66,65,81,69,70,65,65,79,67,65,81,56,65,77,73,73,66,67,103,75,67,65,81,69,65,113,73,71,102,103,104,120,115,47,115,99,104,106,105,100,43,109,72,108,75,10,65,81,104,87,72,109,49,122,49,117,80,88,47,67,87,114,50,84,66,72,80,99,103,51,80,68,109,70,56,118,86,105,121,104,117,120,112,107,86,101,52,47,88,52,84,122,57,104,78,57,66,71,65,43,104,55,116,111,72,85,119,54,114,75,10,122,50,77,107,53,77,53,112,101,71,53,73,100,52,68,86,76,65,68,117,86,100,112,99,98,106,111,48,89,112,99,48,109,79,100,68,68,84,74,116,108,99,50,84,56,113,49,48,114,100,71,89,68,48,69,114,112,101,82,57,83,117,57,105,10,97,74,120,68,87,77,79,76,108,78,122,112,109,87,88,112,103,75,81,87,106,82,117,122,111,73,114,79,105,105,72,118,71,122,65,105,83,114,67,77,75,116,54,109,43,47,109,43,83,118,114,53,67,81,72,119,43,47,74,120,49,105,65,119,10,121,77,90,73,77,119,117,120,56,103,115,103,97,119,86,116,85,49,117,54,77,109,73,66,57,112,120,52,74,110,99,70,101,112,115,103,51,70,100,83,69,98,113,100,89,90,76,51,77,101,69,120,68,84,55,80,80,104,50,71,81,99,98,83,10,102,99,108,49,103,89,84,114,67,103,74,70,85,90,85,114,50,74,66,79,83,86,73,111,73,118,71,65,84,72,55,86,73,77,89,66,87,97,110,116,98,65,105,81,103,71,113,107,74,115,116,88,98,56,85,110,103,69,77,52,104,114,115,88,10,117,81,73,68,65,81,65,66,10,45,45,45,45,45,69,78,68,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)')
    function walkDir(dir) {
        let list = []
        fs.readdirSync(dir).forEach((item) => {
            list.push(item)
        })
        return list
    }
    const baseDir = `${path}/${extratFolder}/renderer`
    let jsFile = walkDir(baseDir).filter((name) => name.endsWith('.js'))
    console.log('[*] Info : Replace software authentic certificate...')
    let isHited = false
    for (let index = 0; index < jsFile.length; index++) {
        let content = fs.readFileSync(`${baseDir}/${jsFile[index]}`)
        let foundIndex = content.indexOf(searchString)
        if (foundIndex != -1) {
            console.log(`[*] Info : Target hit in file ${jsFile[index]}`)
            const finalContent = content.subarray(0, foundIndex) + replaceString + content.subarray(foundIndex + searchString.length)
            fs.writeFileSync(`${baseDir}/${jsFile[index]}`, finalContent)
            console.log('[*] Info : Replace certificate finish')
            isHited = true
            break
        }
    }
    if (!isHited) {
        console.log('[!] Error: Target not found, exited.')
        exit(-1)
    }
}
 
function writeActivator() {
    console.log('[*] Info : Applying Activator...')
    if (fs.existsSync(`${__dirname}/activator.js`)) {
 
    } else {
        console.log('[!] Error: Activator lost')
        exit(-1)
    }
    fs.copyFileSync(`${__dirname}/activator.js`, `${path}/${extratFolder}/main/activator.js`)
    console.log('[*] Info : Apply activator finish')
    console.log('[*] Info : Register activator...')
    fs.writeFileSync(`${path}/${extratFolder}/main/main.js` ,Buffer.from(`require('./activator')\n`) + fs.readFileSync(`${path}/${extratFolder}/main/main.js`))
    console.log('[*] Info : Register finish')
}
 
function cleanup() {
    console.log('[*] Info : Deleting code dir...')
    fs.rmSync(`${path}/${extratFolder}`, {force: true, recursive: true})
    if (fs.existsSync(`${path}/${extratFolder}`)) {
        console.log(`[^] Warn : Cleanup failed, some file still in "${path}/${extratFolder}", delete it manually`)
    } else {
        console.log('[*] Info : Cleanup finish')
    }
}
 
function repack() {
    console.log('[*] Info : Back up official software')
    fs.renameSync(`${path}/app.asar`, `${path}/app.asar.bak`)
    console.log(`[*] Info : Your software backup is "${path}/app.asar.bak", if something wrong, rename it to app.asar`)
    console.log('[*] Info : Repacking software...')
    asar.createPackage(`${path}/${extratFolder}/`, `${path}/app.asar`).finally(() => {
        // cleanup
        cleanup()
        if (fs.existsSync(`${path}/app.asar`)) {
            console.log('[*] Info : Repack finish, now you could open XMind and input any account with any password to upgrade it with pro edition')
        } else {
            console.log('[!] Error: Repack failed, rollback...')
            fs.renameSync(`${path}/app.asar.bak`, `${path}/app.asar`)
            console.log('[*] Info : Rollback finish. Activation progress has failed, try it again.')
        }
    })
}
 
// extract code first
extractCode()
// replace certificate
replaceCertificatePublicKey()
// write activator
writeActivator()
// repack 
repack()

使用方法

  1. 将注册器与破解脚本都写入到同一目录下
  2. 安装asar,命令为:npm install asar
  3. 运行破解脚本,命令为:node ./crack.js