Typora v1.13.7 离线激活破解

前言

由于obsidian不能像typora那样随时随地的打开markdown文件,下载网上别人破解工具我又怕人家留后门,所以就学习了这篇 52破解论坛的文章,将最新版(v1.13.7)破解了一下。目前破解方法只限于windows

ps:自从搞了安全,用啥软件都疑神疑鬼。

Typora v1.13.7分析

这里目前安装的是Typora官网最新版v1.13.7,因此也就对这个版本进行分析。

安装包信息

项目 信息
文件名 typora-setup-x64.exe
版本 1.13.7.0
大小 ~93.7 MB
公司 typora.io
描述 Typora Setup (Inno Setup)
SHA256 04dc5d0ec1ddae9ab1d405be578c2d486e48cca9295029f79d532db80032ab40

安装效果预览

image

分析

【吾爱破解】Typora v1.12.4 安全分析:反反调试与激活劫持

通过吾爱破解论坛上Typora v1.12.4 安全分析:反反调试与激活劫持的文章可知v1.12.4版本存在Fuse 加载限制(反调试)完整性校验renew联网验证等加固手段。因此就来看看v1.13.7v1.12.4有什么区别。

app.asar分析

先安装asar

1
npm i -g asar

解压asar并备份资源文件

1
2
3
4
cd D:\software\Typora\resources\
asar extract app.asar app
robocopy app app.bak /E
rename app.asar app.asar.bak

app目录里面,launch.dist.js 只是字节码加载器,核心逻辑都在 V8 字节码文件 atom.compiled.dist.jsc 里,源码不可直接阅读,硬啃需要从内存 dump 再反编译,工程量很大。并且V8 字节码反编译得到的是类似汇编的 V8 指令(Bytecode Array / Ignition 指令集),不是原始的 JavaScript 源码,变量名、函数名、注释全丢,所以可读性极差。。

1
2
3
4
5
6
7
8
// 注册 .jsc 扩展名
Module._extensions[".jsc"] = function(module, filename) {
let bytecode = fs.readFileSync(filename);
// ...处理 cachedData...
let script = new vm.Script("", { cachedData: bytecode });
// ...
};
require("./atom.compiled.dist.jsc"); // 真正的代码在这里

[steven026]大佬讲解的很清晰,”.jsc的本质还是一个Node模块。这意味着我们可以通过 Hook(劫持)Node.js 或 Electron 的底层 API,间接分析、调试并修改其行为逻辑“。

绕过Fuse 加载限制(反反调试)

先验证一下 Fuse 状态,执行以下命令:

1
npx @electron/fuses read --app "D:\software\Typora\Typora.exe"

输出结果如下

1
2
3
4
5
6
7
8
9
10
Analyzing app: Typora.exe
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

image

配置项 OnlyLoadAppFromAsar is Enabled 限制了程序只能从 app.asar 启动。同Typora v1.12.4 安全分析:反反调试与激活劫持一致。因此这里也需要修改加载配置、使 Electron 恢复默认的文件加载策略(优先加载 app 文件夹)。

运行下面这段js代码,node test_fuses.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { flipFuses, FuseV1Options, FuseVersion } = require("@electron/fuses");
const fs = require("fs");

const fullPath = "D:\\software\\Typora\\Typora.exe";

async function main() {
// 修改前先备份
fs.copyFileSync(fullPath, fullPath + ".bak");
// 修改fuse配置(同时会修改程序hash)
await flipFuses(fullPath, {
version: FuseVersion.V1,
[FuseV1Options.OnlyLoadAppFromAsar]: false,
});
console.log("done. OnlyLoadAppFromAsar → false");
}
main();

脚本运行完后,再次查看配置,OnlyLoadAppFromAsar配置项的值已经被设置成了Disabled

image

此时尝试让Typora.exe通过app目录去启动程序

然后通过下面的js代码去执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const { flipFuses, FuseV1Options, FuseVersion } = require("@electron/fuses");
const asar = require("asar");
const fs = require("fs");
const path = require("path");

const TYPORA_DIR = "D:\\software\\Typora";
const RESOURCES_DIR = path.join(TYPORA_DIR, "resources");
const EXE_PATH = path.join(TYPORA_DIR, "Typora.exe");
const ASAR_PATH = path.join(RESOURCES_DIR, "app.asar");
const APP_DIR = path.join(RESOURCES_DIR, "app");
const BAK_DIR = path.join(RESOURCES_DIR, "app.bak");

async function main() {
// 1. 解压 asar
console.log("解压 asar...");
asar.extractAll(ASAR_PATH, APP_DIR);

// 2. 复制原始文件副本(完整性校验重定向要用)
console.log("复制 app -> app.bak...");
fs.cpSync(APP_DIR, BAK_DIR, { recursive: true });

// 3. 删除 asar
console.log("移除 app.asar...");
fs.renameSync(ASAR_PATH, ASAR_PATH + ".bak");

// 4. 改 Fuse 配置
console.log("修改 Fuse...");
await flipFuses(EXE_PATH, {
version: FuseVersion.V1,
[FuseV1Options.OnlyLoadAppFromAsar]: false,
});

console.log("done. 现在可以双击 Typora.exe 从 app/ 目录启动了");
}

main();

image

此时直接运行,Typora.exe不闪退。

然后在launch.dist.js里面添加一段测试代码,判断是否有从app目录启动。

1
console.log("hook launch.dist.js");

image

启动Typora.exe,控制台输出hook launch.dist.js,说明Typora.exe已经开始从app目录启动。但是由于存在完整性校验,导致exe直接闪退。

image

完整性校验

通过Hook 所有 fs 读取,记录每条经过 resources/app/ 的路径,就能抓到完整校验的文件清单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const fs = require("fs");
const path = require("path");

const APP_DIR = "D:/software/Typora/resources/app";
const LAUNCH = path.join(APP_DIR, "launch.dist.js");
const APP_BAK = "D:/software/Typora/resources/app.bak";

// 改的是 app/ 里的 launch.dist.js,app.bak/ 保持原始(校验时会对比它失败)
const original = fs.readFileSync(LAUNCH, "utf-8"); // 当前可能已经被改过,没关系

const hook = `
/** 纯探针——只记录,不重定向 **/
var _f=require("fs"),_p="${"D:\\\\software\\\\Typora\\\\files_checked.log".replace(/\\/g,"\\\\")}";
try{_f.rmSync(_p,{force:!0})}catch(e){}
function L(s){try{_f.appendFileSync(_p,"["+new Date().toISOString()+"] "+s+"\\n")}catch(e){}}
L("========== 纯探针已就绪 ==========");

var R=/resources[\\\\/]app[\\\\/]/i;

// Hook fs,只记录不拦截
["readFileSync","readFile","statSync","stat","open","openSync",
"existsSync","exists","lstatSync","lstat","readdirSync","readdir",
"accessSync","access","realpathSync","realpath"].forEach(function(k){
if(typeof _f[k]==="function"){
var orig=_f[k];
_f[k]=function(fp){
if(typeof fp==="string"&&R.test(fp)) L("[fs:"+k+"] "+fp);
return orig.apply(this,arguments);
};
}
});
if(_f.promises){
["readFile","open","stat","access","lstat","readdir","realpath"].forEach(function(k){
if(typeof _f.promises[k]==="function"){
var origP=_f.promises[k];
_f.promises[k]=function(fp){
if(typeof fp==="string"&&R.test(fp)) L("[fs.promises:"+k+"] "+fp);
return origP.apply(this,arguments);
};
}
});
}
L("探针注入完毕,等待校验触发...");
`;

fs.writeFileSync(LAUNCH, hook + original, "utf-8");
console.log("纯探针已注入(无重定向)。");
console.log("启动 Typora.exe 后校验会失败闪退,但日志已写出:");
console.log(" type D:\\software\\Typora\\files_checked.log");

运行上面的js代码后,再运行Typora.exe,然后查看日志

1
2
3
4
5
6
7
8
[2026-06-26T13:33:06.416Z] ========== 开始记录被校验的文件 ==========
[2026-06-26T13:33:06.421Z] 探针已就绪,等待 Typora 启动...
[2026-06-26T13:33:06.423Z] [fs:realpathSync] D:\software\Typora\resources\app\atom.compiled.dist.jsc
[2026-06-26T13:33:06.424Z] [fs:readFileSync] D:\software\Typora\resources\app\atom.compiled.dist.jsc
[2026-06-26T13:33:07.549Z] [promises:fs:readFile] D:\software\Typora\resources\app/package.json
[2026-06-26T13:33:07.551Z] [promises:fs:readFile] D:\software\Typora\resources\app/launch.dist.js
[2026-06-26T13:33:07.552Z] [promises:fs:readFile] D:\software\Typora\resources\app/../page-dist/license.html
[2026-06-26T13:33:07.553Z] [promises:fs:readFile] D:\software\Typora\resources\app/../page-dist/static/js/LicenseIndex.180dd4c7.5b58fa97.js

通过分析可知, v1.13.7 版本和 v1.12.4 版本一样,也是通过校验这些文件的 hash 来判断是否被篡改。因此只需要劫持fs模块(覆盖 readFilestatSyncopen等方法),在读取 resources\app/ 路径下的文件时,将其重定向到 resources\app.bak/ 下。这样即使 app目录下的文件已被篡改,校验时实际读取的仍是 app.bak中的原始文件,从而通过完整性校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const fs = require("fs");
const path = require("path");

var typoraPath = "D:/software/Typora";
if (!fs.existsSync(path.join(typoraPath, "Typora.exe"))) {
console.error("未找到 Typora.exe: " + typoraPath);
process.exit(1);
}

var appDir = path.join(typoraPath, "resources/app");
var bakDir = path.join(typoraPath, "resources/app.bak");
var launchJs = path.join(appDir, "launch.dist.js");

