steven026 发表于 2026-1-4 03:52

Typora v1.12.4 安全分析:反反调试与激活劫持

本帖最后由 steven026 于 2026-1-4 16:24 编辑

# Typora v1.12.4 安全分析:反反调试与激活劫持

## 前言

本文旨在以 Typora v1.12.4 为例,探讨 Electron 应用的安全机制与逆向分析思路。
本文默认读者已熟悉 Node.js 及 Electron 的基础知识,故不再赘述相关概念。
本文JavaScript友好,主要从 Node.js/Electron 层面切入,无需具备任何二进制/C++ 层面知识。

> 提示:本篇文章及包含的代码含有经过 AI 润色/优化部分。

## 免责声明

本文内容仅用于**Electron应用安全研究与技术交流**,旨在帮助读者理解 Electron 应用在安全分析、被逆向调试、被攻击面识别与防护以及加固方面的常见思路与潜在的风险点。
本文所涉分析过程、截图、日志与结论均以**教育与防御**目的呈现,不构成对任何特定软件、服务或厂商的恶意攻击建议。
本文仅提供安全研究分析与思路,可能存在漏洞或不足之处,作者不提供任何保证,请勿尽信本文内容,任何读者/用户因使用、引用或传播本文内容造成的一切可能直接或间接后果均由读者/用户自行承担。
本文不会提供任何成品,相关技术的使用须严格遵守适用的法律法规与行业规范,严禁将本文内容用于任何未经授权的测试、商业化侵权、破坏他人系统或其他违法违规行为。
如无法遵守上述声明,请勿阅读或使用本文内容。

## 环境准备

从官网下载截至本文发布时的最新版本 Typora(v1.12.4)。

> 默认安装路径:`C:\Program Files\Typora`

### 初步调试尝试

首先尝试通过 `--debug` 或 `--inspect` 参数启动 `Typora.exe`。
会发现程序设置了反调试机制,当检测到命令行包含调试选项时,程序会自动拒绝启动并抛出错误。

### 定位入口文件

在 `resources` 目录下找到 `package.json`,其定义的入口文件为 `"main": "launch.dist.js"`,但该目录下并不存在此文件。
观察同目录下的 `app.asar` 文件,可以确定入口文件已被打包在 Electron 的 ASAR 归档中。
根据 Electron 默认的加载策略,`app` 文件夹的优先级高于 `app.asar` 文件。因此,我们可以通过解压 `app.asar` 并将其重命名为 `app` 目录来提高加载优先级。

### 安装 `asar` 工具

```bash
npm i -g asar

```

### 解压并备份资源

```bash
asar extract app.asar app
robocopy app app.bak /E
rename app.asar app.asar.bak

```

由于暂时无需对 `node_modules` 或其他依赖库进行操作,此处仅解压 `app.asar`。

> **提示**:Typora 对核心文件存在完整性校验机制。因此需要保留所有经过修改的原始文件的备份,以备后用。

### 分析入口代码

进入解压后的 `app` 目录,格式化并保存 `launch.dist.js`。
阅读代码后发现,该入口文件自定义了一个 V8 环境,并将 `.jsc` 字节码文件作为 Node 模块进行 `require` 加载。
除了初始化V8环境外,该文件不包含任何具体业务逻辑。

**初步结论**:

核心逻辑被编译在 `atom.compiled.dist.jsc` 字节码中,该 `.jsc` 文件本质上仍是一个 Node 模块。
**坏消息**:我们无法直接阅读或修改 `.jsc` 字节码。
**好消息**:`.jsc` 的运行完全依赖 Node.js/Electron 环境,无法脱离 JavaScript 代码去直接执行 C++ 逻辑。

虽然代码开头看上去很吓人,又是 VM 虚拟机、又是 V8 引擎,以为我们需要手撕二进制 / C++ 了,但是最后的`require`暴露了`.jsc`的本质还是一个Node模块。
这意味着我们可以通过 Hook(劫持)Node.js 或 Electron 的底层 API,间接分析、调试并修改其行为逻辑。

## 调试准备与环境劫持

### 修改 Electron Fuses 配置

基于上述简单入口分析后,我们准备开始进行调试。
首先直接运行`Typora.exe`,发现没有任何反应,回忆我们之前的操作,发现我们仅仅是将`app.asar`解压为了`app`
尝试还原 `app.asar`再次运行程序,发现恢复正常,这说明应用对`app`与`app.asar`加载优先级进行了限制。
查阅相关资料发现,Electron 应用可以通过 `@electron/fuses` 查询与配置应用配置。

Typora 的配置如下:

```text
Fuse Version: v1
RunAsNode is Disabled
EnableCookieEncryption is Disabled
EnableNodeOptionsEnvironmentVariable is Enabled
EnableNodeCliInspectArguments is Disabled
EnableEmbeddedAsarIntegrityValidation is Disabled
OnlyLoadAppFromAsar is Enabled
LoadBrowserProcessSpecificV8Snapshot is Disabled
GrantFileProtocolExtraPrivileges is Enabled

```

配置项 `OnlyLoadAppFromAsar is Enabled` 限制了程序只能从 `app.asar` 启动。
我们需要修改此配置,使 Electron 恢复默认的文件加载策略(优先加载 `app` 文件夹)。

(创建一个临时 `.cjs` 文件运行以下代码)

```js
const { flipFuses, FuseV1Options, FuseVersion } = require('@electron/fuses');
const fs = require('fs');

const fullPath = 'C:\\Program Files\\Typora\\Typora.exe';

// 修改前先备份
fs.copyFileSync(fullPath, `${fullPath}.bak`);
// 修改fuse配置(同时会修改程序hash)
flipFuses(fullPath, {
    version: FuseVersion.V1,
    : false,
});

```

再次将 `app.asar` 重命名备份,此时 `Typora.exe` 已能正常运行。
然而,一旦修改 `launch.dist.js`,程序虽然能启动,但几秒后会自动退出。这表明存在完整性校验。

### 绕过完整性校验

校验逻辑显然位于 `atom.compiled.dist.jsc` 中。
(完整性校验代码位于`Typora.exe`可能性非常低,不利于维护;且如果位于,程序一般会立即退出而不是过了几秒才退出)
完整性校验显然分为两个部分,校验&退出
通过Node.js去检测一个文件的完整性,无非就是原生`fs/http/fetch`等模块,不管是哪个模块我们都有能力去劫持与欺骗
Electron应用的主动退出,无非是`app.quit() / app.exit()`或者`process.exit() / process.kill()`等,我们可以尝试将这几个函数全部拦截,就能做到即使完整性校验劫持失败也能使应用不主动退出,从而让我们有更多机会去调试

#### 完整性校验分析结论 (TL;DR)

经排查,校验逻辑调用了 `fs/promises` 模块的 `readFile` 函数,分别读取以下 4 个文件并一一比对 Hash 值。一旦有任何不匹配,立即调用 `app.quit()` 退出程序。

```text
C:\Program Files\Typora\resources\app/package.json
C:\Program Files\Typora\resources\app/launch.dist.js
C:\Program Files\Typora\resources\app/../page-dist/license.html
C:\Program Files\Typora\resources\app/../page-dist/static/js/LicenseIndex.180dd4c7.5789633d.js

```

**绕过策略**:我们可以劫持 `fs.promises.readFile`,当检测到路径中含有 `resources\app/` 时,将其重定向到原始文件的备份目录 `resources/app.bak/`。

#### 注入调试与劫持代码

在 `launch.dist.js` 中,将以下代码插入到 `require` 基础模块之后、加载 `.jsc` 字节码之前,确保调试与劫持的代码优先于核心逻辑生效。

```js
// 输出调试日志
const LOG_PATH = 'D:\\Typora_Log.txt';
//fs.rmSync(LOG_PATH, { force: true });
function writeLog(...data) {
    const log = `[${new Date().toLocaleString()}] ${data.join(' ')}\n------------------\n`;
    fs.appendFileSync(LOG_PATH, log);
}

```

