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(`[👀IPC 请求] 收到 .invoke("${channel}") 参数:`, JSON.stringify(args));
try {
const result = await listener(event, ...args);
filter && writeLog(`[👀IPC 响应] .handle("${channel}") 返回结果:`, JSON.stringify(result));
return result;
} catch (error) {
filter && writeLog(`[👀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('【👀 监控】 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('【👀 监控】 Buffer get', String(p));
const result = Reflect.get(t, p, r);
// 如果结果为函数,二次监控其函数传参
if (typeof result == 'function') {
return new Proxy(result, {
apply(fn, thisArg, args) {
writeLog(`【👀 监控】 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
【👀 监控】 Buffer get toString
【👀 监控】 Buffer.toString apply args=["utf8"]
【👀 监控】 Buffer get length
[👀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(`【👀 JSON监控】 ${text.slice(0, 12)}..."} 被访问属性`, p);
return Reflect.get(t, p, r);
},
});
};
}
return result;
```
通过日志发现,我们终于命中了方法,并成功提取出了激活所需的关键字段。
```text
【👀 JSON监控】 {"test":"123..."} 被访问属性 deviceId
【👀 JSON监控】 {"test":"123..."} 被访问属性 fingerprint
【👀 JSON监控】 {"test":"123..."} 被访问属性 email
【👀 JSON监控】 {"test":"123..."} 被访问属性 license
【👀 JSON监控】 {"test":"123..."} 被访问属性 version
【👀 JSON监控】 {"test":"123..."} 被访问属性 date
【👀 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('【👀 监控】 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(`[👀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(`[👀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 应用安全分析思路。
旨在通过逆向分析的手段,挖掘到平时可能注意不到的安全漏洞、盲区,以便未来更好的正向开发。 ## 激活劫持的补充
关于仍然掉激活的情况,可以阅读 [#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`。 跟着文章的流程走了一遍最后成功了,之前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("[🛡️ 拦截] 程序试图调用 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" });
});
});
}
// 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(
\`[🛡️ 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);
};
});
// 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(
\`[👀IPC 请求] 收到 .invoke("\${channel}") 参数:\`,
JSON.stringify(args)
);
try {
const result = await listener(event, ...args);
filter &&
writeLog(
\`[👀IPC 响应] .handle("\${channel}") 返回结果:\`,
JSON.stringify(result)
);
return result;
} catch (error) {
filter && writeLog(\`[👀IPC 错误] .handle("\${channel}") 执行出错:\`, error);
throw error;
}
});
};
}
const crypto = require("crypto");
const originalPublicDecrypt = crypto.publicDecrypt;
crypto.publicDecrypt = function (key, buffer) {
if (HookDebug) {
writeLog("-------------------------------------------");
writeLog("【👀 监控】 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(\`[👀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(\`[🛡️ 拦截] 伪造激活验证响应: {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(\`[👀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(); 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(\`[👀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(\`[👀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-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-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
【有经济实力的自然是购买蒸蚌更香】
Cleopatra 发表于 2026-1-5 08:55
真的强,想问一下反调试这个花了多少的时间才能推导出来
这篇文章断断续续总共写了3天吧
反反调试/拦截分析,都是通用的原生JS技术,说难不难、说简单也不简单,至少要非常熟悉正向开发,才能知道去哪里埋点
Typora逆向最核心的地方在于jsc字节码,所有核心逻辑都写在这里面,我尝试过好久的反编译jsc结果连静态字符串都看不到……
所以无奈只能从JS层面去做黑盒调试,一点一点去猜出来他的逻辑
我凌晨的时候测试,同样的代码,无论多少次重新启动都不会掉激活
现在重新打开又会掉激活了,怀疑可能是激活码有时效性?
经测试:
第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替换并成功激活后,可以尝试修改下注册表中的激活日期
至于上述推测是否正确,可能要明天才能再次测试了 不错,下载个1.12.4的试试。跑去官网下载的注意了,现在最新的是1.12.6的了,提示是修复了激活问题,估计是修复了这个的 依据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("[🛡️ 拦截] 程序试图调用 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" });
});
});
}
// 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(
\`[🛡️ 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);
};
});
// 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(
\`[👀IPC 请求] 收到 .invoke("\${channel}") 参数:\`,
JSON.stringify(args)
);
try {
const result = await listener(event, ...args);
filter &&
writeLog(
\`[👀IPC 响应] .handle("\${channel}") 返回结果:\`,
JSON.stringify(result)
);
return result;
} catch (error) {
filter && writeLog(\`[👀IPC 错误] .handle("\${channel}") 执行出错:\`, error);
throw error;
}
});
};
}
const crypto = require("crypto");
const originalPublicDecrypt = crypto.publicDecrypt;
crypto.publicDecrypt = function (key, buffer) {
if (HookDebug) {
writeLog("-------------------------------------------");
writeLog("【👀 监控】 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(\`[👀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(\`[🛡️ 拦截] 伪造激活验证响应: {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(\`[👀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】,然后等着就完了。
很厉害很详细 牛了,向大拿致敬 有没有成品哦
感谢分享, 有思路有原理, {:1_921:} 666,真是厉害啊 什么样的水平才能看懂? 认真看完了,逆向就是一个熟悉的过程,对工具越熟悉真的就越快!大佬写的步骤和思路都非常清晰,真的是小白入门佳品!! 技术牛人长见识了 写的不错 受教了。