if (!fs.existsSync(bakDir)) {
console.error("app.bak/ 不存在,请先执行:");
console.error(" asar extract app.asar app/");
console.error(" 复制 app/ → app.bak/");
console.error(" flipFuses Typora.exe OnlyLoadAppFromAsar→false");
process.exit(1);
}

var original = fs.readFileSync(path.join(bakDir, "launch.dist.js"), "utf-8");

var hook = [
'/** fs 重定向:app/ → app.bak/ (绕过完整性校验) **/',
'var _f=require("fs");',
'var _r=/resources[\\\\/]app[\\\\/]/i,_t="resources\\\\app.bak\\\\";',
'["readFileSync","readFile","statSync","stat","open","openSync",',
' "existsSync","exists","lstatSync","lstat","readdirSync","readdir",',
' "accessSync","access","realpathSync","realpath"].forEach(function(k){',
' if(typeof _f[k]==="function"){var o=_f[k];_f[k]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
'});',
'if(_f.promises){["readFile","open","stat","access","lstat","readdir","realpath"].forEach(function(k){',
' if(typeof _f.promises[k]==="function"){var o=_f.promises[k];_f.promises[k]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
'})}',
'/** End **/',
'',
].join("");

fs.writeFileSync(launchJs, hook + original, "utf-8");
console.log("已注入 fs 重定向: " + launchJs);
console.log("大小: " + fs.statSync(launchJs).size + " 字节");
console.log("现在可以双击 Typora.exe,校验会被绕过。");

此时双击,不会闪退了。

image

开启devtools,前端激活分析

开启Typora的开启devtools,可以看到激活流程的相关逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// f12_min.js
const fs = require("fs");
const path = require("path");
const asar = require("asar");
const { flipFuses, FuseV1Options, FuseVersion } = require("@electron/fuses");

const TYPORA = "D:/software/Typora";
const RESOURCES = path.join(TYPORA, "resources");
const ASAR_PATH = path.join(RESOURCES, "app.asar");
const APP_DIR = path.join(RESOURCES, "app");
const APP_BAK = path.join(RESOURCES, "app.bak");
const EXE_PATH = path.join(TYPORA, "Typora.exe");
const LAUNCH = path.join(APP_DIR, "launch.dist.js");

async function main() {
// ======== 初始化 ========
console.log("=== 初始化 ===");
if (!fs.existsSync(EXE_PATH + ".bak")) { fs.copyFileSync(EXE_PATH, EXE_PATH + ".bak"); console.log("exe .bak"); }
if (!fs.existsSync(ASAR_PATH + ".bak") && fs.existsSync(ASAR_PATH)) { fs.copyFileSync(ASAR_PATH, ASAR_PATH + ".bak"); console.log("asar .bak"); }
if (fs.existsSync(ASAR_PATH)) {
try { fs.rmSync(APP_DIR, { recursive: true, force: true }); } catch(e) {}
asar.extractAll(ASAR_PATH, APP_DIR);
console.log("asar → app/");
}
if (!fs.existsSync(APP_BAK)) {
(function copyDir(s, d) {
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
var entries = fs.readdirSync(s, { withFileTypes: true });
for (var i = 0; i < entries.length; i++) {
var sp = path.join(s, entries[i].name), dp = path.join(d, entries[i].name);
entries[i].isDirectory() ? copyDir(sp, dp) : fs.copyFileSync(sp, dp);
}
})(APP_DIR, APP_BAK);
console.log("app/ → app.bak/");
}
if (fs.existsSync(ASAR_PATH)) { fs.rmSync(ASAR_PATH, { force: true }); console.log("asar 已删除"); }
await flipFuses(EXE_PATH, { version: FuseVersion.V1, [FuseV1Options.OnlyLoadAppFromAsar]: false });
console.log("Fuse OK");

// ======== 注入 ========
var original = fs.readFileSync(path.join(APP_BAK, "launch.dist.js"), "utf-8");
var hook = [
'/** F12 MIN **/',
'var _f=require("fs");',
'var _p="D:/software/Typora/f12.log";',
'try{_f.rmSync(_p,{force:!0})}catch(e){}',
'function W(){try{_f.appendFileSync(_p,Array.prototype.slice.call(arguments).join(" ")+"\\n")}catch(e){}}',
'W("HOOK START");',
'var _e;',
'try{_e=require("electron");W("electron loaded")}catch(e){W("electron FAILED:",e.message)}',
'try{Object.defineProperty(_e.app,"quit",{value:function(){W("[BLOCKED] app.quit")},writable:!0,configurable:!0});W("app.quit hooked")}catch(e){W("app.quit FAILED:",e.message)}',
'/* fs 重定向 */try{',
'var _r=/resources[\\\\/]app[\\\\/]/i;',
'var _t="resources/app.bak/";',
'[_f].forEach(function(fsMod){',
' ["readFileSync","readFile","statSync","stat","open","openSync","existsSync","exists","lstatSync","lstat","readdirSync","readdir","accessSync","access"].forEach(function(p){',
' if(typeof fsMod[p]==="function"){var o=fsMod[p];fsMod[p]=function(fp){if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);return o.apply(this,arguments)}}',
' });',
' if(fsMod.promises){["readFile","open","stat","access","lstat","readdir"].forEach(function(p){',
' if(typeof fsMod.promises[p]==="function"){var o=fsMod.promises[p];fsMod.promises[p]=function(fp){if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);return o.apply(this,arguments)}}',
' })}',
'});',
'W("fs redirect installed");',
'}catch(e){W("fs redirect FAILED:",e.message)}',
'',
'/* 开 F12 */',
'_e.app.on("browser-window-created",function(_ev,win){win.webContents.once("dom-ready",function(){win.webContents.openDevTools({mode:"detach"})})});',
'W("F12 hook installed, HOOK DONE");',
'',
'/** End **/',
''
].join("");

fs.writeFileSync(LAUNCH, hook + original, "utf-8");
console.log("F12 已注入, 大小:", fs.statSync(LAUNCH).size);
console.log("双击 Typora.exe 启动,F12 自动打开。");
}
main().catch(e => console.error(e));

image

case 0 校验激活码是否是+开始和#结束;

case 11通过window.Setting.invokeWithCallback("offlineActivation", t);发起IPC请求。

case 14的逻辑里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
l = e.sent;          // IPC 返回值
c = Object(f.a)(l, 4); // 解构 4 个值
s = c[0]; // success (bool)
d = c[1]; // email
p = c[2]; // license
h = c[3]; // date

if (s) {
Y(d); // 设置 email
_(!0); // 标记已激活
S(0); // 切换页面状态
M(p); // 设置 license
U(h); // 设置 date
Q("off");
} else {
window.alert("Invalid Activation Token"); // 激活失败
}

通过case 11和case 14,也就知道为IPC劫持的核心逻辑了。

1
2
3
4
5
6
7
let _h = window.Setting.invokeWithCallback;
window.Setting.invokeWithCallback = function(ch) {
if (ch === "offlineActivation") {
return Promise.resolve([true, "1/1/2099", "admin@localhost", "FAKE-LICENSE"]);
}
return _h.apply(this, arguments);
};

image

image

离线激活

hook Crypto模块publicDecrypt方法

Typora里面是有一个离线激活的。

image

其中Machine Code机器码是base64格式的字符串

1
eyJ2Ijoid2lufDEuMTMuNyIsImkiOiJRcktFSGtNY1h6IiwibCI6IkRFU0tUT1AtWFhYWFhYWCB8IEFETUlOIHwgV2luZG93cyJ9
1
2
3
4
5
{
"v": "win|1.13.7",
"i": "QrKEHkMcXz",
"l": "DESKTOP-XXXXXXX | ADMIN | Windows"
}

理论上,这里的核心逻辑应该没变,还是通过crypto 模块publicDecrypt 方法实现的公钥解密。原因是、像签名、授权这类的为了防伪造,都是通过rsa算法,因为rsa的私钥不知道的话,就没办法伪造密文。

所以这里还是的通过尝试hook crypto 模块publicDecrypt方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// test_passive.js —— 被动侦察模式:只记录不拦截
// 观察 crypto.publicDecrypt 和 IPC 的真实调用,不修改任何数据
const fs = require("fs");
const path = require("path");

const TYPORA = "D:/software/Typora";
const LAUNCH = path.join(TYPORA, "resources/app/launch.dist.js");
const APP_BAK = path.join(TYPORA, "resources/app.bak");

var original = fs.readFileSync(path.join(APP_BAK, "launch.dist.js"), "utf-8");