```js
// Node模块require后会进行缓存,即使再次require会指向同一个对象
const electron = require('electron');
Object.defineProperty(electron.app, 'quit', {
    value: function () {
      writeLog('[🛡️ 拦截] 程序试图调用 app.quit(),已阻止。');
    },
    writable: true,
    configurable: true,
});
electron.app.on('browser-window-created', (_event, win) => {
    writeLog('【👀 监控】检测到 BrowserWindow 实例化!');

    // 确保dom-ready后再打开DevTools 否则第一个窗口可能会无法打开
    win.webContents.once('dom-ready', () => {
      writeLog('【🔧】打开 DevTools...');
      win.webContents.openDevTools({ mode: 'detach' });
    });
});

```

> **提示**:劫持 `electron.app.quit` 会导致用户也无法正常关闭程序,需使用任务管理器强制结束。
> 当成功完成后续的文件校验劫持后,建议移除 `electron.app.quit` 劫持。

```js
// resources/app/ → resources/app.bak/
const fsPathFrom = /resources[\\/]app[\\/]/i;
const fsPathTo = 'resources\\app.bak\\';
const fsHook = {};
['readFileSync', 'readFile', 'statSync', 'stat', 'Stats', 'StatsFs', 'open', 'openSync'].forEach((property) => {
    fsHook = fs;
    fs = function (filePath, ...args) {
      if (typeof filePath == 'string' && fsPathFrom.test(filePath)) {
            const redirectPath = filePath.replace(fsPathFrom, fsPathTo);
            writeLog(`[🛡️ fsHook] 程序试图 fs.${property} 重定向 ${filePath} --> ${redirectPath}`);
            return fsHook.call(this, redirectPath, ...args);
      }
      writeLog(`[🛡️ fsHook] 程序试图 fs.${property} ${filePath}`);
      return fsHook.call(this, filePath, ...args);
    };
});
const fsPromisesHook = {};
['readFile', 'open', 'stat'].forEach((property) => {
    fsPromisesHook = fs.promises;
    fs.promises = async function (filePath, ...args) {
      if (typeof filePath == 'string' && fsPathFrom.test(filePath)) {
            const redirectPath = filePath.replace(fsPathFrom, fsPathTo);
            writeLog(`[🛡️ fsHook/Promises] 程序试图 fs.promises.${property} 重定向 ${filePath} --> ${redirectPath}`);
            return fsPromisesHook.call(this, redirectPath, ...args);
      }
      writeLog(`[🛡️ fsHook/Promises] 程序试图 fs.promises.${property} ${filePath}`);
      return fsPromisesHook.call(this, filePath, ...args);
    };
});

```

## 离线激活逻辑分析

> 本节参考了文章:(https://www.52pojie.cn/thread-2040749-1-1.html)

Typora激活分为在线激活以及离线激活,虽然作者有劫持在线激活思路,但由于缺少在线请求响应样本,故无法给出相应的代码。
作者通过上述参考文章中的离线激活样本,成功劫持了离线激活代码,故本文只对离线激活进行分析与调试。

### 前端逻辑定位

通过上文[注入调试与劫持代码]开启 DevTools 后,进入“离线激活”页面。输入任意字符并点击激活,发现界面无任何响应,包括激活失败提示,说明存在前端格式校验。
利用 DevTools 的断点调试功能,监听激活按钮点击事件,我们定位到了 React 状态机中的关键逻辑:



代码未混淆,逻辑如下:

```js
if ("+" == t || "#" == t)
// 激活码必须以 "+" 开头,或以 "#" 结尾

t = t.substr(1, t.length - 2)
// 去除激活码首&尾字符
// (注:Windows 环境下 window.webkit 为 false,后续逻辑可以忽略)

window.Setting.invokeWithCallback("offlineActivation", t);
// 核心:通过 Electron IPC 将处理后的激活码发送至主进程的 `offlineActivation` 频道

```

前端仅负责基础格式校验和 IPC 通信,真正的激活验证逻辑位于后端(主进程)。

为深入分析,我们对 IPC 通信进行监控:

```js
// IPC通信监控: invoke <-> handle
const invokeFilter = ['document.addSnapAndLastSync', 'document.setContent'];
const originalIpcMainHandle = electron.ipcMain.handle;
electron.ipcMain.handle = function (channel, listener) {
    // writeLog(` .handle 监听频道: "${channel}"`);
    const filter = !invokeFilter.includes(channel);
    return originalIpcMainHandle.call(this, channel, async (event, ...args) => {
      filter && writeLog(`[&#128064;IPC 请求] 收到 .invoke("${channel}") 参数:`, JSON.stringify(args));
      try {
            const result = await listener(event, ...args);
            filter && writeLog(`[&#128064;IPC 响应] .handle("${channel}") 返回结果:`, JSON.stringify(result));
            return result;
      } catch (error) {
            filter && writeLog(`[&#128064;IPC 错误] .handle("${channel}") 执行出错:`, error);
            throw error;
      }
    });
};

```

### RSA 公钥解密分析

通过参考(https://www.52pojie.cn/thread-2040749-1-1.html)这篇文章,可以得知 `.jsc` 内部预置了 RSA 公钥,用于解密传入的激活码。
由于缺乏私钥,我们无法生成合法的加密激活码。但只要能定位到解密函数,我们就能通过 **劫持返回值** 的方式,直接伪造解密后的明文数据,从而绕过解密过程。

经测试,v1.12.4 版本依旧使用 Node.js 原生 `crypto` 模块的 `publicDecrypt` 方法。我们可以对此进行劫持:

```js
const crypto = require('crypto');

const originalPublicDecrypt = crypto.publicDecrypt;
crypto.publicDecrypt = function (key, buffer) {
    writeLog('-------------------------------------------');
    writeLog('【&#128064; 监控】 crypto.publicDecrypt 被调用');
    writeLog('Key:', key);
    writeLog('Buffer (Hex):', buffer.toString('hex'));
    writeLog('-------------------------------------------');
    return originalPublicDecrypt.call(this, key, buffer);
};

```



输入符合前端规则的激活码(`+` 开头,`#` 结尾)后,日志显示 `crypto.publicDecrypt` 确实被调用。这验证了我们的切入点是正确的。

### 黑盒调试:推导解密后数据结构

根据 `crypto.publicDecrypt` API类型发现,只有在公钥与密文匹配时才会返回 Buffer,否则会抛出错误。随便输入的激活码会导致程序返回 `Please input a valid license code`。
为了探究程序期望的解密结果,我们不再调用原始公钥解密函数,而是直接强制返回一个我们自己构造的 Buffer。

通过黑盒测试,我们尝试推断程序如何处理解密后的 Buffer:

1. **假设一**:直接比对 Buffer?(经测试,无 `Buffer.compare / Buffer.equals` 等调用)
2. **假设二**:二次哈希验证?(经测试,无 `crypto.verify / crypto.createHash` 等调用)
3. **假设三**:转换为字符串再处理?(**命中**,检测到 `Buffer.toString('utf-8')` 调用)

```js
return new Proxy(Buffer.from('test'), {
      get(t, p, r) {
            writeLog('【&#128064; 监控】 Buffer get', String(p));
            const result = Reflect.get(t, p, r);
            // 如果结果为函数,二次监控其函数传参
            if (typeof result == 'function') {
                return new Proxy(result, {
                  apply(fn, thisArg, args) {
                        writeLog(`【&#128064; 监控】 Buffer.${String(p)} apply args=${JSON.stringify(args)}`);
                        try {
                            // 尝试先指向 Proxy
                            return Reflect.apply(fn, r, args);
                        } catch (e) {
                            // 再指向 Buffer
                            return Reflect.apply(fn, t, args);
                        }
                  },
                });
            } else {
                return result;
            }
      },
    });