var hook = [
'/** PASSIVE MODE —— 只记录,不拦截 **/',
'var _f=require("fs"),_p="D:/software/Typora/passive_hook.log";',
'try{_f.rmSync(_p,{force:!0})}catch(e){}',
'function L(){var a=arguments;try{_f.appendFileSync(_p,"["+new Date().toISOString()+"] "+Array.prototype.slice.call(a).join(" ")+"\\n")}catch(e){}}',
'L("========== PASSIVE MODE ==========");',
'',
'/* -- fs 重定向(绕过完整性检查)-- */',
'var _r=/resources[\\\\/]app[\\\\/]/i,_t="resources\\\\app.bak\\\\";',
'["readFileSync","readFile","statSync","stat","open","openSync",',
' "existsSync","exists","lstatSync","lstat","readdirSync","readdir",',
' "accessSync","access","realpathSync","realpath"].forEach(function(k){',
' if(typeof _f[k]==="function"){var o=_f[k];_f[k]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
'});',
'if(_f.promises){["readFile","open","stat","access","lstat","readdir","realpath"].forEach(function(k){',
' if(typeof _f.promises[k]==="function"){var o=_f.promises[k];_f.promises[k]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
'})}',
'L("[HOOK] fs 重定向已安装");',
'',
'/* -- 被动记录 crypto.publicDecrypt(不拦截返回值!)-- */',
'var _c=require("crypto");',
'["publicDecrypt","privateDecrypt","publicEncrypt","privateEncrypt","sign","verify"].forEach(function(k){',
' if(typeof _c[k]!=="function")return;',
' var o=_c[k];_c[k]=function(){',
' L("[crypto:"+k+"]");',
' L("[crypto:"+k+"] key:",typeof arguments[0],arguments[0]&&String(arguments[0]).substring(0,300));',
' L("[crypto:"+k+"] buf:",arguments[1]?"len="+arguments[1].length+" hex="+Buffer.from(arguments[1]).toString("hex").substring(0,200):"N/A");',
' var r=o.apply(this,arguments);',
' if(r&&r.toString)try{L("[crypto:"+k+"] RESULT:",r.toString("utf-8").substring(0,500))}catch(e){}',
' return r',
' }',
'});',
'L("[HOOK] crypto 被动记录已安装");',
'',
'/* -- IPC 全量日志(注册+调用+响应)-- */',
'var _e=require("electron"),_oh=_e.ipcMain.handle;',
'_e.ipcMain.handle=function(ch,listener){',
' L("[IPC:reg] "+ch);',
' return _oh.call(this,ch,async function(){',
' L("[IPC:call] "+ch,JSON.stringify(Array.prototype.slice.call(arguments,1)).substring(0,500));',
' var r=await listener.apply(this,arguments);',
' L("[IPC:resp] "+ch,JSON.stringify(r).substring(0,500));',
' return r',
' })',
'};',
'L("[HOOK] IPC 日志已安装");',
'',
'L("========== PASSIVE MODE READY ==========");',
'/** End **/',
'',
].join("");

fs.writeFileSync(LAUNCH, hook + original, "utf-8");
console.log("passive 探针已注入, 大小:", fs.statSync(LAUNCH).size);
console.log("启动 Typora.exe 后查看日志:");
console.log(" type D:\\software\\Typora\\passive_hook.log");

然后在离线激活位置输入+123456789#

触发日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
[2026-06-26T15:03:25.661Z] ========== PASSIVE MODE ==========
[2026-06-26T15:03:25.663Z] [HOOK] fs 重定向已安装
[2026-06-26T15:03:25.670Z] [HOOK] crypto 被动记录已安装
[2026-06-26T15:03:25.670Z] [HOOK] IPC 日志已安装
[2026-06-26T15:03:25.671Z] ========== PASSIVE MODE READY ==========
[2026-06-26T15:03:25.678Z] [IPC:reg] env.info
[2026-06-26T15:03:25.765Z] [IPC:reg] window.setTitle
[2026-06-26T15:03:25.765Z] [IPC:reg] window.focus
[2026-06-26T15:03:25.766Z] [IPC:reg] window.setInSourceMode
[2026-06-26T15:03:25.766Z] [IPC:reg] window.minimize
[2026-06-26T15:03:25.766Z] [IPC:reg] window.close
[2026-06-26T15:03:25.766Z] [IPC:reg] window.restore
[2026-06-26T15:03:25.767Z] [IPC:reg] window.maximize
[2026-06-26T15:03:25.767Z] [IPC:reg] window.fullscreen
[2026-06-26T15:03:25.767Z] [IPC:reg] window.setMenuBarVisibility
[2026-06-26T15:03:25.768Z] [IPC:reg] window.pin
[2026-06-26T15:03:25.768Z] [IPC:reg] window.unpin
[2026-06-26T15:03:25.768Z] [IPC:reg] window.updateMenuForIsAlwaysOnTop
[2026-06-26T15:03:25.768Z] [IPC:reg] window.toggleDevTools
[2026-06-26T15:03:25.769Z] [IPC:reg] window.inspectElement
[2026-06-26T15:03:25.769Z] [IPC:reg] window.checkAsFocus
[2026-06-26T15:03:25.769Z] [IPC:reg] controller.switchFolder
[2026-06-26T15:03:25.769Z] [IPC:reg] webContents.copy
[2026-06-26T15:03:25.770Z] [IPC:reg] webContents.cut
[2026-06-26T15:03:25.770Z] [IPC:reg] webContents.paste
[2026-06-26T15:03:25.770Z] [IPC:reg] webContents.selectAll
[2026-06-26T15:03:25.770Z] [IPC:reg] webContents.undo
[2026-06-26T15:03:25.770Z] [IPC:reg] webContents.redo
[2026-06-26T15:03:25.771Z] [IPC:reg] webContents.clearCache
[2026-06-26T15:03:25.771Z] [IPC:reg] webContents.action
[2026-06-26T15:03:25.772Z] [IPC:reg] window.enterFullscreen
[2026-06-26T15:03:25.772Z] [IPC:reg] window.exitFullscreen
[2026-06-26T15:03:25.773Z] [IPC:reg] app.setZoom
[2026-06-26T15:03:25.796Z] [IPC:reg] license.machineCode
[2026-06-26T15:03:25.796Z] [IPC:reg] addLicense
[2026-06-26T15:03:25.797Z] [IPC:reg] offlineActivation
[2026-06-26T15:03:25.797Z] [IPC:reg] license.show
[2026-06-26T15:03:25.797Z] [IPC:reg] license.show.debug
[2026-06-26T15:03:25.797Z] [IPC:reg] removeLicense
[2026-06-26T15:03:25.798Z] [IPC:reg] menu.updateCustomZoom
[2026-06-26T15:03:25.798Z] [IPC:reg] menu.refreshThemeMenu
[2026-06-26T15:03:25.798Z] [IPC:reg] menu.reloadExportMenu
[2026-06-26T15:03:25.799Z] [IPC:reg] menu.popup
[2026-06-26T15:03:25.803Z] [IPC:reg] setting.loadExports
[2026-06-26T15:03:25.803Z] [IPC:reg] export.recordLastExport
[2026-06-26T15:03:25.804Z] [IPC:reg] setting.getDownloadingDicts
[2026-06-26T15:03:25.804Z] [IPC:reg] setting.getUserDictionaryPath
[2026-06-26T15:03:25.804Z] [IPC:reg] setting.getUserDict
[2026-06-26T15:03:25.804Z] [IPC:reg] setting.downloadDict
[2026-06-26T15:03:25.805Z] [IPC:reg] setting.getThemes
[2026-06-26T15:03:25.805Z] [IPC:reg] setting.setCurTheme
[2026-06-26T15:03:25.805Z] [IPC:reg] setting.resetAdvancedSettings
[2026-06-26T15:03:25.805Z] [IPC:reg] logger.error
[2026-06-26T15:03:25.806Z] [IPC:reg] logger.warn
[2026-06-26T15:03:25.806Z] [IPC:reg] setting.askForClearRecentDocuments
[2026-06-26T15:03:25.806Z] [IPC:reg] setting.removeRecentFolder
[2026-06-26T15:03:25.806Z] [IPC:reg] setting.removeRecentDocument
[2026-06-26T15:03:25.807Z] [IPC:reg] setting.addRecentFolder
[2026-06-26T15:03:25.807Z] [IPC:reg] setting.getRecentFiles
[2026-06-26T15:03:25.807Z] [IPC:reg] setting.updateRecentFile
[2026-06-26T15:03:25.807Z] [IPC:reg] setting.put
[2026-06-26T15:03:25.807Z] [IPC:reg] setting.get
[2026-06-26T15:03:25.808Z] [IPC:reg] setting.fetchAnalytics
[2026-06-26T15:03:25.808Z] [IPC:reg] setting.loadAll
[2026-06-26T15:03:25.808Z] [IPC:reg] setting.getUnsavedDraftsPath
[2026-06-26T15:03:25.808Z] [IPC:reg] setting.getExtraOption
[2026-06-26T15:03:25.809Z] [IPC:reg] setting.doDownloadPicgo
[2026-06-26T15:03:25.809Z] [IPC:reg] setting.getKeyBinding
[2026-06-26T15:03:25.809Z] [IPC:reg] shell.saveItem
[2026-06-26T15:03:25.809Z] [IPC:reg] shell.openItem
[2026-06-26T15:03:25.810Z] [IPC:reg] shell.trashItem
[2026-06-26T15:03:25.810Z] [IPC:reg] shell.openExternal
[2026-06-26T15:03:25.810Z] [IPC:reg] shell.showItemInFolder
[2026-06-26T15:03:25.810Z] [IPC:reg] shell.showDownload
[2026-06-26T15:03:25.811Z] [IPC:reg] dialog.showMessageBox
[2026-06-26T15:03:25.811Z] [IPC:reg] dialog.showSaveDialog
[2026-06-26T15:03:25.811Z] [IPC:reg] dialog.showOpenDialog
[2026-06-26T15:03:25.811Z] [IPC:reg] dialog.showOpenDialogAlone
[2026-06-26T15:03:25.812Z] [IPC:reg] export.genPrintView
[2026-06-26T15:03:25.812Z] [IPC:reg] export.destroyPrintView
[2026-06-26T15:03:25.812Z] [IPC:reg] export.getPageColor
[2026-06-26T15:03:25.812Z] [IPC:reg] export.print
[2026-06-26T15:03:25.812Z] [IPC:reg] export.printToPDF
[2026-06-26T15:03:25.813Z] [IPC:reg] export.prepareImageCapture
[2026-06-26T15:03:25.813Z] [IPC:reg] export.captureImage
[2026-06-26T15:03:25.813Z] [IPC:reg] url.request
[2026-06-26T15:03:25.813Z] [IPC:reg] page.screenshot
[2026-06-26T15:03:25.814Z] [IPC:reg] controller.screenshotHTML
[2026-06-26T15:03:25.814Z] [IPC:reg] clipboard.write
[2026-06-26T15:03:25.814Z] [IPC:reg] pandoc.import
[2026-06-26T15:03:25.814Z] [IPC:reg] pandoc.version
[2026-06-26T15:03:25.814Z] [IPC:reg] pandoc.setVersion
[2026-06-26T15:03:25.822Z] [IPC:reg] theme.apply
[2026-06-26T15:03:25.822Z] [IPC:reg] theme.setThemeSource
[2026-06-26T15:03:25.823Z] [IPC:reg] getPath
[2026-06-26T15:03:25.823Z] [IPC:reg] registry.addOpenInTypora
[2026-06-26T15:03:25.823Z] [IPC:reg] registry.removeOpenInTypora
[2026-06-26T15:03:25.823Z] [IPC:reg] registry.addNewMarkdown
[2026-06-26T15:03:25.824Z] [IPC:reg] registry.removeNewMarkdown
[2026-06-26T15:03:25.824Z] [IPC:reg] filesOp.registerUndo
[2026-06-26T15:03:25.824Z] [IPC:reg] filesOp.clearUndo
[2026-06-26T15:03:25.824Z] [IPC:reg] filesOp.performUndo
[2026-06-26T15:03:25.825Z] [IPC:reg] filesOp.getFocusPath
[2026-06-26T15:03:25.825Z] [IPC:reg] filesOp.undoLabel
[2026-06-26T15:03:25.851Z] [IPC:reg] document.switchToUntitled
[2026-06-26T15:03:25.851Z] [IPC:reg] document.setLastSync
[2026-06-26T15:03:25.852Z] [IPC:reg] document.syncFullContent
[2026-06-26T15:03:25.852Z] [IPC:reg] document.getContent
[2026-06-26T15:03:25.852Z] [IPC:reg] document.currentPath
[2026-06-26T15:03:25.852Z] [IPC:reg] document.loadData
[2026-06-26T15:03:25.853Z] [IPC:reg] document.switchDocument
[2026-06-26T15:03:25.853Z] [IPC:reg] document.rename
[2026-06-26T15:03:25.853Z] [IPC:reg] document.setContent
[2026-06-26T15:03:25.853Z] [IPC:reg] document.addSnap
[2026-06-26T15:03:25.854Z] [IPC:reg] document.addSnapAndLastSync
[2026-06-26T15:03:25.854Z] [IPC:reg] document.shouldSaveSnap
[2026-06-26T15:03:25.854Z] [IPC:reg] document.getSnap
[2026-06-26T15:03:25.854Z] [IPC:reg] document.getSnapWithValidation
[2026-06-26T15:03:25.855Z] [IPC:reg] document.enterOversize
[2026-06-26T15:03:25.855Z] [IPC:reg] document.newWindow
[2026-06-26T15:03:25.855Z] [IPC:reg] document.checkIfMoveOnSave
[2026-06-26T15:03:25.855Z] [IPC:reg] document.hasDuplicateName
[2026-06-26T15:03:25.856Z] [IPC:reg] document.noOtherWindow
[2026-06-26T15:03:25.878Z] [IPC:reg] app.openFile
[2026-06-26T15:03:25.878Z] [IPC:reg] app.openFileOrFolder
[2026-06-26T15:03:25.878Z] [IPC:reg] app.openOrSwitch
[2026-06-26T15:03:25.879Z] [IPC:reg] app.openFolder
[2026-06-26T15:03:25.879Z] [IPC:reg] app.onCloseWin
[2026-06-26T15:03:25.879Z] [IPC:reg] app.sendEvent
[2026-06-26T15:03:25.879Z] [IPC:reg] executeJavaScript
[2026-06-26T15:03:25.880Z] [IPC:reg] app.cancelQuit
[2026-06-26T15:03:25.880Z] [IPC:reg] quit
[2026-06-26T15:03:25.880Z] [IPC:reg] app.download
[2026-06-26T15:03:26.045Z] [IPC:reg] updater.checkForUpdates
[2026-06-26T15:03:26.045Z] [IPC:reg] updater.cancelUpdate
[2026-06-26T15:03:26.046Z] [IPC:reg] updater.skipUpdate
[2026-06-26T15:03:26.046Z] [IPC:reg] updater.downloadUpdate
[2026-06-26T15:03:26.920Z] [IPC:call] document.loadData []
[2026-06-26T15:03:26.921Z] [IPC:resp] document.loadData "{\"snap\":null,\"filePath\":null,\"backups\":null,\"shouldReadFromDisk\":false,\"windowCounts\":1}"
[2026-06-26T15:03:26.937Z] [IPC:call] setting.put ["sidebar_tab","outline"]
[2026-06-26T15:03:26.938Z] [IPC:call] window.updateMenuForIsAlwaysOnTop []
[2026-06-26T15:03:26.939Z] [IPC:call] menu.updateCustomZoom []
[2026-06-26T15:03:26.974Z] [IPC:call] window.updateMenuForIsAlwaysOnTop []
[2026-06-26T15:03:26.975Z] [IPC:call] menu.updateCustomZoom []
[2026-06-26T15:03:26.977Z] [IPC:call] setting.getRecentFiles []
[2026-06-26T15:03:26.977Z] [IPC:resp] setting.getRecentFiles {"files":[],"folders":[]}
[2026-06-26T15:03:26.978Z] [IPC:call] window.updateMenuForIsAlwaysOnTop []
[2026-06-26T15:03:26.979Z] [IPC:call] menu.updateCustomZoom []
[2026-06-26T15:03:27.011Z] [IPC:call] document.setLastSync [null]
[2026-06-26T15:03:27.013Z] [IPC:call] window.updateMenuForIsAlwaysOnTop []
[2026-06-26T15:03:27.013Z] [IPC:call] menu.updateCustomZoom []
[2026-06-26T15:03:27.125Z] [IPC:call] window.updateMenuForIsAlwaysOnTop []
[2026-06-26T15:03:27.126Z] [IPC:call] menu.updateCustomZoom []
[2026-06-26T15:03:27.127Z] [IPC:call] window.checkAsFocus []
[2026-06-26T15:03:27.147Z] [IPC:call] document.getSnapWithValidation [null]
[2026-06-26T15:03:27.148Z] [IPC:resp] document.getSnapWithValidation "{\"snap\":null,\"shouldReadFromDisk\":false}"
[2026-06-26T15:03:27.151Z] [IPC:call] theme.setThemeSource ["system"]
[2026-06-26T15:03:27.151Z] [IPC:call] setting.put ["backgroundColor","#FFFFFF"]
[2026-06-26T15:03:27.152Z] [IPC:call] setting.put ["isDarkMode",false]
[2026-06-26T15:03:27.153Z] [IPC:call] window.updateMenuForIsAlwaysOnTop []
[2026-06-26T15:03:27.154Z] [IPC:call] menu.updateCustomZoom []
[2026-06-26T15:03:27.156Z] [IPC:call] window.checkAsFocus []
[2026-06-26T15:03:27.157Z] [IPC:call] setting.getDownloadingDicts []
[2026-06-26T15:03:27.158Z] [IPC:resp] setting.getDownloadingDicts "[]"
[2026-06-26T15:03:27.158Z] [IPC:call] setting.getUserDictionaryPath []
[2026-06-26T15:03:27.159Z] [IPC:resp] setting.getUserDictionaryPath "C:\\Users\\admin\\AppData\\Roaming\\Typora\\typora-dictionaries"
[2026-06-26T15:03:27.160Z] [IPC:call] setting.getUserDict []
[2026-06-26T15:03:27.160Z] [IPC:resp] setting.getUserDict "{}"
[2026-06-26T15:03:27.196Z] [IPC:call] window.updateMenuForIsAlwaysOnTop []
[2026-06-26T15:03:27.197Z] [IPC:call] menu.updateCustomZoom []
[2026-06-26T15:03:27.198Z] [IPC:call] window.checkAsFocus []
[2026-06-26T15:03:27.482Z] [IPC:call] document.setContent [""]
[2026-06-26T15:03:27.483Z] [IPC:resp] document.setContent false
[2026-06-26T15:03:27.484Z] [IPC:call] document.addSnapAndLastSync ["{\"snap\":{\"nodeMap\":\"{\\\"blocks\\\":[{\\\"content\\\":{\\\"text\\\":\\\"\\\",\\\"type\\\":\\\"paragraph\\\"},\\\"relation\\\":{\\\"before_id\\\":null,\\\"after_id\\\":null,\\\"parent_id\\\":null},\\\"children\\\":[],\\\"is_top\\\":true,\\\"id\\\":\\\"n0\\\"}],\\\"nid\\\":2}\",\"undoRedo\":\"{\\\"index\\\":-1,\\\"commandStackJson\\\":\\\"[]\\\"}\",\"timeStamp\":1782486207002,\"cursorPos\":\"{\\\"type\\\":\\\"cursor\\\",\\\"id\\\":\\\"n0\\\",\\\"start\\\":0,\\\"end\\\":0}\",\"scrollOffset\"
[2026-06-26T15:03:27.484Z] [IPC:resp] document.addSnapAndLastSync true
[2026-06-26T15:03:28.019Z] [IPC:call] setting.getKeyBinding []
[2026-06-26T15:03:28.019Z] [IPC:resp] setting.getKeyBinding {"_useLowerCase":true}
[2026-06-26T15:03:40.380Z] [IPC:call] license.machineCode []
[2026-06-26T15:03:40.382Z] [IPC:resp] license.machineCode "eyJ2Ijoid2lufDEuMTMuNyIsImkiOiJRcktFSGtNY1h6IiwibCI6IkRFU0tUT1AtWFhYWFhYWCB8IEFETUlOIHwgV2luZG93cyJ9"
[2026-06-26T15:03:44.520Z] [IPC:call] offlineActivation ["12345678"]
[2026-06-26T15:03:44.521Z] [crypto:publicDecrypt]
[2026-06-26T15:03:44.521Z] [crypto:publicDecrypt] key: string -----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nVoGCHqIMJyqgALEUrc
5JJhap0+HtJqzPE04pz4y+nrOmY7/12f3HvZyyoRsxKdXTZbO0wEHFIh0cRqsuaJ
PyaOOPbA0BsalofIAY3mRhQQ3vSf+rn3g+w0S+udWmKV9DnmJlpWqizFajU4T/E4
5ZgMNcXt3E1ips32rdbTR0Nnen9PVITvrbJ3l6CI2BFBImZQZ2P8N+LsqfJsqyVV
wDkt3mHAVxV7F
[2026-06-26T15:03:44.521Z] [crypto:publicDecrypt] buf: len=6 hex=d76df8e7aefc
[2026-06-26T15:03:44.523Z] [IPC:resp] offlineActivation [false,"Please input a valid license code"]

image

从这段日志读出:

  1. 激活码去壳+12345678# → 前端剥掉 +# → IPC 收到 "12345678"(8 字符)
  2. 编码转换:解密前变成 6 字节 d76df8e7aefc
  3. RSA 2048 公钥硬编码在字节码里
  4. 非法激活码 → publicDecryptDATA_LEN_NOT_EQUAL_TO_MOD_LEN → 返回 [false, ...]

离线激活劫持

首先需要伪造字段,用 Proxy 构造一个Object——程序问什么字段都说有、读什么都给假值,同时记录字段名,从而探测出程序期望的全部字段:先劫持 publicDecrypt 返回空对象 {}Buffer 并绕过 RSA 解密,再劫持 JSON.parseProxyhas trap 对任意字段返回 trueget trap 对任意读取返回 dummy 并记录字段名,程序逐个检查字段时就会自己暴露出 deviceIdfingerprintemaillicenseversiondatetype 全部 7 个字段。

和原文的思路差不多,但是v1.13.7v1.12.4多了一个先检查字段是否存在然后再读取,所以用原文里的代码也就读取不到值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// test_spy.js —— 字段监听模式
// 只保留:fs 重定向 + JSON.parse/publicDecrypt 字段访问监听
const fs = require("fs");
const path = require("path");

const RESOURCES = "D:/software/Typora/resources";
const APP_DIR = path.join(RESOURCES, "app");
const APP_BAK_DIR = path.join(RESOURCES, "app.bak");
const LAUNCH_JS = path.join(APP_DIR, "launch.dist.js");

var original = fs.readFileSync(path.join(APP_BAK_DIR, "launch.dist.js"), "utf-8");

var hook = [
'/** ===== SPY HOOK ===== */',
'var _f=require("fs"),_p="D:/software/Typora/spy.log";',
'try{_f.rmSync(_p,{force:!0})}catch(e){}',
'function W(){var a=arguments;try{_f.appendFileSync(_p,"["+new Date().toISOString()+"] "+Array.prototype.slice.call(a).join(" ")+"\\n")}catch(e){}}',
'',
'/* -- fs 重定向(绕过完整性检查)-- */',
'var _r=/resources[\\\\/]app[\\\\/]/i,_t="resources\\\\app.bak\\\\";',
'["readFileSync","readFile","statSync","stat","open","openSync",',
' "existsSync","exists","lstatSync","lstat","readdirSync","readdir",',
' "accessSync","access","realpathSync","realpath"].forEach(function(k){',
' if(typeof _f[k]==="function"){var o=_f[k];_f[k]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
'});',
'if(_f.promises){["readFile","open","stat","access","lstat","readdir","realpath"].forEach(function(k){',
' if(typeof _f.promises[k]==="function"){var o=_f.promises[k];_f.promises[k]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
'})}',
'W("[HOOK] fs redirect installed");',
'',
'/* -- JSON.parse 字段监听(通配探测,不预知字段名)-- */',
'var _origParse=JSON.parse,_spyActive=false;',
'JSON.parse=function(){',
' var obj=_origParse.apply(this,arguments);',
' if(_spyActive&&typeof obj==="object"&&obj&&!Array.isArray(obj)){',
' _spyActive=false;',
' W("[JSON:SPY] parse result keys:",Object.keys(obj).join(","));',
' return new Proxy(obj,{',
' has:function(t,p){',
' if(typeof p==="string")W("[JSON:SPY:has] "+p);',
' return typeof p==="string"?true:Reflect.has(t,p)',
' },',
' get:function(t,p,r){',
' if(typeof p==="string"&&p!=="toJSON"&&p!=="constructor"&&p!=="toString"&&p!=="valueOf"&&p!==Symbol.toPrimitive&&p!==Symbol.iterator){',
' var val=Reflect.get(t,p,r);',
' if(val===undefined)val="dummy";',
' W("[JSON:SPY:get] "+String(p)+" = "+(typeof val==="string"?val:JSON.stringify(val).substring(0,100)));',
' return val',
' }',
' return Reflect.get(t,p,r)',
' }',
' })',
' }',
' return obj',
'};',
'W("[HOOK] JSON.parse spy installed");',
'',
'/* -- publicDecrypt 主动伪造 + 字段监听 -- */',
'var _c=require("crypto");',
'_c.publicDecrypt=function(){',
' W("[CRYPTO:publicDecrypt] CALLED");',
' W("[CRYPTO:publicDecrypt] buf:",arguments[1]?"len="+arguments[1].length:"N/A");',
' var buf=Buffer.from("{}");',
' W("[CRYPTO:publicDecrypt] RETURNING: {}");',
' _spyActive=true;',
' return new Proxy(buf,{',
' get:function(t,p,r){',
' var result=Reflect.get(t,p,r);',
' if(typeof result==="function"){',
' return new Proxy(result,{',
' apply:function(fn,thisArg,args){',
' W("[BUFFER:"+String(p)+"] called, args:",JSON.stringify(args).substring(0,200));',
' try{return Reflect.apply(fn,r,args)}catch(e){return Reflect.apply(fn,t,args)}',
' }',
' })',
' }',
' W("[BUFFER:get] "+String(p));',
' return result',
' }',
' })',
'};',
'W("[HOOK] publicDecrypt spy installed");',
'',
'W("===== SPY HOOK READY =====");',
'/** ===== End ===== */',
'',
].join("");

fs.writeFileSync(LAUNCH_JS, hook + original, "utf-8");
console.log("Spy hook injected, size:", fs.statSync(LAUNCH_JS).size);
console.log("Log: type D:\\software\\Typora\\spy.log");

image

激活分析

通过proxy获得了json7个的属性字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// test_hook_v3.js (fs 重定向 + machineCode 缓存 + publicDecrypt 伪造 )
// 前提:已解压 asar 到 app/,已创建 app.bak/,已 flipFuses

const fs = require("fs");
const path = require("path");

const RESOURCES = "D:/software/Typora/resources";
const APP_DIR = path.join(RESOURCES, "app");
const APP_BAK_DIR = path.join(RESOURCES, "app.bak");
const LAUNCH_JS = path.join(APP_DIR, "launch.dist.js");
const MC_CACHE = path.join(RESOURCES, ".mc_cache.json");

var original = fs.readFileSync(path.join(APP_BAK_DIR, "launch.dist.js"), "utf-8");

var hook = [
'/** ===== HOOK V3 ===== */',
'var _f=require("fs"),_e=require("electron"),_c=require("crypto");',
'var _LOG="D:/software/Typora/hook_v3.log";',
'try{_f.rmSync(_LOG,{force:!0})}catch(e){}',
'function W(){var a=arguments;try{_f.appendFileSync(_LOG,"["+new Date().toISOString()+"] "+Array.prototype.slice.call(a).join(" ")+"\\n")}catch(e){}}',
'',
'/* -- 1. fs 重定向(绕过完整性校验)-- */',
'var _r=/resources[\\\\/]app[\\\\/]/i,_t="resources\\\\app.bak\\\\";',
'[_f].forEach(function(fsMod){',
' ["readFileSync","readFile","statSync","stat","open","openSync",',
' "existsSync","exists","lstatSync","lstat","readdirSync","readdir",',
' "accessSync","access","realpathSync","realpath"].forEach(function(p){',
' if(typeof fsMod[p]==="function"){var o=fsMod[p];fsMod[p]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
' });',
' if(fsMod.promises){["readFile","open","stat","access","lstat","readdir","realpath"].forEach(function(p){',
' if(typeof fsMod.promises[p]==="function"){var o=fsMod.promises[p];fsMod.promises[p]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
' })}',
'});',
'W("[HOOK] fs redirect installed");',
'',
'/* -- 2. machineCode 捕获(通过 IPC)-- */',
'var _mc=null;',
'var _oh=_e.ipcMain.handle;',
'_e.ipcMain.handle=function(ch,listener){',
' return _oh.call(this,ch,async function(){',
' var r=await listener.apply(this,arguments);',
' if(ch==="license.machineCode"&&typeof r==="string"){',
' try{_mc=JSON.parse(Buffer.from(r,"base64").toString("utf-8"));W("[MC] captured:",JSON.stringify(_mc))}catch(e){}',
' }',
' return r',
' })',
'};',
'W("[HOOK] IPC machineCode capture ready");',
'',
'/* -- 3. publicDecrypt 伪造(用真实 machineCode 填充字段)-- */',
'_c.publicDecrypt=function(){',
' W("[CRYPTO] publicDecrypt intercepted");',
' if(!_mc){W("[CRYPTO] WARNING: machineCode not ready");_mc={l:"unknown",i:"unknown",v:"win|1.13.7"}}',
' var fake={deviceId:_mc.l,fingerprint:_mc.i,email:"test@test.com",license:"Cracked",version:_mc.v,date:"06/26/2026",type:"Test"};',
' W("[CRYPTO] returning fake:",JSON.stringify(fake).substring(0,200));',
' return Buffer.from(JSON.stringify(fake))',
'};',
'W("[HOOK] publicDecrypt intercept ready");',
'',
'W("===== HOOK V3 READY =====");',
'/** ===== End ===== */',
'',
].join("");

fs.writeFileSync(LAUNCH_JS, hook + original, "utf-8");
console.log("Hook V3 injected, size:", fs.statSync(LAUNCH_JS).size);
console.log("Log: type D:\\software\\Typora\\hook_v3.log");

脚本运行后,提示激活成功

image

重启Typora.exe后,又弹出激活校验。大概率是字节码文件在启动的时候,会对缓存进行识别。比如注册表。

image

reg query "HKCU\Software\Typora"

1
2
3
HKEY_CURRENT_USER\Software\Typora
IDate REG_SZ 6/26/2026
SLicense REG_SZ

可以看见SLicense的值是空的,可能是第一次没缓存到注册表,也有可能是联网验证导致的。

此时的思路就是,第一次输入激活码后,缓存激活码信息,并劫持publicDecrypt,这样第二次就不会再需要填激活码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* machineCode 缓存:启动读缓存,IPC 捕获后写缓存 */
var _mcCache="D:/software/Typora/resources/.mc_cache.json";
var _mc=null;
try{_mc=JSON.parse(_f.readFileSync(_mcCache,"utf-8"))}catch(e){}

var _oh=_e.ipcMain.handle;
_e.ipcMain.handle=function(ch,listener){
return _oh.call(this,ch,async function(){
var r=await listener.apply(this,arguments);
if(ch==="license.machineCode"&&typeof r==="string"){
try{_mc=JSON.parse(Buffer.from(r,"base64").toString("utf-8"));
_f.writeFileSync(_mcCache,JSON.stringify(_mc),"utf-8")}catch(e){}
}
return r
})
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* machineCode 缓存:启动读注册表,IPC 捕获后写注册表 */
var _cp=require("child_process");
var _mc=null;
try{
var o=_cp.execSync('reg query "HKCU\\Software\\Typora" /v MCInfo',{encoding:"utf-8",timeout:5000});
var m=o.match(/MCInfo\s+REG_SZ\s+(.+)/i);
if(m){_mc=JSON.parse(Buffer.from(m[1].trim(),"base64").toString("utf-8"))}
}catch(e){}

var _oh=_e.ipcMain.handle;
_e.ipcMain.handle=function(ch,listener){
return _oh.call(this,ch,async function(){
var r=await listener.apply(this,arguments);
if(ch==="license.machineCode"&&typeof r==="string"){
try{
_mc=JSON.parse(Buffer.from(r,"base64").toString("utf-8"));
_cp.execSync('reg add "HKCU\\Software\\Typora" /v MCInfo /t REG_SZ /d "'+r+'" /f',{encoding:"utf-8",timeout:5000})
}catch(e){}
}
return r
})
};

简单描述一下流程

第一次运行(需要输入激活码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
启动 Typora


加载 launch.dist.js → 安装 Hook


读缓存 → 无 (_mc=null)
│ ├─ 方式一:读文件 .mc_cache.json (fs.readFileSync)
│ └─ 方式二:读注册表 MCInfo (reg query)

字节码启动验证 → 触发 publicDecrypt


fingerprint="unknown" (无真实值)


验证失败 → 弹激活窗口


用户输入激活码 → 触发 offlineActivation

├─→ IPC: license.machineCode 被调用
│ └─→ 捕获真实 machineCode → 写入缓存
│ ├─ 方式一:写文件 .mc_cache.json (fs.writeFileSync)
│ └─ 方式二:写注册表 MCInfo (reg add)

└─→ publicDecrypt 再次触发
└─→ fingerprint 还是 "unknown" (时序问题)
└─→ 激活结果取决于此时 _mc 是否已捕获

第二次及以后(自动激活):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
启动 Typora


加载 launch.dist.js → 安装 Hook


读缓存 → 命中 _mc={i:"QrKEHkMcXz",...}
│ ├─ 方式一:读文件 .mc_cache.json (fs.readFileSync)
│ └─ 方式二:读注册表 MCInfo (reg query)

字节码启动验证 → 触发 publicDecrypt


fingerprint="QrKEHkMcXz" (真实值)


验证通过 → 直接进入主界面 (无弹窗)

renew联网验证劫持

文章里面还有提到Typora会每12小时向服务器(https://store.typora.io/api/client/renew)进行联网验证。

首先这里先用electron.net.fetch 拦截,再用electron.protocol.handle 兜底

1
2
3
4
5
6
7
8
9
var origFetch=electron.net.fetch;
electron.net.fetch=function(input,init){
var u="";if(typeof input==="string")u=input;else if(input&&input.url)u=input.url;else u=String(input);
if(u.indexOf("renew")>=0){
W("[NET] 拦截 renew 请求");
return Promise.resolve(new Response(JSON.stringify({success:true,msg:B("ok")}),{status:200,headers:{"content-type":"application/json"}}))
}
return origFetch.apply(this,arguments)
};
1
2
3
4
5
6
7
8
9
10
11
electron.app.whenReady().then(function(){
electron.protocol.handle("https",async function(req){
if(req.url.indexOf("renew")>=0){
W("[NET] protocol 拦截 renew");
return new Response(JSON.stringify({success:true,msg:B("ok")}),{status:200,headers:{"content-type":"application/json"}})
}
try{return await electron.net.fetch(req,{bypassCustomProtocolHandlers:true})}catch(e){throw e}
});
});

function B(s){return Buffer.from(s).toString("base64")}

electron.net.fetchlaunch.dist.js 最开头就同步替换了,能拦到最早的请求;electron.protocol.handle 是兜底,等 app.whenReady() 后再补一层。两层都返回 {success:true} ,骗过程序验证通过,SLicense 就不会被清空。

require 原型链检测

上面发现 SLicense 会变空,思路是 Hook 注册表读取,让程序读到假的 SLicenseTyporawinreg 模块读取注册表,因此考虑劫持 require("winreg")

劫持 require 最直接的方式是篡改 Module.prototype.require

reg query "HKCU\Software\Typora"

1
2
3
HKEY_CURRENT_USER\Software\Typora
IDate REG_SZ 6/26/2026
SLicense REG_SZ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// test_prototype.js
// 把 Module.prototype.require hook 注入 launch.dist.js
const fs = require("fs");
const path = require("path");

const LAUNCH = "D:/software/Typora/resources/app/launch.dist.js";
const APP_BAK = "D:/software/Typora/resources/app.bak";

var original = fs.readFileSync(path.join(APP_BAK, "launch.dist.js"), "utf-8");

var hook = [
'/** ===== PROTOTYPE HOOK TEST ===== */',
'var _f=require("fs");',
'var _LOG="D:/software/Typora/prototype.log";',
'try{_f.rmSync(_LOG,{force:!0})}catch(e){}',
'function W(s){try{_f.appendFileSync(_LOG,"["+new Date().toISOString()+"] "+s+"\\n")}catch(e){}}',
'',
'/* -- fs 重定向(绕过完整性检查的文件 hash 部分)-- */',
'var _r=/resources[\\\\/]app[\\\\/]/i,_t="resources\\\\app.bak\\\\";',
'["readFileSync","readFile","statSync","stat","open","openSync",',
' "existsSync","exists","lstatSync","lstat","readdirSync","readdir",',
' "accessSync","access","realpathSync","realpath"].forEach(function(k){',
' if(typeof _f[k]==="function"){var o=_f[k];_f[k]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
'});',
'if(_f.promises){["readFile","open","stat","access","lstat","readdir","realpath"].forEach(function(k){',
' if(typeof _f.promises[k]==="function"){var o=_f.promises[k];_f.promises[k]=function(fp){',
' if(typeof fp==="string"&&_r.test(fp))fp=fp.replace(_r,_t);',
' return o.apply(this,arguments)}}',
'})}',
'W("[HOOK] fs redirect installed");',
'',
'/* -- 原型链 Hook(直接触发完整性校验崩溃)-- */',
'W("[HOOK] installing Module.prototype.require hook...");',
'var _origRequire=require("module").prototype.require;',
'require("module").prototype.require=function(id){',
' W("[REQUIRE] "+id);',
' var result=_origRequire.apply(this,arguments);',
' if(id==="winreg"){W("[REQUIRE] winreg loaded, would hook here")}',
' return result',
'};',
'W("[HOOK] Module.prototype.require hooked");',
'',
'W("===== PROTOTYPE HOOK READY =====");',
'/** ===== End ===== */',
'',
].join("");

fs.writeFileSync(LAUNCH, hook + original, "utf-8");
console.log("Prototype hook injected, size:", fs.statSync(LAUNCH).size);
console.log("启动 Typora.exe 后查看崩溃日志:");
console.log(" type D:\\software\\Typora\\prototype.log");

1
2
3
4
5
6
7
8
9
10
11
12
13
A JavaScript error occurred in the main process
Uncaught Exception:
Error: Integrity check failed
at D:\software\Typora\resources\app.bak\atom.compiled.dist.jsc:1:210357
at D:\software\Typora\resources\app.bak\atom.compiled.dist.jsc:1:210478
at D:\software\Typora\resources\app.bak\atom.compiled.dist.jsc:1:235150
at Object.<anonymous> (D:\software\Typora\resources\app.bak\atom.compiled.dist.jsc:1:235166)
at Module._extensions..jsc.Module._extensions..cjsc (D:\software\Typora\resources\app\launch.dist.js:1:2798)
at Module.load (node:internal/modules/cjs/loader:1472:32)
at Module._load (node:internal/modules/cjs/loader:1289:12)
at c._load (node:electron/js2c/node_init:2:17950)
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:242:24)

image

字节码加载后 requirepathmodule ,然后在检测 module 时发现原型链被篡改,直接抛 Integrity check failed 崩溃。

所以就直接改用 child_process.execSyncreg 命令直接读写注册表,不碰 require 。这样也就绕开 require原型链,不会触发完整性校验

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
const asarMod = require("asar");
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const { flipFuses, FuseV1Options, FuseVersion } = require("@electron/fuses");

// 控制台 UTF-8(修复中文乱码)
if (process.platform === "win32") {
try { execSync("chcp 65001 >nul", { stdio: "ignore" }); } catch(e) {}
}

// ========== 查找 Typora 安装路径 ==========

function findFromRegistry() {
var roots = [
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
"HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
];
for (var i = 0; i < roots.length; i++) {
try {
// 第1步:用关键词搜索,定位到子项路径(HKEY_ 开头的行)
var out = execSync(
'reg query "' + roots[i] + '" /s /f Typora',
{ encoding: "utf-8", timeout: 10000 }
);
var subKeys = out.split(/\r?\n/)
.map(function(s){return s.trim()})
.filter(function(s){return /^HKEY_/i.test(s)});
// 第2步:对每个匹配的子项,读取全部值,找 InstallLocation
for (var j = 0; j < subKeys.length; j++) {
try {
var detail = execSync(
'reg query "' + subKeys[j] + '"',
{ encoding: "utf-8", timeout: 5000 }
);
var m = detail.match(/InstallLocation\s+REG_SZ\s+(.+)/i);
if (m) {
var p = m[1].trim().replace(/\\/g, "/");
// 去掉末尾斜杠
p = p.replace(/\/+$/, "");
if (fs.existsSync(path.join(p, "Typora.exe"))) return p;
}
} catch(e2) {}
}
} catch(e) {}
}
return null;
}

function findFromShortcut() {
var os = require("os");
var searchDirs = [
path.join(os.homedir(), "Desktop"),
"C:/Users/Public/Desktop",
path.join(os.homedir(), "AppData/Roaming/Microsoft/Windows/Start Menu/Programs"),
"C:/ProgramData/Microsoft/Windows/Start Menu/Programs",
];
// 递归收集所有 .lnk
function listLnk(dir) {
var out = [];
try {
var stack = [dir];
while (stack.length) {
var cur = stack.pop();
var entries;
try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch(e) { continue; }
for (var i = 0; i < entries.length; i++) {
var full = path.join(cur, entries[i].name);
if (entries[i].isDirectory()) {
if (entries[i].name === "Internet Explorer") continue; // 跳过慢目录
stack.push(full);
} else if (entries[i].name.toLowerCase().endsWith(".lnk")) {
out.push(full);
}
}
}
} catch(e) {}
return out;
}
// 用 PowerShell 解析单个 .lnk(单文件调用,避免引号嵌套)
function resolveLnk(lnkPath) {
// Base64 编码命令,彻底避开引号/空格问题
// -NoProfile -ExecutionPolicy Bypass 跳过首次加载;重定向 6>$null 抑制 CLIXML 进度噪音
var cmd = '$ErrorActionPreference="SilentlyContinue"; $ProgressPreference="SilentlyContinue"; $ws=New-Object -ComObject WScript.Shell; $ws.CreateShortcut("' + lnkPath.replace(/"/g, '`"') + '").TargetPath';
var b64 = Buffer.from(cmd, "utf16le").toString("base64");
try {
var out = execSync('powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ' + b64 + ' 6>$null', { encoding: "utf-8", timeout: 5000 });
var t = out.trim();
if (t) return t;
} catch(e) {}
return null;
}

var lnkFiles = [];
for (var i = 0; i < searchDirs.length; i++) {
if (fs.existsSync(searchDirs[i])) {
lnkFiles = lnkFiles.concat(listLnk(searchDirs[i]));
}
}
for (var j = 0; j < lnkFiles.length; j++) {
// 先按文件名快速过滤
if (!/typora/i.test(path.basename(lnkFiles[j], ".lnk"))) continue;
var target = resolveLnk(lnkFiles[j]);
if (target && /Typora\.exe$/i.test(target) && fs.existsSync(target)) {
return { lnk: lnkFiles[j], dir: path.dirname(target) };
}
}
return null;
}

function resolvePath(input) {
input = input.trim().replace(/"/g, "").replace(/\\/g, "/");
if (!fs.existsSync(input)) return null;
var stat = fs.statSync(input);
if (stat.isDirectory()) {
if (fs.existsSync(path.join(input, "Typora.exe"))) return input;
return null;
}
if (stat.isFile() && input.toLowerCase().endsWith(".exe")) {
return path.dirname(input);
}
return null;
}

function launch(cliPath) {
// 1. 命令行参数
if (cliPath) {
var r = resolvePath(cliPath);
if (r) { console.log("Typora 安装路径: " + r + " 【命令行参数】"); start(r); return; }
}
// 2. 注册表
var reg = findFromRegistry();
if (reg) { console.log("Typora 安装路径: " + reg + " 【注册表】"); start(reg); return; }
// 3. 桌面快捷方式
var sc = findFromShortcut();
if (sc) {
console.log("Typora 安装路径: " + sc.dir + " 【桌面快捷方式】");
start(sc.dir); return;
}
// 4. 手动输入
console.log("未自动找到 Typora,请手动输入路径。");
console.log("支持: 安装目录 (D:/software/Typora) 或 exe 路径");
var rl = require("readline").createInterface({ input: process.stdin, output: process.stdout });
rl.question("路径: ", function(input) {
rl.close();
var resolved = resolvePath(input);
if (!resolved) { console.error("无效路径,未找到 Typora.exe"); process.exit(1); }
console.log("Typora 路径: " + resolved);
start(resolved);
});
}
launch(process.argv[2]);

// ========== 主流程 ==========

async function start(typoraPath) {
var RESOURCES = path.join(typoraPath, "resources");
var ASAR_PATH = path.join(RESOURCES, "app.asar");
var APP_DIR = path.join(RESOURCES, "app");
var APP_BAK_DIR = path.join(RESOURCES, "app.bak");
var EXE_PATH = path.join(typoraPath, "Typora.exe");
var LAUNCH_JS = path.join(APP_DIR, "launch.dist.js");

var now = new Date();
var dateStr = [
String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, "0"),
now.getFullYear(),
].join("/");

console.log("=== 初始化 ===");

// 备份 exe
if (!fs.existsSync(EXE_PATH + ".bak")) {
fs.copyFileSync(EXE_PATH, EXE_PATH + ".bak");
console.log("Typora.exe → .bak");
}
// 备份 asar
if (!fs.existsSync(ASAR_PATH + ".bak") && fs.existsSync(ASAR_PATH)) {
fs.copyFileSync(ASAR_PATH, ASAR_PATH + ".bak");
console.log("app.asar → .bak");
}
// 解压 asar
if (fs.existsSync(ASAR_PATH)) {
try { fs.rmSync(APP_DIR, { recursive: true, force: true }); } catch(e) {}
asarMod.extractAll(ASAR_PATH, APP_DIR);
console.log("asar → app/");
}
// 复制原始文件
if (!fs.existsSync(APP_BAK_DIR)) {
(function copyDir(s, d) {
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
var entries = fs.readdirSync(s, { withFileTypes: true });
for (var i = 0; i < entries.length; i++) {
var sp = path.join(s, entries[i].name);
var dp = path.join(d, entries[i].name);
entries[i].isDirectory() ? copyDir(sp, dp) : fs.copyFileSync(sp, dp);
}
})(APP_DIR, APP_BAK_DIR);
console.log("app/ → app.bak/");
}
// 删除 app.asar
if (fs.existsSync(ASAR_PATH)) {
fs.rmSync(ASAR_PATH, { force: true });
console.log("app.asar 已删除");
}
// flipFuses
try {
await flipFuses(EXE_PATH, {
version: FuseVersion.V1,
[FuseV1Options.OnlyLoadAppFromAsar]: false,
});
console.log("Fuse 已修改");
} catch(e) {
if (e.code === "EBUSY") {
console.error("Typora.exe 被占用,请先关闭所有 Typora 进程再运行。");
process.exit(1);
}
throw e;
}

// ========== 版本检查 ==========
var version = "unknown";
try {
// 从刚解压的 app/package.json 读取
var pkgPath = path.join(APP_DIR, "package.json");
var pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
version = pkg.version;
} catch(e) {}
console.log("Typora 版本: " + version);
if (version !== "1.13.7") {
console.log("警告: 仅 1.13.7 验证通过,其他版本可能不兼容,继续执行...");
}

// ========== Hook 注入 ==========
var hookCode = `
var hookFs=require("fs"),hookCp=require("child_process"),hookOs=require("os");
var LOG="${typoraPath.replace(/\\/g,"\\\\")}\\\\typora.log";
try{hookFs.rmSync(LOG,{force:true})}catch(e){}
function W(){var a=arguments;try{hookFs.appendFileSync(LOG,"["+new Date().toISOString()+"] "+Array.prototype.slice.call(a).join(" ")+"\\n")}catch(e){}}

W("========== Hook 启动 ==========");

// -- 0. 写入 SLicense(抢在字节码检查注册表之前) --
W("[REG] 写入 SLicense...");
try{
hookCp.execSync('reg add "HKCU\\\\Software\\\\Typora" /v SLicense /t REG_SZ /d "VHlwb3Jh#0#1/1/2029" /f',{encoding:"utf-8",timeout:5000});
W("[REG] SLicense 写入完成");
}catch(e){W("[REG] SLicense 写入失败:",e.message)}

// -- 1. 读取机器码缓存 --
var cachedMC=null;
W("[MC] 读取机器码缓存...");
try{
var o=hookCp.execSync('reg query "HKCU\\\\Software\\\\Typora" /v MCInfo',{encoding:"utf-8",timeout:5000});
var m=o.match(/MCInfo\\s+REG_SZ\\s+(.+)/i);
if(m){
cachedMC=JSON.parse(Buffer.from(m[1].trim(),"base64").toString("utf-8"));
W("[MC] 缓存命中, fingerprint="+cachedMC.i);
}
}catch(e){W("[MC] 缓存未命中 (首次运行)")}

// -- 2. fs 路径重定向 --
var redirectFrom=/resources[\\\\/]app[\\\\/]/i,redirectTo="resources\\\\app.bak\\\\";
var hookedCount=0;
[hookFs].forEach(function(fsMod){
["readFileSync","readFile","statSync","stat","open","openSync","existsSync","exists","lstatSync","lstat","readdirSync","readdir","accessSync","access","realpathSync","realpath"].forEach(function(p){
if(typeof fsMod[p]==="function"){var origFn=fsMod[p];fsMod[p]=function(fp){if(typeof fp==="string"&&redirectFrom.test(fp))fp=fp.replace(redirectFrom,redirectTo);return origFn.apply(this,arguments)};hookedCount++}
});
if(fsMod.promises){["readFile","open","stat","access","lstat","readdir","realpath"].forEach(function(p){
if(typeof fsMod.promises[p]==="function"){var origP=fsMod.promises[p];fsMod.promises[p]=function(fp){if(typeof fp==="string"&&redirectFrom.test(fp))fp=fp.replace(redirectFrom,redirectTo);return origP.apply(this,arguments)};hookedCount++}
})}
});
W("[HOOK] fs 重定向已安装 (函数数="+hookedCount+")");

// -- 3. 机器码自动捕获 --
var electron=require("electron"),origHandle=electron.ipcMain.handle;
electron.ipcMain.handle=function(ch,listener){
// 日志所有 IPC 注册
W("[IPC:reg] "+ch);
if(ch==="license.machineCode"){
return origHandle.call(this,ch,async function(evt){
var r=await listener.apply(this,arguments);
try{
var mc=JSON.parse(Buffer.from(r,"base64").toString("utf-8"));
if(!cachedMC||cachedMC.i!==mc.i){
cachedMC=mc;
hookCp.execSync('reg add "HKCU\\\\Software\\\\Typora" /v MCInfo /t REG_SZ /d "'+r+'" /f',{encoding:"utf-8",timeout:5000});
W("[MC] 捕获新机器码 -> 已写入注册表, fingerprint="+mc.i);
}
}catch(e){W("[MC] 解析失败:",e.message)}
return r
});
}
// 所有 IPC 调用都记录(过滤高频的 document.addSnapAndLastSync 和 document.setContent)
if(ch!=="document.addSnapAndLastSync"&&ch!=="document.setContent"){
return origHandle.call(this,ch,async function(evt){
var args=Array.prototype.slice.call(arguments,1);
W("[IPC:call] "+ch+" "+JSON.stringify(args).substring(0,200));
var r=await listener.apply(this,arguments);
var rs=JSON.stringify(r);
if(rs&&rs.length<500)W("[IPC:resp] "+ch+" "+rs);
return r
});
}
return origHandle.call(this,ch,listener)
};
W("[HOOK] 机器码自动捕获已安装");

// -- 4. crypto.publicDecrypt 劫持 --
var cryptoMod=require("crypto");
cryptoMod.publicDecrypt=function(k,buf){
var mc=cachedMC;
var deviceId=mc?mc.l:(hookOs.hostname()+" | "+(hookOs.userInfo().username||"user")+" | Windows");
var fingerprint=mc?mc.i:"pending";
var ver=mc?mc.v:"win|1.13.7";
W("[CRYPTO] publicDecrypt 被调用, 输入="+buf.length+"字节, fingerprint="+fingerprint+(mc?"":" (待捕获)"));
var data={
deviceId:deviceId,
fingerprint:fingerprint,
email:"admin@localhost",
license:"Cracked_Typora",
version:ver,
date:"${dateStr}",
type:"Standard"
};
return Buffer.from(JSON.stringify(data))
};
W("[HOOK] crypto.publicDecrypt 已安装");

// -- 5. electron.net.fetch 劫持 --
var origFetch=electron.net.fetch;
electron.net.fetch=function(input,init){
var u="";if(typeof input==="string")u=input;else if(input&&input.url)u=input.url;else u=String(input);
if(u.indexOf("renew")>=0){
W("[NET] 拦截 renew 请求");
return Promise.resolve(new Response(JSON.stringify({success:true,msg:B("ok")}),{status:200,headers:{"content-type":"application/json"}}))
}
return origFetch.apply(this,arguments)
};
W("[HOOK] electron.net.fetch 已安装 (拦截 renew)");

// -- 6. protocol handle 兜底 --
electron.app.whenReady().then(function(){
electron.protocol.handle("https",async function(req){
if(req.url.indexOf("renew")>=0){
W("[NET] protocol 拦截 renew");
return new Response(JSON.stringify({success:true,msg:B("ok")}),{status:200,headers:{"content-type":"application/json"}})
}
try{return await electron.net.fetch(req,{bypassCustomProtocolHandlers:true})}catch(e){throw e}
});
W("[HOOK] protocol handle 已安装")
});

function B(s){return Buffer.from(s).toString("base64")}
W("========== Hook 全部就绪 (MC="+(cachedMC?"已缓存,指纹="+cachedMC.i:"待捕获")+") ==========");
`;

var original = fs.readFileSync(path.join(APP_BAK_DIR, "launch.dist.js"), "utf-8");
fs.writeFileSync(LAUNCH_JS, hookCode + "\n" + original, "utf-8");
console.log("Hook 注入完成");
console.log("");
console.log("====================================");
console.log(" 激活码格式: +XXXXXXXX#");
console.log(" 示例: +12345678#");
console.log(" 必须以 + 开头、# 结尾,中间任意字符");
console.log("====================================");
console.log("");

// 注册表 IDate
try {
execSync('powershell -Command "Set-ItemProperty -Path \'HKCU:\\Software\\Typora\' -Name \'IDate\' -Value \'' + dateStr + '\' -Force"', { encoding: "utf-8" });
} catch(e) {}

try { fs.rmSync(path.join(typoraPath, "typora.log"), { force: true }); } catch(e) {}
console.log("Typora 已启动。");
execSync('start "" "' + EXE_PATH + '"', { encoding: "utf-8" });
}

破解效果

目前用了两周多了,也没出现问题。

image

启动日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[2026-06-27T12:15:58.112Z] ========== Hook 启动 ==========
[2026-06-27T12:15:58.114Z] [REG] 写入 SLicense...
[2026-06-27T12:15:58.183Z] [REG] SLicense 写入完成
[2026-06-27T12:15:58.184Z] [MC] 读取机器码缓存...
[2026-06-27T12:15:58.252Z] [MC] 缓存命中, fingerprint=QrKEHkMcXz
[2026-06-27T12:15:58.253Z] [HOOK] fs 重定向已安装 (函数数=23)
[2026-06-27T12:15:58.253Z] [HOOK] 机器码自动捕获已安装
[2026-06-27T12:15:58.260Z] [HOOK] crypto.publicDecrypt 已安装
[2026-06-27T12:15:58.261Z] [HOOK] electron.net.fetch 已安装 (拦截 renew)
[2026-06-27T12:15:58.261Z] ========== Hook 全部就绪 (MC=已缓存,指纹=QrKEHkMcXz) ==========
...
[2026-06-27T12:15:58.384Z] [IPC:reg] license.machineCode
[2026-06-27T12:15:58.385Z] [IPC:reg] addLicense
[2026-06-27T12:15:58.385Z] [IPC:reg] offlineActivation
[2026-06-27T12:15:58.385Z] [IPC:reg] license.show
[2026-06-27T12:15:58.386Z] [IPC:reg] removeLicense
[2026-06-27T12:15:58.386Z] [IPC:reg] ... (其他约 120 个 IPC)
...
[2026-06-27T12:15:58.568Z] [HOOK] protocol handle 已安装
[2026-06-27T12:15:58.569Z] [CRYPTO] publicDecrypt 被调用, 输入=6字节, fingerprint=QrKEHkMcXz
...
[2026-06-27T12:15:59.380Z] [IPC:call] document.loadData []
[2026-06-27T12:15:59.381Z] [IPC:resp] document.loadData "{\"snap\":null,\"filePath\":null,\"backups\":null,\"shouldReadFromDisk\":false,\"windowCounts\":1}"
[2026-06-27T12:15:59.430Z] [IPC:call] setting.getRecentFiles []
[2026-06-27T12:15:59.587Z] [IPC:call] theme.setThemeSource ["system"]
[2026-06-27T12:15:59.587Z] [IPC:call] setting.put ["backgroundColor","#FFFFFF"]
[2026-06-27T12:15:59.588Z] [IPC:call] setting.put ["isDarkMode",false]
[2026-06-27T12:15:59.589Z] [IPC:call] setting.getDownloadingDicts []
[2026-06-27T12:15:59.589Z] [IPC:resp] setting.getDownloadingDicts "[]"
[2026-06-27T12:15:59.590Z] [IPC:call] setting.getUserDictionaryPath []
[2026-06-27T12:15:59.590Z] [IPC:resp] setting.getUserDictionaryPath "C:\\Users\\admin\\AppData\\Roaming\\Typora\\typora-dictionaries"
[2026-06-27T12:15:59.591Z] [IPC:call] setting.getUserDict []
[2026-06-27T12:15:59.592Z] [IPC:resp] setting.getUserDict "{}"
...
[2026-06-27T12:16:00.007Z] [IPC:call] app.onCloseWin [null]

Author: jdr
Link: https://jdr2021.github.io/2026/06/26/Typora-v1-13-7-离线激活破解/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.