```

日志显示 Buffer 被转为 UTF-8 字符串,并被读取了长度

```text
【&#128064; 监控】 Buffer get toString
【&#128064; 监控】 Buffer.toString apply args=["utf8"]
【&#128064; 监控】 Buffer get length
[&#128064;IPC 响应] .handle("offlineActivation") 返回结果:
```

尝试将字符串转变为Machine Code,发现结果仍不对,经过多轮尝试,剩余可能性已不多
进一步猜测,代码可能会将字符串通过`JSON.parse`解析为对象,然后对对象进行取值。
我们这次劫持 `JSON.parse` 去进行验证:

```js
    const result = Buffer.from(JSON.stringify({ test: '123'.repeat(50) }));
    if (!JSON.originalParse) {
      JSON.originalParse = JSON.parse;
      JSON.parse = function (text, ...args) {
            const obj = JSON.originalParse.call(this, text, ...args);
            return new Proxy(obj, {
                get(t, p, r) {
                  writeLog(`【&#128064; JSON监控】 ${text.slice(0, 12)}..."} 被访问属性`, p);
                  return Reflect.get(t, p, r);
                },
            });
      };
    }
    return result;

```

通过日志发现,我们终于命中了方法,并成功提取出了激活所需的关键字段。

```text
【&#128064; JSON监控】 {"test":"123..."} 被访问属性 deviceId
【&#128064; JSON监控】 {"test":"123..."} 被访问属性 fingerprint
【&#128064; JSON监控】 {"test":"123..."} 被访问属性 email
【&#128064; JSON监控】 {"test":"123..."} 被访问属性 license
【&#128064; JSON监控】 {"test":"123..."} 被访问属性 version
【&#128064; JSON监控】 {"test":"123..."} 被访问属性 date
【&#128064; JSON监控】 {"test":"123..."} 被访问属性 type

```

## 离线激活劫持

### 解码 Machine Code

离线激活界面显示的 `Machine Code` 显然是 Base64 编码。将其`atob`解密后得到以下内容:

```json
{
    "v": "win|1.12.4",
    "i": "CaXXXXXXXJ",
    "l": "XXXXXXX | XXXXXXX | Windows"
}

```

推测:`v` 为 `version`,`i` 为 `fingerprint`,`l` 可能对应 `deviceId`。

### 构造离线激活码

查阅相关文章后,我们大致确定了离线激活码可以是以下形式(部分字段可以随便填):

```json
{
"deviceId": "XXXXXXX | XXXXXXX | Windows",
"fingerprint": "CaXXXXXXXJ",
"email": "DreamNya@Dream.Nya",
"license": "Cracked_By_DreamNya",
"version": "win|1.12.4",
"date": "01/04/2026",
"type": "DreamNya"
}

```

### 劫持公钥解密函数返回值

修改 `crypto.publicDecrypt` 的 Hook 逻辑,直接返回上述 JSON 的 Buffer:

```js
crypto.publicDecrypt = function (key, buffer) {
    writeLog('-------------------------------------------');
    writeLog('【&#128064; 监控】 crypto.publicDecrypt 被调用');
    writeLog('Key:', key);
    writeLog('Buffer (Hex):', buffer.toString('hex'));
    // return originalPublicDecrypt.call(this, key, buffer);
    // 直接返回伪造的明文 Buffer
    return Buffer.from(
      JSON.stringify({
            deviceId: 'XXXXXXX | XXXXXXX | Windows',
            fingerprint: 'CaXXXXXXXJ',
            email: 'DreamNya@Dream.Nya',
            license: 'Cracked_By_DreamNya',
            version: 'win|1.12.4',
            date: '01/04/2026',
            type: 'DreamNya',
      }),
    );
};

```




查看 IPC 日志,响应终于从 `false` 变为 `true` 了,同时主界面左下角的“未激活”图标消失。
说明我们劫持`crypto.publicDecrypt`的方法确实有效,初步激活成功。

### 劫持联网验证

重启 Typora 后发现激活状态失效。分析日志发现,程序在启动时会再次调用公钥解密函数,由于该函数已被我们完全劫持,故本地校验仍通过了。
即使如此激活状态仍失效了,说明程序可能还存在远程验证。
我们可以通过抓包、劫持请求的方式去调试远程请求
经各种远程请求模块调试,最终发现 Typora 几乎均在用 `electron.net.request` 发送核心请求,
对此,我们可以利用 `electron.protocol.handle` 进行处理。

```js
// 请求日志&拦截
electron.app.whenReady().then(() => {
    electron.protocol.handle('https', async (request) => {
      writeLog(`[&#128064;electron.net Request] ${request.method} ${request.url}`);

      // 尝试打印 Request Body
      try {
            const reqClone = request.clone();
            const reqBody = await reqClone.text();
            if (reqBody) {
                writeLog(':', reqBody);
            }
      } catch {}

      const response = await electron.net.fetch(request, { bypassCustomProtocolHandlers: true });

      // 克隆响应用于劫持 原始响应后续直接转发
      const resClone = response.clone();
      resClone
            .text()
            .then((resText) => {
                writeLog(`[&#128064;electron.net Response] ${response.status} ${request.url}`);
                writeLog(':', resText.substring(0, 500));
            })
            .catch((err) => {
                console.error(':', err);
            });

      // 转发原始响应
      return response;
    });
});

```

经调试后发现,Typora在离线激活状态时,运行程序会自动将离线注册信息POST给`https://store.typora.io/api/client/renew`进行联网验证,
当响应结果为`{success:false}`时则自动清除之前的激活信息。
故我们直接通过请求`url`判断,拦截该`url`的请求,直接立即响应`{success:true}`,即可骗过验证。

```js
      // 拦截目标请求,伪造响应
      if (request.url == 'https://store.typora.io/api/client/renew') {
            return new Response(JSON.stringify({ success: true }), {
                status: 200,
                headers: { 'content-type': 'application/json' },
            });
      }

```

再次执行离线激活流程,更新代码、重启程序后,可以发现激活状态不会再掉了。
(建议在设置中关闭自动更新,并在最终成品中移除调试日志等不必要的代码)。



## 完结撒花

至此,我们仅凭 JavaScript 技术,就完成了Electron应用的逆向安全分析与实战应用。
本文展示了从反转 Fuses 配置限制、绕过文件完整性校验,到黑盒推导数据结构及网络请求劫持的完整流程。
但本文的目的不是为了分析、破解、激活特定软件,更多是一种通用的 Electron 应用安全分析思路。
旨在通过逆向分析的手段,挖掘到平时可能注意不到的安全漏洞、盲区,以便未来更好的正向开发。

steven026 发表于 2026-1-6 20:13

## 激活劫持的补充
关于仍然掉激活的情况,可以阅读 [#79](https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=2084047&pid=54595419) 以及 [#121](https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=2084047&pid=54602690)的研究分析
分析过程可以直接去看原帖,这里就直接放结论了,根据以下操作可以保持激活状态

### 更新拦截在线校验代码
经测试,在线校验不是一次性的,而是每12小时进行一次。
(程序运行一段时间后似乎会有二次随机校验)
在拦截`https://store.typora.io/api/client/renew`的请求时,响应JSON内容需要新增一个字段`msg`,这个字段和离线激活码相同,内容无所谓,但最好是个Base64字符串。
例如:
```js
            return new Response(JSON.stringify({ success: true, msg: btoa('DreamNya') }), {
                status: 200,
                headers: { 'content-type': 'application/json' },
            });
```

> 提示:`https://store.typora.io/api/client/renew`是默认的官方服务器,如果在设置中勾选了使用国内服务器,域名可能需要根据日志中实际请求的服务器域名进行修改。

### 手动修改注册表

激活信息存储在注册表 `HKEY_CURRENT_USER\Software\Typora` 中
Typora在进行上一节提到的在线校验时,会根据响应结果修改注册表中的`SLicense`字段内容。

`IDate`字段为15天试用的开始日期,修改该字段会影响剩余试用天数。

`SLicense`字段为激活信息,以`#`分割;
第一段内容为上一节提到的响应中的`msg`;
第二段`0`目前暂时不清楚有什么作用,可能代表激活成功;
第三段的日期经测试可能是存储的上一次在线校验时间。

因此在激活完毕后可以,手动修改一下`SLicense`的内容
例如,可以改为`RHJlYW1OeWE=#0#1/1/2029`。

studied 发表于 2026-1-8 20:05

跟着文章的流程走了一遍最后成功了,之前c和java的hook代码太难了,感谢楼主出的这期js的文章,受教了。

附一个我整理好的一键自动脚本,懒人可以直接运行:lol

const asar = require("asar");
const chalk = require("chalk");
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const readlineSync = require("readline-sync");
const WinReg = require("winreg");
const { flipFuses, FuseV1Options, FuseVersion } = require("@electron/fuses");

function getInsertCode(EnableHookDebug, atobMachineCode, email, nowDateStr) {
    return `
/** Hook破解开始 */
const electron = require("electron");

// 是否启用劫持调试
const HookDebug = ${EnableHookDebug ? "true" : "false"};

// 调试日志定义
const LOG_PATH = ".\\\\Typora_Hook_Log.txt";
//fs.rmSync(LOG_PATH, { force: true });
function writeLog(...data) {
    const log = \`[\${new Date().toLocaleString()}] \${data.join(
      " "
    )}\\n------------------\\n\`;
    fs.appendFileSync(LOG_PATH, log);
}

// 开启调试窗口,阻止关闭 app.quit 调用
// Hook Electron 模块,监控 BrowserWindow 实例化及阻止 app.quit 调用
// Node模块require后会进行缓存,即使再次require会指向同一个对象
if (HookDebug) {
    Object.defineProperty(electron.app, "quit", {
      value: function () {
            writeLog("[&#128737;️ 拦截] 程序试图调用 app.quit(),已阻止。");
      },
      writable: true,
      configurable: true,
    });
    electron.app.on("browser-window-created", (_event, win) => {
      writeLog("【&#128064; 监控】检测到 BrowserWindow 实例化!");

      // 确保dom-ready后再打开DevTools 否则第一个窗口可能会无法打开
      win.webContents.once("dom-ready", () => {
            writeLog("【&#128295;】打开 DevTools...");
            win.webContents.openDevTools({ mode: "detach" });
      });
    });
}

// Hook fs 模块,重定向对 resources/app 目录的访问
// resources/app/ → resources/app.bak/
const fsPathFrom = /resources[\\\\/]app[\\\\/]/i;
const fsPathTo = "resources\\\\app.bak\\\\";
const fsHook = {};
[
    "readFileSync",
    "readFile",
    "statSync",
    "stat",
    "Stats",
    "StatsFs",
    "open",
    "openSync",
].forEach((property) => {
    fsHook = fs;
    fs = function (filePath, ...args) {
      if (typeof filePath == "string" && fsPathFrom.test(filePath)) {
            const redirectPath = filePath.replace(fsPathFrom, fsPathTo);
            writeLog(
                \`[&#128737;️ fsHook] 程序试图 fs.\${property} 重定向 \${filePath} --> \${redirectPath}\`
            );
            return fsHook.call(this, redirectPath, ...args);
      }
      writeLog(\`[&#128737;️ fsHook] 程序试图 fs.\${property} \${filePath}\`);
      return fsHook.call(this, filePath, ...args);
    };
});
const fsPromisesHook = {};
["readFile", "open", "stat"].forEach((property) => {
    fsPromisesHook = fs.promises;
    fs.promises = async function (filePath, ...args) {
      if (typeof filePath == "string" && fsPathFrom.test(filePath)) {
            const redirectPath = filePath.replace(fsPathFrom, fsPathTo);
            writeLog(
                \`[&#128737;️ fsHook/Promises] 程序试图 fs.promises.\${property} 重定向 \${filePath} --> \${redirectPath}\`
            );
            return fsPromisesHook.call(this, redirectPath, ...args);
      }
      writeLog(
            \`[&#128737;️ fsHook/Promises] 程序试图 fs.promises.\${property} \${filePath}\`
      );
      return fsPromisesHook.call(this, filePath, ...args);
    };
});

// IPC 通信进行监控
if (HookDebug) {
    const invokeFilter = ["document.addSnapAndLastSync", "document.setContent"];
    const originalIpcMainHandle = electron.ipcMain.handle;
    electron.ipcMain.handle = function (channel, listener) {
      // writeLog(\` .handle 监听频道: "\${channel}"\`);
      const filter = !invokeFilter.includes(channel);
      return originalIpcMainHandle.call(this, channel, async (event, ...args) => {
            filter &&
                writeLog(
                  \`[&#128064;IPC 请求] 收到 .invoke("\${channel}") 参数:\`,
                  JSON.stringify(args)
                );
            try {
                const result = await listener(event, ...args);
                filter &&
                  writeLog(
                        \`[&#128064;IPC 响应] .handle("\${channel}") 返回结果:\`,
                        JSON.stringify(result)
                  );
                return result;
            } catch (error) {
                filter && writeLog(\`[&#128064;IPC 错误] .handle("\${channel}") 执行出错:\`, error);
                throw error;
            }
      });
    };
}


const crypto = require("crypto");

const originalPublicDecrypt = crypto.publicDecrypt;
crypto.publicDecrypt = function (key, buffer) {
    if (HookDebug) {
      writeLog("-------------------------------------------");
      writeLog("【&#128064; 监控】 crypto.publicDecrypt 被调用");
      writeLog("Key:", key);
      writeLog("Buffer (Hex):", buffer.toString("hex"));
    }
    // return originalPublicDecrypt.call(this, key, buffer);
    // 直接返回伪造的明文 Buffer
    return Buffer.from(
      JSON.stringify({
            deviceId: "${atobMachineCode.l}",
            fingerprint: "${atobMachineCode.i}",
            email: "${email}",
            license: "Cracked_By_DreamNya",
            version: "${atobMachineCode.v}",
            date: "${nowDateStr}",
            type: "DreamNya",
      })
    );
};

// 劫持联网验证
electron.app.whenReady().then(() => {
    electron.protocol.handle("https", async (request) => {
      writeLog(\`[&#128064;electron.net Request] \${request.method} \${request.url}\`);

      writeLog("request.url typeof:", typeof request.url, "value:", request.url);
      // 拦截目标请求,伪造响应
      if (request.url === "https://store.typora.io/api/client/renew") {
            if (HookDebug){
                writeLog(\`[&#128737;️ 拦截] 伪造激活验证响应: {success:true, msg: \${btoa("DreamNya")}}\`);
            }
            return new Response(
                JSON.stringify({ success: true, msg: btoa("DreamNya") }),
                {
                  status: 200,
                  headers: { "content-type": "application/json" },
                }
            );
      }

      if (HookDebug) {
            // 尝试打印 Request Body
            try {
                const reqClone = request.clone();
                const reqBody = await reqClone.text();
                if (reqBody) {
                  writeLog(':', reqBody);
                }
            } catch { }

            // 其他请求正常转发
            const response = await electron.net.fetch(request, { bypassCustomProtocolHandlers: true });

            // 克隆响应用于日志
            const resClone = response.clone();
            resClone
                .text()
                .then((resText) => {
                  writeLog(\`[&#128064;electron.net Response] \${response.status} \${request.url}\`);
                  writeLog(':', resText.substring(0, 500));
                })
                .catch((err) => {
                  console.error(':', err);
                });

            return response;
      }


    });
});
/** Hook破解结束 */
`;
}

let EnableBackup = false; // 是否备份原始文件
let EnableHookDebug = false; // 是否启用调试日志

const Typora_Installation_Path = "D:\\software\\Typora";
const resourcesPath = path.join(Typora_Installation_Path, "resources");
const asarPath = path.join(resourcesPath, "app.asar");
const appDir = path.join(resourcesPath, "app");
const appBakDir = path.join(resourcesPath, "app.bak");
const asarBakPath = path.join(resourcesPath, "app.asar.bak");
const TyporaEXE = path.join(Typora_Installation_Path, "Typora.exe");
const LaunchDistJS = path.join(appDir, "launch.dist.js");

// 随机生成一个符合前端验证格式的注册码
function generateRegCode() {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    let code = '+';
    for (let i = 0; i < 8; i++) {
      code += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    code += '#';
    return code;
}

function closeTyporaProcesses() {
    try {
      execSync("taskkill /F /IM Typora.exe");
      console.log(chalk.green("已关闭所有 Typora.exe 进程"));
    } catch (e) {
      console.log(chalk.red("Typora.exe 未运行或关闭失败,请手动关闭后继续。"));
    }

    console.log(
      chalk.yellow(
            "已尝试自动关闭所有 Typora.exe 进程,如果未关闭请手动关闭后再运行此程序。"
      )
    );

    // 回车继续
    console.log(chalk.cyan("请按回车键继续..."));
    readlineSync.question();
}

function setRegValue(regKey, name, value) {
    return new Promise((resolve, reject) => {
      regKey.set(name, WinReg.REG_SZ, value, function (err) {
            if (err) reject(err);
            else resolve();
      });
    });
}

function getNowDateStr() {
    const now = new Date();
    const dd = String(now.getDate()).padStart(2, "0");
    const mm = String(now.getMonth() + 1).padStart(2, "0");
    const yyyy = now.getFullYear();
    return `${mm}/${dd}/${yyyy}`;
}
const nowDateStr = getNowDateStr();

// 要求输入机器码和邮箱
console.log(chalk.cyan("请输入机器码: "));
const machineCode = readlineSync.question();
console.log(chalk.cyan("请输入邮箱: "));
const email = readlineSync.question();
// 询问是否开启备份(默认开启)与调试(默认关闭)
console.log(chalk.cyan("请选择是否开启备份与调试选项:"));
console.log(chalk.cyan("【建议开启】是否开启备份?(Y/N): "));
const backupAnswer = readlineSync.question();
console.log(chalk.cyan("【建议关闭】是否开启调试?(Y/N): "));
const debugAnswer = readlineSync.question();
EnableBackup = backupAnswer.toLowerCase() === "y";
EnableHookDebug = debugAnswer.toLowerCase() === "y";

// Base64 解码
function atob(str) {
    return Buffer.from(str, "base64").toString("utf-8");
}
const atobMachineCode = JSON.parse(atob(machineCode));

console.log(chalk.yellow("deviceId: " + atobMachineCode.l));
console.log(chalk.yellow("fingerprint: " + atobMachineCode.i));
console.log(chalk.yellow("version: " + atobMachineCode.v));

// 关闭所有 Typora.exe 进程
closeTyporaProcesses();
console.log(chalk.green("==== 开始破解... ===="));

async function main() {
    // 一、反反调试

    console.log(chalk.yellow("一、正在进行反反调试操作..."));
    console.log(chalk.yellow("解包 asar"));
    await asar.extractAll(asarPath, appDir);

    console.log(
      chalk.yellow("复制 app 到 app.bak(递归复制)【应对完整性校验】")
    );
    // 2. 复制 app 到 app.bak(递归复制)
    function copyDir(src, dest) {
      if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
      for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
            const srcPath = path.join(src, entry.name);
            const destPath = path.join(dest, entry.name);
            if (entry.isDirectory()) {
                copyDir(srcPath, destPath);
            } else {
                fs.copyFileSync(srcPath, destPath);
            }
      }
    }
    copyDir(appDir, appBakDir);

    console.log(chalk.yellow("移除 app.asar 文件"));
    // 3. 重命名 app.asar 为 app.asar.bak
    if (EnableBackup) {
      fs.renameSync(asarPath, asarBakPath);
    } else {
      fs.rmSync(asarPath, { force: true });
    }

    console.log(
      chalk.yellow("修改 Typora.exe 的 fuse 配置,允许加载未打包的 app 目录")
    );

    if (EnableBackup) {
      // 修改前先备份
      fs.copyFileSync(TyporaEXE, `${TyporaEXE}.bak`);
    }

    // 修改fuse配置(同时会修改程序hash)
    flipFuses(TyporaEXE, {
      version: FuseVersion.V1,
      : false,
    });

    console.log(chalk.green("反反调试操作完成!"));

    // 二、注入破解代码
    console.log(chalk.yellow("二、正在注入破解代码到 launch.dist.js..."));

    // 读取原文件内容
    let content = fs.readFileSync(LaunchDistJS, "utf-8");

    // 查找第一个require语句后的分号
    const requireRegex = /require\([^)]+\);/;
    const match = requireRegex.exec(content);

    if (match) {
      const insertPos = match.index + match.length;
      const insertCode = getInsertCode(
            EnableHookDebug,
            atobMachineCode,
            email,
            nowDateStr
      );
      // 插入代码
      content =
            content.slice(0, insertPos) + insertCode + content.slice(insertPos);
      fs.writeFileSync(LaunchDistJS, content, "utf-8");
      console.log(chalk.green("成功插入破解代码到 launch.dist.js"));
    } else {
      console.log(
            chalk.red("未找到 require 语句,破解代码未插入launch.dist.js。")
      );
    }

    console.log(chalk.green("注入破解代码完成!"));

    // // 三、注册激活
    // console.log(chalk.yellow("三、正在注册激活..."));
    // try {
    //   execSync(`start "" "${TyporaEXE}"`);
    //   console.log(chalk.green("Typora 已启动!"));
    // } catch (e) {
    //   console.log(chalk.red("Typora 启动失败,请手动打开。"));
    // }

    // const regCode = generateRegCode();
    // console.log(chalk.green(`您的注册码为:${regCode}`));
    // console.log(chalk.yellow("请复制并用于激活。"));
    // console.log(chalk.cyan("请按回车键继续..."));


    // // 关闭Typora进程
    // closeTyporaProcesses();

    // 三、修改注册表
    console.log(chalk.yellow("三、正在修改注册表以关闭联网验证..."));
    // 修改注册表,尽量关闭联网验证
    // 注册表路径
    const regKey = new WinReg({
      hive: WinReg.HKCU,
      key: "\\Software\\Typora",
    });

    try {
      await setRegValue(regKey, "SLicense", "RHJlYW1OeWE=#0#1/1/2029");
      console.log(chalk.green("SLicense 注册表字段写入成功"));
      await setRegValue(regKey, "IDate", nowDateStr);
      console.log(chalk.green("IDate 注册表字段写入成功"));
    } catch (err) {
      console.log(chalk.red("写入注册表失败:"), err);
    }

    console.log(chalk.green("==== 破解完成!使用愉快!===="));
    console.log(chalk.yellow("后续操作建议:\n"));
    const regCode = generateRegCode();
    console.log(chalk.green(`\t1.您的注册码为:${regCode} 请复制并用于激活。`));
    console.log(chalk.yellow("\t2. 关闭【自动检查更新】功能,防止被覆盖。"));
    console.log(
      chalk.yellow(
            "\t3. 关闭【Typora服务器使用国内服务器】功能,避免绕过联网验证失败。"
      )
    );
}

main();

凉风 发表于 2026-1-18 14:31

q51658258 发表于 2026-1-17 20:59
好像只是拦截联网验证功能

图片不能显示很可能是图片资源设置了防盗链导致的,这种只能手动下载

源代码里的
      if (HookDebug) {
            // 尝试打印 Request Body
            try {
                const reqClone = request.clone();
                const reqBody = await reqClone.text();
                if (reqBody) {
                  writeLog(':', reqBody);
                }
            } catch { }

            // 其他请求正常转发
            const response = await electron.net.fetch(request, { bypassCustomProtocolHandlers: true });

            // 克隆响应用于日志
            const resClone = response.clone();
            resClone
                .text()
                .then((resText) => {
                  writeLog(\`[&#128064;electron.net Response] \${response.status} \${request.url}\`);
                  writeLog(':', resText.substring(0, 500));
                })
                .catch((err) => {
                  console.error(':', err);
                });

            return response;
      }
修改后的
      if (HookDebug) {
            // 尝试打印 Request Body
            try {
                const reqClone = request.clone();
                const reqBody = await reqClone.text();
                if (reqBody) {
                  writeLog(':', reqBody);
                }
            } catch { }

            // 其他请求正常转发
            const response = await electron.net.fetch(request, { bypassCustomProtocolHandlers: true });

            // 克隆响应用于日志
            const resClone = response.clone();
            resClone
                .text()
                .then((resText) => {
                  writeLog(\`[&#128064;electron.net Response] \${response.status} \${request.url}\`);
                  writeLog(':', resText.substring(0, 500));
                })
                .catch((err) => {
                  console.error(':', err);
                });

            return response;
      }else{
             // 非调试模式:静默转发请求
            return electron.net.fetch(request, { bypassCustomProtocolHandlers: true });
      }
修改后的是可以的。

NorthEgg 发表于 2026-1-6 17:08

本帖最后由 NorthEgg 于 2026-9-12 13:16 编辑

2026.9.12更新
新的Bun 1.4.2版本可以不外挂regedit依赖了编译后只有一个exe
新的源码


以下为历史回复-------------------------------------------------------------------------------------------------------------
用JS实现了一个自动破解脚本,有兴趣的可以看看,下面附上源码


下面是使用Bun打包的exe可执行程序,不是很熟悉Bun的SEA不保证可用,我自己另一台Windows电脑测试可以使用。
链接:https://pan.quark.cn/s/10cf7491b2e7
提取码:YCRj

m1y3ll0w 发表于 2026-1-5 18:17

本帖最后由 m1y3ll0w 于 2026-1-6 12:47 编辑

Typora 1.12.4 Win64 验证可用!感谢大佬!
按照楼主的步骤操作的话,建议点“只看该作者”,大佬有很多补充和回复!
比如:“不要在Typora目录下安装fuse”、“fingerprint应该要填入你Machine Code中的base64解码对应的i字段内容”、“离线激活界面,激活码规则(+ 开头,# 结尾)”

提供一个免安装 Node.js 食用方式 ↓
(为什么不直接给成品?论坛规章不允许!大佬这个帖子相比之前的破-解方式,已经是比较好实践操作的了)

1. 手动修补 OnlyLoadAppFromAsar
010 Editor 打开安装目录下主程序 Typora.exe,Ctrl + F 搜索类型为 Hex Bytes(注意先结束Typora,安装在系统盘可能需要以管理员身份运行 010 Editor)
31 30 31 00 00 -> 30 30 31 00 00

2. 下载坛友提供的 app.zip
https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=2084047&pid=54593474
解压后得到 app 文件夹,复制这个 app 文件夹并重命名为 app.bak,即可得到重定向文件夹,app.bak 文件夹内,删掉 launch.dist.js,把那个副本文件还原成 launch.dist.js 即可。(注意,坛友这个 app.zip 只适用 1.12.4,尽快下载,避免后续被删)

修改 app 文件夹中 launch.dist.js 里面的 deviceId 和 fingerprint
离线激活界面的 Machine Code,Base64 解码后可以得到这些信息,浏览器搜“Base64 解码”有在线解码工具站
解码后,v 对应 version,i 对应 fingerprint,l 对应 deviceId,对应复制进去即可

3. 修复注册表信息
https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=2084047&pid=54595235

3.1 或者按照楼主置顶评论,手动修改注册表信息
https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=2084047&pid=54595419

4. 有坛友提供了自动修补程序,原理应该也是跟上面的操作类似,但是,需要自行验证是否有木马病毒!!
(评论已被删除!成品、Z册机、补丁等都不让提供!)

5. 也可以参考之前大佬拦截修改在线激活返回信息的方式,融合到现在这个方案中。他的方法支持到 1.9.5,非常稳定!
https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=2084047&pid=54602848

【有经济实力的自然是购买蒸蚌更香】

steven026 发表于 2026-1-5 09:44

Cleopatra 发表于 2026-1-5 08:55
真的强,想问一下反调试这个花了多少的时间才能推导出来

这篇文章断断续续总共写了3天吧
反反调试/拦截分析,都是通用的原生JS技术,说难不难、说简单也不简单,至少要非常熟悉正向开发,才能知道去哪里埋点
Typora逆向最核心的地方在于jsc字节码,所有核心逻辑都写在这里面,我尝试过好久的反编译jsc结果连静态字符串都看不到……
所以无奈只能从JS层面去做黑盒调试,一点一点去猜出来他的逻辑

steven026 发表于 2026-1-4 19:37

我凌晨的时候测试,同样的代码,无论多少次重新启动都不会掉激活
现在重新打开又会掉激活了,怀疑可能是激活码有时效性?

经测试:
第1次打开时未激活,离线激活后成功
第2次打开时已激活,本地校验、远程校验均成功
第3次打开时未激活,再次离线激活后成功
之后会一直循环下去2~3
说明第2此打开时可能存在一个未被发现的校验,或者是远程校验不仅需要返回success:true,还需要返回其他未知字段

观察`AppData\Roaming\Typora\typora.log`发现了一段日志内容
程序会从注册表`HKEY_CURRENT_USER\Software\Typora`读取2个字段`IDate`和`SLicense`
`IDate`显然是试用日期
`SLicense`显然是离线激活的注册信息
通过` license renewed in 12h`可以判断,程序会每12小时远程校验一次,这也许就对上了我开头说的凌晨成功,晚上失败
至于远程校验的时间可能和`1/4/2026`以及时区有关

尝试将`SLicense`中的日期改成2029年,`IDate`改成15天前已失效日期
再次反复关闭再运行程序,发现激活状态又不会掉了
可以推断`SLicense`中的日期修改可能起作用了

INFO 2026/1/4 19:24:0455OS is win32 10.0.26100
INFO 2026/1/4 19:24:0455start LM in devVersion=false
INFO 2026/1/4 19:24:0458
DEBUG 2026/1/4 19:24:0458 SLicense : aaaa#0#1/4/2029
DEBUG 2026/1/4 19:24:0458pure = undefined
INFO 2026/1/4 19:24:0459renew
DEBUG 2026/1/4 19:24:0459 license renewed in 12h
DEBUG 2026/1/4 19:24:0459 pass
DEBUG 2026/1/4 19:24:0459 IDate : 12/1/2025
DEBUG 2026/1/4 19:24:0459trailRemains is 0
INFO 2026/1/4 19:24:0459 installDate is 12/1/2025, trail remains: 0 days
DEBUG 2026/1/4 19:24:0459after setting
INFO 2026/1/4 19:24:0459typora version: 1.12.4
INFO 2026/1/4 19:24:0484 hasL: true

因此我们在完成js替换并成功激活后,可以尝试修改下注册表中的激活日期
至于上述推测是否正确,可能要明天才能再次测试了

justfly99 发表于 2026-1-4 15:22

不错,下载个1.12.4的试试。跑去官网下载的注意了,现在最新的是1.12.6的了,提示是修复了激活问题,估计是修复了这个的

笨笨家的唯一 发表于 2026-8-1 17:03

依据ai的建议调整了一下js文件:
const asar = require("asar");
const chalk = require("chalk");
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const readlineSync = require("readline-sync");
const WinReg = require("winreg");
const { flipFuses, FuseV1Options, FuseVersion } = require("@electron/fuses");

function getInsertCode(EnableHookDebug, atobMachineCode, email, nowDateStr) {
return `
/** Hook破解开始 */
const electron = require("electron");

// 是否启用劫持调试
const HookDebug = ${EnableHookDebug ? "true" : "false"};

// 调试日志定义
const LOG_PATH = ".\\\\Typora_Hook_Log.txt";
//fs.rmSync(LOG_PATH, { force: true });
function writeLog(...data) {
    const log = \`[\${new Date().toLocaleString()}] \${data.join(
      " "
    )}\\n------------------\\n\`;
    fs.appendFileSync(LOG_PATH, log);
}

// 开启调试窗口,阻止关闭 app.quit 调用
// Hook Electron 模块,监控 BrowserWindow 实例化及阻止 app.quit 调用
// Node模块require后会进行缓存,即使再次require会指向同一个对象
if (HookDebug) {
    Object.defineProperty(electron.app, "quit", {
      value: function () {
            writeLog("[&#128737;️ 拦截] 程序试图调用 app.quit(),已阻止。");
      },
      writable: true,
      configurable: true,
    });
    electron.app.on("browser-window-created", (_event, win) => {
      writeLog("【&#128064; 监控】检测到 BrowserWindow 实例化!");

      // 确保dom-ready后再打开DevTools 否则第一个窗口可能会无法打开
      win.webContents.once("dom-ready", () => {
            writeLog("【&#128295;】打开 DevTools...");
            win.webContents.openDevTools({ mode: "detach" });
      });
    });
}

// Hook fs 模块,重定向对 resources/app 目录的访问
// resources/app/ → resources/app.bak/
const fsPathFrom = /resources[\\\\/]app[\\\\/]/i;
const fsPathTo = "resources\\\\app.bak\\\\";
const fsHook = {};
[
    "readFileSync",
    "readFile",
    "statSync",
    "stat",
    "Stats",
    "StatsFs",
    "open",
    "openSync",
].forEach((property) => {
    fsHook = fs;
    fs = function (filePath, ...args) {
      if (typeof filePath == "string" && fsPathFrom.test(filePath)) {
            const redirectPath = filePath.replace(fsPathFrom, fsPathTo);
            writeLog(
                \`[&#128737;️ fsHook] 程序试图 fs.\${property} 重定向 \${filePath} --> \${redirectPath}\`
            );
            return fsHook.call(this, redirectPath, ...args);
      }
      writeLog(\`[&#128737;️ fsHook] 程序试图 fs.\${property} \${filePath}\`);
      return fsHook.call(this, filePath, ...args);
    };
});
const fsPromisesHook = {};
["readFile", "open", "stat"].forEach((property) => {
    fsPromisesHook = fs.promises;
    fs.promises = async function (filePath, ...args) {
      if (typeof filePath == "string" && fsPathFrom.test(filePath)) {
            const redirectPath = filePath.replace(fsPathFrom, fsPathTo);
            writeLog(
                \`[&#128737;️ fsHook/Promises] 程序试图 fs.promises.\${property} 重定向 \${filePath} --> \${redirectPath}\`
            );
            return fsPromisesHook.call(this, redirectPath, ...args);
      }
      writeLog(
            \`[&#128737;️ fsHook/Promises] 程序试图 fs.promises.\${property} \${filePath}\`
      );
      return fsPromisesHook.call(this, filePath, ...args);
    };
});

// IPC 通信进行监控
if (HookDebug) {
    const invokeFilter = ["document.addSnapAndLastSync", "document.setContent"];
    const originalIpcMainHandle = electron.ipcMain.handle;
    electron.ipcMain.handle = function (channel, listener) {
      // writeLog(\` .handle 监听频道: "\${channel}"\`);
      const filter = !invokeFilter.includes(channel);
      return originalIpcMainHandle.call(this, channel, async (event, ...args) => {
            filter &&
                writeLog(
                  \`[&#128064;IPC 请求] 收到 .invoke("\${channel}") 参数:\`,
                  JSON.stringify(args)
                );
            try {
                const result = await listener(event, ...args);
                filter &&
                  writeLog(
                        \`[&#128064;IPC 响应] .handle("\${channel}") 返回结果:\`,
                        JSON.stringify(result)
                  );
                return result;
            } catch (error) {
                filter && writeLog(\`[&#128064;IPC 错误] .handle("\${channel}") 执行出错:\`, error);
                throw error;
            }
      });
    };
}


const crypto = require("crypto");

const originalPublicDecrypt = crypto.publicDecrypt;
crypto.publicDecrypt = function (key, buffer) {
    if (HookDebug) {
      writeLog("-------------------------------------------");
      writeLog("【&#128064; 监控】 crypto.publicDecrypt 被调用");
      writeLog("Key:", key);
      writeLog("Buffer (Hex):", buffer.toString("hex"));
    }
    // return originalPublicDecrypt.call(this, key, buffer);
    // 直接返回伪造的明文 Buffer
    return Buffer.from(
      JSON.stringify({
            deviceId: "${atobMachineCode.l}",
            fingerprint: "${atobMachineCode.i}",
            email: "${email}",
            license: "Cracked_By_DreamNya",
            version: "${atobMachineCode.v}",
            date: "${nowDateStr}",
            type: "DreamNya",
      })
    );
};

// 劫持联网验证
electron.app.whenReady().then(() => {
    electron.protocol.handle("https", async (request) => {
      writeLog(\`[&#128064;electron.net Request] \${request.method} \${request.url}\`);

      writeLog("request.url typeof:", typeof request.url, "value:", request.url);
      // 拦截目标请求,伪造响应
      if (request.url === "https://store.typora.io/api/client/renew") {
            if (HookDebug){
                writeLog(\`[&#128737;️ 拦截] 伪造激活验证响应: {success:true, msg: \${btoa("DreamNya")}}\`);
            }
            return new Response(
                JSON.stringify({ success: true, msg: btoa("DreamNya") }),
                {
                  status: 200,
                  headers: { "content-type": "application/json" },
                }
            );
      }

      if (HookDebug) {
            // 尝试打印 Request Body
            try {
                const reqClone = request.clone();
                const reqBody = await reqClone.text();
                if (reqBody) {
                  writeLog(':', reqBody);
                }
            } catch { }

            // 其他请求正常转发
            const response = await electron.net.fetch(request, { bypassCustomProtocolHandlers: true });

            // 克隆响应用于日志
            const resClone = response.clone();
            resClone
                .text()
                .then((resText) => {
                  writeLog(\`[&#128064;electron.net Response] \${response.status} \${request.url}\`);
                  writeLog(':', resText.substring(0, 500));
                })
                .catch((err) => {
                  console.error(':', err);
                });

            return response;
      }


    });
});
/** Hook破解结束 */
`;
}

let EnableBackup = false; // 是否备份原始文件
let EnableHookDebug = false; // 是否启用调试日志

const Typora_Installation_Path = "D:\\Program Files\\Typora";
const resourcesPath = path.join(Typora_Installation_Path, "resources");
const asarPath = path.join(resourcesPath, "app.asar");
const appDir = path.join(resourcesPath, "app");
const appBakDir = path.join(resourcesPath, "app.bak");
const asarBakPath = path.join(resourcesPath, "app.asar.bak");
const TyporaEXE = path.join(Typora_Installation_Path, "Typora.exe");
const LaunchDistJS = path.join(appDir, "launch.dist.js");

// 随机生成一个符合前端验证格式的注册码
function generateRegCode() {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let code = "+";
for (let i = 0; i < 8; i++) {
    code += chars.charAt(Math.floor(Math.random() * chars.length));
}
code += "#";
return code;
}

function closeTyporaProcesses() {
// 杀掉所有 Typora 相关进程
const processes = ["Typora.exe", "TyporaUpdate.exe", "TyporaHelper.exe"];
let anyKilled = false;

for (const proc of processes) {
    try {
      execSync(`taskkill /F /IM ${proc}`, { stdio: "pipe" });
      console.log(chalk.green(`已关闭 ${proc} 进程`));
      anyKilled = true;
    } catch (e) {
      // 进程不存在或已关闭,忽略
    }
}

if (!anyKilled) {
    console.log(
      chalk.red("Typora 相关进程未运行或关闭失败,请手动关闭后继续。"),
    );
}

// 等待系统释放文件句柄
const waitMs = 3000;
console.log(chalk.yellow(`等待 ${waitMs / 1000} 秒让系统释放文件锁...`));
const waitEnd = Date.now() + waitMs;
while (Date.now() < waitEnd) {
    // 忙等待,让出 CPU
}

// 回车继续
console.log(chalk.cyan("请按回车键继续..."));
readlineSync.question();
}

// flipFuses 带重试机制的封装(处理 EBUSY 文件锁错误)
async function flipFusesWithRetry(exePath, fuseConfig, maxRetries = 5) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await flipFuses(exePath, fuseConfig);
      return;
    } catch (err) {
      if (err.code === "EBUSY" && attempt < maxRetries) {
      console.log(
          chalk.yellow(
            `\n文件被锁定: ${err.path}。再次尝试关闭 Typora 进程,${
            3 * attempt
            } 秒后重试 (${attempt}/${maxRetries})...`,
          ),
      );
      // 再次尝试关闭 Typora 进程
      try {
          execSync("taskkill /F /IM Typora.exe", { stdio: "pipe" });
          execSync("taskkill /F /IM TyporaUpdate.exe", {
            stdio: "pipe",
          });
      } catch (e) {
          // 忽略
      }
      // 等待文件句柄释放
      const waitMs = 3000 * attempt;
      const waitEnd = Date.now() + waitMs;
      while (Date.now() < waitEnd) {
          // 忙等待,让出 CPU
      }
      } else {
      throw err;
      }
    }
}
}

function setRegValue(regKey, name, value) {
return new Promise((resolve, reject) => {
    regKey.set(name, WinReg.REG_SZ, value, function (err) {
      if (err) reject(err);
      else resolve();
    });
});
}

function getNowDateStr() {
const now = new Date();
const dd = String(now.getDate()).padStart(2, "0");
const mm = String(now.getMonth() + 1).padStart(2, "0");
const yyyy = now.getFullYear();
return `${mm}/${dd}/${yyyy}`;
}
const nowDateStr = getNowDateStr();

// 要求输入机器码和邮箱
console.log(chalk.cyan("请输入机器码: "));
const machineCode = readlineSync.question();
console.log(chalk.cyan("请输入邮箱: "));
const email = readlineSync.question();
// 询问是否开启备份(默认开启)与调试(默认关闭)
console.log(chalk.cyan("请选择是否开启备份与调试选项:"));
console.log(chalk.cyan("【建议开启】是否开启备份?(Y/N): "));
const backupAnswer = readlineSync.question();
console.log(chalk.cyan("【建议关闭】是否开启调试?(Y/N): "));
const debugAnswer = readlineSync.question();
EnableBackup = backupAnswer.toLowerCase() === "y";
EnableHookDebug = debugAnswer.toLowerCase() === "y";

// Base64 解码
function atob(str) {
return Buffer.from(str, "base64").toString("utf-8");
}
const atobMachineCode = JSON.parse(atob(machineCode));

console.log(chalk.yellow("deviceId: " + atobMachineCode.l));
console.log(chalk.yellow("fingerprint: " + atobMachineCode.i));
console.log(chalk.yellow("version: " + atobMachineCode.v));

// 关闭所有 Typora.exe 进程
closeTyporaProcesses();
console.log(chalk.green("==== 开始破解... ===="));

async function main() {
// 一、反反调试

console.log(chalk.yellow("一、正在进行反反调试操作..."));
console.log(chalk.yellow("解包 asar"));
await asar.extractAll(asarPath, appDir);

console.log(
    chalk.yellow("复制 app 到 app.bak(递归复制)【应对完整性校验】"),
);
// 2. 复制 app 到 app.bak(递归复制)
function copyDir(src, dest) {
    if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
    for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
      const srcPath = path.join(src, entry.name);
      const destPath = path.join(dest, entry.name);
      if (entry.isDirectory()) {
      copyDir(srcPath, destPath);
      } else {
      fs.copyFileSync(srcPath, destPath);
      }
    }
}
copyDir(appDir, appBakDir);

console.log(chalk.yellow("移除 app.asar 文件"));
// 3. 重命名 app.asar 为 app.asar.bak
if (EnableBackup) {
    fs.renameSync(asarPath, asarBakPath);
} else {
    fs.rmSync(asarPath, { force: true });
}

console.log(
    chalk.yellow("修改 Typora.exe 的 fuse 配置,允许加载未打包的 app 目录"),
);

if (EnableBackup) {
    // 修改前先备份
    fs.copyFileSync(TyporaEXE, `${TyporaEXE}.bak`);
}

// 修改fuse配置(同时会修改程序hash),带重试机制处理文件锁
await flipFusesWithRetry(TyporaEXE, {
    version: FuseVersion.V1,
    : false,
});

console.log(chalk.green("反反调试操作完成!"));

// 二、注入破解代码
console.log(chalk.yellow("二、正在注入破解代码到 launch.dist.js..."));

// 读取原文件内容
let content = fs.readFileSync(LaunchDistJS, "utf-8");

// 查找第一个require语句后的分号
const requireRegex = /require\([^)]+\);/;
const match = requireRegex.exec(content);

if (match) {
    const insertPos = match.index + match.length;
    const insertCode = getInsertCode(
      EnableHookDebug,
      atobMachineCode,
      email,
      nowDateStr,
    );
    // 插入代码
    content =
      content.slice(0, insertPos) + insertCode + content.slice(insertPos);
    fs.writeFileSync(LaunchDistJS, content, "utf-8");
    console.log(chalk.green("成功插入破解代码到 launch.dist.js"));
} else {
    console.log(
      chalk.red("未找到 require 语句,破解代码未插入launch.dist.js。"),
    );
}

console.log(chalk.green("注入破解代码完成!"));

// // 三、注册激活
// console.log(chalk.yellow("三、正在注册激活..."));
// try {
//   execSync(`start "" "${TyporaEXE}"`);
//   console.log(chalk.green("Typora 已启动!"));
// } catch (e) {
//   console.log(chalk.red("Typora 启动失败,请手动打开。"));
// }

// const regCode = generateRegCode();
// console.log(chalk.green(`您的注册码为:${regCode}`));
// console.log(chalk.yellow("请复制并用于激活。"));
// console.log(chalk.cyan("请按回车键继续..."));

// // 关闭Typora进程
// closeTyporaProcesses();

// 三、修改注册表
console.log(chalk.yellow("三、正在修改注册表以关闭联网验证..."));
// 修改注册表,尽量关闭联网验证
// 注册表路径
const regKey = new WinReg({
    hive: WinReg.HKCU,
    key: "\\Software\\Typora",
});

try {
    await setRegValue(regKey, "SLicense", "RHJlYW1OeWE=#0#1/1/2029");
    console.log(chalk.green("SLicense 注册表字段写入成功"));
    await setRegValue(regKey, "IDate", nowDateStr);
    console.log(chalk.green("IDate 注册表字段写入成功"));
} catch (err) {
    console.log(chalk.red("写入注册表失败:"), err);
}

console.log(chalk.green("==== 破解完成!使用愉快!===="));
console.log(chalk.yellow("后续操作建议:\n"));
const regCode = generateRegCode();
console.log(chalk.green(`\t1.您的注册码为:${regCode} 请复制并用于激活。`));
console.log(chalk.yellow("\t2. 关闭【自动检查更新】功能,防止被覆盖。"));
console.log(
    chalk.yellow(
      "\t3. 关闭【Typora服务器使用国内服务器】功能,避免绕过联网验证失败。",
    ),
);
}

main();


这里增加食用说明:
1、安装主程序,然后在某个地方创建一个新文件夹,新建一个文件【temp.cjs】把上面的代码复制进去,
2、确认你的电脑有node,没有的去百度,
3、安装依赖:新建一个【package.json】文件,把下面的东西复制进去。复制保存完了后运行【npm install】
{
"dependencies": {
    "@electron/fuses": "^2.1.3",
    "asar": "^3.2.0",
    "chalk": "^4.1.2",
    "readline-sync": "^1.4.10",
    "winreg": "^1.2.5"
}
}

4、打开软件,点击【离线激活】这里会有一个【机器码】,复制下来,一会要用。软件不要退出
5、在文件管理器的地址栏输入【cmd】,然后输入【nodetemp.cjs】
6、按照提示,将复制的【机器码】输入进去,然后输入【邮箱地址】,然后输入【y】,【n】,然后等着就完了。

linyuuki 发表于 2026-1-4 07:37

很厉害很详细

iamjjt 发表于 2026-1-4 08:45

牛了,向大拿致敬

zhzhx 发表于 2026-1-4 09:06

有没有成品哦

snowfox 发表于 2026-1-4 09:09

感谢分享, 有思路有原理, {:1_921:}

花下野死鬼 发表于 2026-1-4 09:14

666,真是厉害啊

ydafu168 发表于 2026-1-4 09:17

什么样的水平才能看懂?

联盟少侠 发表于 2026-1-4 09:18

认真看完了,逆向就是一个熟悉的过程,对工具越熟悉真的就越快!大佬写的步骤和思路都非常清晰,真的是小白入门佳品!!

张向华 发表于 2026-1-4 09:20

技术牛人长见识了

飘浮 发表于 2026-1-4 09:24

写的不错 受教了。
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: Typora v1.12.4 安全分析:反反调试与激活劫持