### 前置条件 | 软件 | 用途 | 如何检查 | | ----------------- | -------------- | --------------------------------- | | Python 3.9 及以上 | 运行脚本 | 打开终端输入 `python --version` | | Node.js | 解压/打包 asar | 输入 `node -v` 和 `npx --version` | | Typora | 目标程序 | 确认安装目录存在 | 1. 新增 typora_crack.py 文件,内容如下: ```python #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Typora 1.13.x 本机激活脚本(研究/学习用) ===================================== 已验证路径(Windows / Typora 1.13.7): - crypto.publicDecrypt 返回伪造许可 JSON - 自动 license.machineCode + offlineActivation - 完整性:app -> app.bak 文件重定向 + launch sha256 equals 放行 - renew 接口进程内返回 {success:true} - 默认安静日志、快速 fs hook、SLicense 正常时跳过重复 offline 不需要部署在线站点 / 本地 HTTP 服务。 用法: python typora_crack.py --typora-dir "E:\\Program Files\\Typora" activate python typora_crack.py --typora-dir "E:\\Program Files\\Typora" status python typora_crack.py --typora-dir "E:\\Program Files\\Typora" restore 环境变量: CTF_HOOK_VERBOSE=1 开启 hook 文件日志(默认关闭以提速) """ from __future__ import annotations import argparse import base64 import hashlib import json import os import shutil import subprocess import sys import time import winreg from datetime import date from pathlib import Path DEFAULT_TYPORA = Path(r"E:\Program Files\Typora") WORK = Path(os.environ.get("TEMP", r"C:\Users\Public")) / "typora_crack_work" ORIG_LAUNCH_SHA_FALLBACK = ( "3ec9df885d96feaa030b2e34f02007a1b0659971624f043726ce12ce8d97b81b" ) def log(*args): print(*args, flush=True) def _resolve_cmd(cmd): """Windows: prefer .cmd for npx/npm (CreateProcess cannot run .ps1).""" if not isinstance(cmd, (list, tuple)) or not cmd: return cmd exe = str(cmd[0]) if os.name == "nt" and exe.lower() in ("npx", "npm", "node"): for candidate in (exe + ".cmd", exe + ".exe", exe): path = shutil.which(candidate) if path and not path.lower().endswith(".ps1"): return [path, *list(cmd)[1:]] return subprocess.list2cmdline(list(cmd)) return cmd def run(cmd, **kwargs): cmd2 = _resolve_cmd(cmd) if isinstance(cmd2, str): return subprocess.run(cmd2, shell=True, **kwargs) return subprocess.run(cmd2, shell=False, **kwargs) def kill_typora(): run("taskkill /F /IM Typora.exe >NUL 2>&1", check=False) time.sleep(0.4) run("taskkill /F /IM Typora.exe >NUL 2>&1", check=False) def require_admin_hint(path: Path): try: test = path / ".write_test_tmp" test.write_text("ok", encoding="utf-8") test.unlink() except Exception: log("[!] 无法写入:", path) log(" 请用【管理员】PowerShell/CMD 重新运行。") sys.exit(1) class Paths: def __init__(self, typora_dir: Path): self.root = typora_dir self.exe = typora_dir / "Typora.exe" self.exe_bak = typora_dir / "Typora.exe.crackbak" self.res = typora_dir / "resources" self.asar = self.res / "app.asar" self.asar_bak = self.res / "app.asar.crackbak" self.app = self.res / "app" self.app_bak = self.res / "app.bak" self.launch = self.app / "launch.dist.js" self.launch_clean = self.app_bak / "launch.dist.js" self.hook_log = WORK / "Typora_HookLog.txt" self.result = WORK / "last_result.txt" self.backup_dir = WORK / "backup_before_crack" self.forge_cache = WORK / "forge_cache.json" self.appdata_log = Path(os.environ["APPDATA"]) / "Typora" / "typora.log" def get_machine_guid() -> str: with winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography" ) as k: guid, _ = winreg.QueryValueEx(k, "MachineGuid") return str(guid) def compute_fp_full(guid: str) -> str: return base64.b64encode( hashlib.sha256((guid + "typora").encode()).digest() ).decode() def compute_fp_short(guid: str) -> str: # machineCode field "i" is first 10 chars of base64(sha256(guid+"typora")) return compute_fp_full(guid)[:10] def guess_device_id() -> str: """Best-effort deviceId before machineCode is available.""" import socket host = socket.gethostname() or "PC" user = os.environ.get("USERNAME") or os.environ.get("USER") or "user" return f"{host} | {user} | Windows" def today_str() -> str: d = date.today() return f"{d.month}/{d.day}/{d.year}" def is_placeholder_slicense(sl: str) -> bool: """BwcH... is Buffer.alloc(128,7) base64 placeholder used by old buggy hooks.""" if not sl: return True if sl.startswith("#0#"): return True # pure placeholder blob (all 0x07) if sl.startswith("BwcHBwcH") and "#0#" in sl: return True return False def backup(p: Paths): kill_typora() b = p.backup_dir b.mkdir(parents=True, exist_ok=True) if p.asar.exists(): shutil.copy2(p.asar, b / "app.asar") log("[+] 已备份 app.asar") if p.exe.exists() and not (b / "Typora.exe").exists(): shutil.copy2(p.exe, b / "Typora.exe") log("[+] 已备份 Typora.exe") try: run( f'reg export "HKCU\\Software\\Typora" "{b / "typora.reg"}" /y', check=False, capture_output=True, ) except Exception: pass log("[+] 备份目录:", b) def restore(p: Paths): kill_typora() b = p.backup_dir if (b / "app.asar").exists(): shutil.copy2(b / "app.asar", p.asar) log("[+] 已还原 app.asar") if (b / "Typora.exe").exists(): shutil.copy2(b / "Typora.exe", p.exe) log("[+] 已还原 Typora.exe") for d in (p.app, p.app_bak): if d.exists(): shutil.rmtree(d, ignore_errors=True) if (b / "typora.reg").exists(): run(f'reg import "{b / "typora.reg"}"', check=False) try: key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, r"Software\Typora") winreg.SetValueEx(key, "SLicense", 0, winreg.REG_SZ, "") winreg.CloseKey(key) except Exception: pass log("[+] 还原完成") def extract_clean(p: Paths): kill_typora() require_admin_hint(p.res) src = p.asar if not src.exists() and p.asar_bak.exists(): src = p.asar_bak if not src.exists() and (p.backup_dir / "app.asar").exists(): src = p.backup_dir / "app.asar" if not src.exists(): raise SystemExit(f"找不到 app.asar: {p.res}") if not p.asar_bak.exists() and p.asar.exists(): shutil.copy2(p.asar, p.asar_bak) log("[+] 创建", p.asar_bak) # always rebuild pristine from cleanest source clean_src = p.asar_bak if p.asar_bak.exists() else src if p.app_bak.exists(): shutil.rmtree(p.app_bak) p.app_bak.mkdir(parents=True) log("[*] 解压干净 app.bak ...") r = run( ["npx", "--yes", "@electron/asar", "extract", str(clean_src), str(p.app_bak)], capture_output=True, ) if r.returncode != 0: log(r.stderr.decode("utf-8", "replace")) raise SystemExit("asar extract 失败(请安装 Node.js 并确保 npx 可用)") if p.app.exists(): shutil.rmtree(p.app) shutil.copytree(p.app_bak, p.app) log("[+] app + app.bak 就绪") def pack_asar(p: Paths): log("[*] 打包 app.asar ...") r = run( ["npx", "--yes", "@electron/asar", "pack", str(p.app), str(p.asar)], capture_output=True, ) if r.returncode != 0: log(r.stderr.decode("utf-8", "replace")) raise SystemExit("asar pack 失败") log("[+] 已打包", p.asar, "size", p.asar.stat().st_size) def original_launch_sha(p: Paths) -> str: f = p.launch_clean if not f.exists(): return ORIG_LAUNCH_SHA_FALLBACK return hashlib.sha256(f.read_bytes()).hexdigest() def try_flip_fuse(p: Paths): work = WORK / "fuses" work.mkdir(parents=True, exist_ok=True) script = work / "flip.cjs" script.write_text( f""" const fs = require('fs'); async function main() {{ let flipFuses, FuseV1Options, FuseVersion; try {{ ({{ flipFuses, FuseV1Options, FuseVersion }} = require('@electron/fuses')); }} catch (e) {{ console.log('FUSES_NOT_INSTALLED'); process.exit(0); }} const fullPath = {json.dumps(str(p.exe))}; if (!fs.existsSync(fullPath + '.crackbak')) {{ fs.copyFileSync(fullPath, fullPath + '.crackbak'); console.log('EXE_BACKUP_OK'); }} try {{ await flipFuses(fullPath, {{ version: FuseVersion.V1, [FuseV1Options.OnlyLoadAppFromAsar]: false, }}); console.log('FUSE_OK'); }} catch (e) {{ console.log('FUSE_FAIL', e && e.message); }} }} main(); """, encoding="utf-8", newline="\n", ) run( ["npm", "install", "@electron/fuses", "--no-fund", "--no-audit", "--prefix", str(work)], capture_output=True, ) r = run(["node", str(script)], cwd=str(work), capture_output=True) msg = (r.stdout + r.stderr).decode("utf-8", "replace").strip() log("[fuse]", msg or "(no output)") def build_hook_js(p: Paths, forge_list: list, launch_sha: str) -> str: """Fast + stable hook (quiet by default).""" cache_path = str(p.forge_cache).replace("\\", "/") log_path = str(p.hook_log).replace("\\", "/") return f'''"use strict"; (function () {{ const fs = require("fs"); const crypto = require("crypto"); const Module = require("module"); const LOG_PATH = {json.dumps(log_path)}; const CACHE_PATH = {json.dumps(cache_path)}; const VERBOSE = process.env.CTF_HOOK_VERBOSE === "1"; function writeLog() {{ if (!VERBOSE) return; try {{ const parts = []; for (let i = 0; i < arguments.length; i++) {{ const x = arguments[i]; try {{ parts.push(typeof x === "string" ? x : JSON.stringify(x)); }} catch (e) {{ parts.push(String(x)); }} }} fs.appendFileSync(LOG_PATH, "[" + new Date().toLocaleString() + "] " + parts.join(" ") + "\\n"); }} catch (e) {{}} }} // CRITICAL: // - Placeholder SLicense (BwcH...#0#date from Buffer.alloc(128,7)) is NOT a real license. // - Never set "activated" / skip offline based on that placeholder. // - Only offlineActivation success marks real activation (slGood=true). let slGood = false; let needOffline = true; function isPlaceholderSL(sl) {{ if (!sl) return true; if (sl.charAt(0) === "#") return true; // "#0#7/17/2026" if (sl.indexOf("BwcHBwcH") === 0) return true; // old buggy placeholder if (sl.length < 80) return true; if (sl.indexOf("#") <= 20) return true; return false; }} // Early SLicense cleanup only (do NOT write fake success placeholder) (function cleanupSLicenseEarly() {{ try {{ const {{ execFileSync }} = require("child_process"); function regQuery(name) {{ try {{ const out = execFileSync("reg", ["query", "HKCU\\\\Software\\\\Typora", "/v", name], {{ encoding: "utf8" }}); const m = out.match(new RegExp(name + "\\\\s+REG_SZ\\\\s+(.+)$", "mi")); return m ? m[1].trim() : ""; }} catch (e) {{ return ""; }} }} function regAdd(name, value) {{ execFileSync("reg", ["add", "HKCU\\\\Software\\\\Typora", "/v", name, "/t", "REG_SZ", "/d", value, "/f"], {{ encoding: "utf8" }}); }} let sl = regQuery("SLicense"); writeLog("[SL] current", sl ? (sl.slice(0, 48) + " len=" + sl.length) : "(empty)"); if (isPlaceholderSL(sl)) {{ // clear corrupt/placeholder so LM starts clean; offlineActivation will write real value regAdd("SLicense", ""); needOffline = true; slGood = false; writeLog("[SL] cleared placeholder/corrupt; needOffline=true"); }} else {{ // looks non-placeholder; still run machineCode once, but may skip offline if decrypt path passes // Safer default: still needOffline once per cold start unless env forces skip needOffline = true; slGood = false; writeLog("[SL] non-placeholder present, still will try offline once"); }} }} catch (e) {{ writeLog("[SL] cleanup fail", e && e.message); needOffline = true; slGood = false; }} }})(); // Fast fs redirect for integrity function redir(filePath) {{ if (typeof filePath !== "string" || filePath.length < 12) return filePath; let i = filePath.indexOf("resources"); if (i < 0) {{ i = filePath.indexOf("Resources"); if (i < 0) return filePath; }} const rest = filePath.slice(i + 9); if (!(rest.startsWith("\\\\app\\\\") || rest.startsWith("/app/") || rest.startsWith("\\\\App\\\\") || rest.startsWith("/App/") || rest.startsWith("\\\\app/") || rest.startsWith("/app\\\\"))) {{ return filePath; }} const lower = filePath.toLowerCase(); let idx = lower.indexOf("resources\\\\app\\\\"); if (idx >= 0) {{ return filePath.slice(0, idx) + "resources\\\\app.bak\\\\" + filePath.slice(idx + "resources\\\\app\\\\".length); }} idx = lower.indexOf("resources/app/"); if (idx >= 0) {{ return filePath.slice(0, idx) + "resources/app.bak/" + filePath.slice(idx + "resources/app/".length); }} return filePath; }} function wrapFs(obj, names) {{ for (let n = 0; n < names.length; n++) {{ const key = names[n]; if (!obj[key]) continue; const orig = obj[key].bind(obj); obj[key] = function (filePath) {{ if (arguments.length === 0) return orig.apply(obj, arguments); if (arguments.length === 1) return orig(redir(filePath)); if (arguments.length === 2) return orig(redir(filePath), arguments[1]); if (arguments.length === 3) return orig(redir(filePath), arguments[1], arguments[2]); const args = Array.prototype.slice.call(arguments); args[0] = redir(filePath); return orig.apply(obj, args); }}; }} }} wrapFs(fs, ["readFileSync", "readFile", "statSync", "stat", "open", "openSync", "realpathSync", "realpath"]); if (fs.promises) wrapFs(fs.promises, ["readFile", "open", "stat", "realpath"]); // Integrity equals bypass (silent) try {{ const _eq = Buffer.prototype.equals; const EXPECTED = Buffer.from({json.dumps(launch_sha)}, "hex"); Buffer.prototype.equals = function (other) {{ try {{ if (this && other && this.length === 32 && other.length === 32) {{ if (_eq.call(this, EXPECTED) || _eq.call(other, EXPECTED)) return true; }} }} catch (e) {{}} return _eq.call(this, other); }}; }} catch (e) {{}} const FORGES = {json.dumps(forge_list, ensure_ascii=False)}; let forgeIndex = 0; function currentForge() {{ return FORGES[forgeIndex % FORGES.length]; }} try {{ if (fs.existsSync(CACHE_PATH)) {{ const cached = JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); if (cached && cached.fingerprint && cached.deviceId) {{ for (let i = 0; i < FORGES.length; i++) {{ if (cached.fingerprint) FORGES[i].fingerprint = cached.fingerprint; if (cached.deviceId) FORGES[i].deviceId = cached.deviceId; if (cached.version) FORGES[i].version = cached.version; }} }} }} }} catch (e) {{}} let payloadCache = FORGES.map(function (f) {{ return Buffer.from(JSON.stringify(f), "utf8"); }}); function refreshPayloadCache() {{ payloadCache = FORGES.map(function (f) {{ return Buffer.from(JSON.stringify(f), "utf8"); }}); }} crypto.publicDecrypt = function (key, buffer) {{ return payloadCache[forgeIndex % payloadCache.length]; }}; function jsDecryptBridge() {{ return currentForge(); }} try {{ Object.defineProperty(crypto, "jsDecrypt", {{ configurable: true, enumerable: true, get: function () {{ return jsDecryptBridge; }}, set: function () {{}} }}); }} catch (e) {{ crypto.jsDecrypt = jsDecryptBridge; }} global.jsDecrypt = jsDecryptBridge; const handlerMap = {{}}; function hookElectron() {{ try {{ const electron = require("electron"); if (!electron || !electron.app) return false; if (electron.ipcMain && electron.ipcMain.handle) {{ const origHandle = electron.ipcMain.handle.bind(electron.ipcMain); electron.ipcMain.handle = function (channel, listener) {{ const ch = String(channel); handlerMap[ch] = listener; if (!/license|activ|machine|offline/i.test(ch)) {{ return origHandle(channel, listener); }} return origHandle(channel, async function (event) {{ return listener.apply(null, arguments); }}); }}; }} electron.app.whenReady().then(function () {{ try {{ if (electron.protocol && electron.protocol.handle) {{ electron.protocol.handle("https", async function (request) {{ const url = String(request.url); if (url.indexOf("/api/client/renew") >= 0) {{ return new Response(JSON.stringify({{ success: true }}), {{ status: 200, headers: {{ "content-type": "application/json" }} }}); }} return electron.net.fetch(request, {{ bypassCustomProtocolHandlers: true }}); }}); }} }} catch (e) {{}} setTimeout(async function () {{ // Always capture real machineCode first (deviceId/fingerprint must match this PC) try {{ if (handlerMap["license.machineCode"]) {{ const mc = await handlerMap["license.machineCode"]({{ sender: {{ id: 0 }} }}); writeLog("[auto] machineCode", mc); try {{ const o = JSON.parse(Buffer.from(String(mc), "base64").toString("utf8")); writeLog("[auto] decoded", JSON.stringify(o)); for (let i = 0; i < FORGES.length; i++) {{ if (o.i) FORGES[i].fingerprint = o.i; if (o.l) FORGES[i].deviceId = o.l; if (o.v) FORGES[i].version = o.v; }} refreshPayloadCache(); try {{ fs.writeFileSync(CACHE_PATH, JSON.stringify({{ fingerprint: o.i, deviceId: o.l, version: o.v }})); writeLog("[forge-cache] saved"); }} catch (e) {{ writeLog("[forge-cache] save fail", e && e.message); }} }} catch (e) {{ writeLog("[auto] machine decode fail", e && e.message); }} }} else {{ writeLog("[auto] no license.machineCode handler"); }} }} catch (e) {{ writeLog("[auto] machineCode err", e && e.message); }} // MUST run offlineActivation unless already succeeded this session if (!handlerMap["offlineActivation"]) {{ writeLog("[auto] offlineActivation handler missing"); return; }} if (slGood && !needOffline) {{ writeLog("[auto] skip offline, already activated this session"); return; }} for (let i = 0; i < FORGES.length; i++) {{ forgeIndex = i; try {{ writeLog("[auto] offline try", i, JSON.stringify(currentForge())); // dummy ciphertext; publicDecrypt is forged so content ignored const dummy = Buffer.alloc(128, 9).toString("base64"); const res = await handlerMap["offlineActivation"]({{ sender: {{ id: 0 }} }}, dummy); writeLog("[auto] offline res", i, JSON.stringify(res)); if (Array.isArray(res) && res[0] === true) {{ slGood = true; needOffline = false; writeLog("[SUCCESS] offlineActivation", i); break; }} }} catch (e) {{ writeLog("[auto] offline err", i, e && e.message); }} }} }}, 2000); }}); return true; }} catch (e) {{ return false; }} }} if (!hookElectron()) {{ const origReq = Module.prototype.require; Module.prototype.require = function (id) {{ const exp = origReq.apply(this, arguments); if (id === "electron") {{ try {{ hookElectron(); }} catch (e) {{}} }} return exp; }}; }} }})(); ''' def inject_launch(p: Paths, forge_list: list): if not p.launch_clean.exists(): raise SystemExit("缺少 app.bak/launch.dist.js,请先 extract") clean = p.launch_clean.read_text(encoding="utf-8") if not clean.startswith('"use strict";'): raise SystemExit("launch.dist.js 格式异常") body = clean[len('"use strict";') :] sha = original_launch_sha(p) log("[*] 原始 launch sha256:", sha) # apply forge cache into list if present if p.forge_cache.exists(): try: cached = json.loads(p.forge_cache.read_text(encoding="utf-8")) for f in forge_list: if cached.get("fingerprint"): f["fingerprint"] = cached["fingerprint"] if cached.get("deviceId"): f["deviceId"] = cached["deviceId"] if cached.get("version"): f["version"] = cached["version"] log("[*] 已加载 forge_cache") except Exception: pass hook = build_hook_js(p, forge_list, sha) text = hook + body p.launch.write_text(text, encoding="utf-8") r = run(["node", "--check", str(p.launch)], capture_output=True) if r.returncode != 0: log(r.stderr.decode("utf-8", "replace")) raise SystemExit("launch.dist.js 语法错误") log("[+] 已注入 launch.dist.js, size", p.launch.stat().st_size) def set_registry_for_activation(): key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, r"Software\Typora") # Always clear before activate so LM + offline path run cleanly winreg.SetValueEx(key, "SLicense", 0, winreg.REG_SZ, "") winreg.SetValueEx(key, "IDate", 0, winreg.REG_SZ, today_str()) winreg.CloseKey(key) def start_typora(p: Paths): if p.hook_log.exists(): try: p.hook_log.unlink() except Exception: pass try: if p.appdata_log.exists(): p.appdata_log.write_text("", encoding="utf-8") except Exception: pass # Force verbose hooks during activate so last_result/hook logs are useful env = os.environ.copy() env["CTF_HOOK_VERBOSE"] = "1" subprocess.Popen([str(p.exe)], env=env, cwd=str(p.root)) log("[*] 已启动 Typora (CTF_HOOK_VERBOSE=1)") def _read_logs(p: Paths): hook = p.hook_log.read_text(encoding="utf-8", errors="replace") if p.hook_log.exists() else "" logtxt = ( p.appdata_log.read_text(encoding="utf-8", errors="replace") if p.appdata_log.exists() else "" ) return hook, logtxt def _print_highlights(hook: str, logtxt: str): log("---- hook 摘要 ----") for ln in hook.splitlines(): if any( k in ln for k in [ "SUCCESS", "offline", "machineCode", "decoded", "forge", "SL", "publicDecrypt", ] ): log(ln) log("---- typora.log 摘要 ----") for ln in logtxt.splitlines(): if any( k in ln for k in [ "hasL", "no info", "pass", "pure", "Integrity", "SLicense", "start LM", "offline", ] ): log(ln) def wait_and_check(p: Paths, seconds: int = 12) -> bool: time.sleep(seconds) hook, logtxt = _read_logs(p) p.result.write_text(hook + "\n\n==== TYPORA LOG (run1) ====\n" + logtxt, encoding="utf-8") _print_highlights(hook, logtxt) has_true = "hasL: true" in logtxt success_marker = "[SUCCESS] offlineActivation" in hook log("[*] run1 hasL:true ?", has_true, "| offline SUCCESS ?", success_marker) # Even if slGood skipped offline, restart once if first read was empty then later wrote # Offline success always needs restart because hasL is set at LM start. need_restart = success_marker or ( not has_true and p.appdata_log.exists() and "SLicense" in logtxt ) # If already true, good. If offline succeeded, restart. if success_marker and not has_true: need_restart = True elif not has_true: # check registry for written SLicense try: key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Typora") sl, _ = winreg.QueryValueEx(key, "SLicense") if isinstance(sl, str) and len(sl) >= 80 and not sl.startswith("#0#"): need_restart = True except Exception: pass if need_restart and not has_true: log("[*] 许可可能已写入,重启 Typora 验证 hasL ...") kill_typora() time.sleep(1) try: if p.appdata_log.exists(): p.appdata_log.write_text("", encoding="utf-8") except Exception: pass start_typora(p) time.sleep(8) hook2, logtxt2 = _read_logs(p) p.result.write_text( hook + "\n\n==== RUN1 LOG ====\n" + logtxt + "\n\n==== RUN2 LOG ====\n" + logtxt2 + "\n\n==== HOOK2 ====\n" + hook2, encoding="utf-8", ) log("---- run2 typora.log 摘要 ----") for ln in logtxt2.splitlines(): if any( k in ln for k in ["hasL", "no info", "pass", "pure", "Integrity", "SLicense", "start LM"] ): log(ln) has_true = "hasL: true" in logtxt2 log("[*] run2 hasL:true ?", has_true) log("[*] 完整日志:", p.result) return has_true def build_default_forges(guid: str) -> list: fp = compute_fp_short(guid) # Real machineCode.l looks like: "HOST | USER | Windows" # GUID-based guess is wrong and causes offlineActivation failure on fresh PCs. device = guess_device_id() ver = "win|1.13.7" d = today_str() base = { "deviceId": device, "fingerprint": fp, "email": "ctf@local.test", "license": "CTF_LOCAL_LICENSE", "version": ver, "date": d, } return [ {**base, "type": "pro"}, {**base, "type": "DreamNya"}, {**base, "type": "SUCCESS"}, {**base, "type": ""}, dict(base), ] def cmd_activate(p: Paths, skip_fuse: bool = False): WORK.mkdir(parents=True, exist_ok=True) if not p.exe.exists(): raise SystemExit(f"找不到 Typora.exe: {p.exe}") # ensure license.html is original (do NOT rewrite page-dist integrity files) lic = p.res / "page-dist" / "license.html" lic_bak = p.res / "page-dist" / "license.html.ctfbak" if lic_bak.exists() and lic.exists(): # if current looks like auto-close stub, restore try: txt = lic.read_text(encoding="utf-8", errors="replace") if "window.close" in txt and lic.stat().st_size < 1500: shutil.copy2(lic_bak, lic) log("[+] 已从 ctfbak 恢复原始 license.html") except Exception: pass backup(p) extract_clean(p) if not skip_fuse: try_flip_fuse(p) else: log("[*] 跳过 fuse 修改") guid = get_machine_guid() log("[*] MachineGuid:", guid) log("[*] fp_full:", compute_fp_full(guid)) log("[*] fp_short(i):", compute_fp_short(guid)) WORK.mkdir(parents=True, exist_ok=True) # drop stale forge cache so first run always re-captures this machine if p.forge_cache.exists(): try: p.forge_cache.unlink() log("[*] 已删除旧 forge_cache.json") except Exception: pass forges = build_default_forges(guid) log("[*] 初始 deviceId 猜测:", forges[0]["deviceId"]) log("[*] 将通过 license.machineCode 覆盖为真实值") inject_launch(p, forges) pack_asar(p) set_registry_for_activation() kill_typora() start_typora(p) ok = wait_and_check(p, seconds=12) kill_typora() if ok: log("") log("=" * 60) log(" SUCCESS: hasL:true") log(" 保持当前 resources/app + app.asar + 注册表 SLicense") log(" Hook 日志:", p.hook_log) log(" 结果文件:", p.result) log(" 调试日志: 设置环境变量 CTF_HOOK_VERBOSE=1 后启动 Typora") log("=" * 60) else: log("") log("=" * 60) log(" 尚未确认 hasL:true") log(" 请查看:", p.result) log(" 常见处理:") log(" - 管理员运行") log(" - 安装 Node.js / 确认 npx 可用") log(" - 无法启动时: python typora_crack.py restore") log("=" * 60) return ok def cmd_status(p: Paths): logtxt = "" if p.appdata_log.exists(): logtxt = p.appdata_log.read_text(encoding="utf-8", errors="replace") has_true = "hasL: true" in logtxt has_false = "hasL: false" in logtxt log("typora.log:", p.appdata_log) log("hasL:true ?", has_true, "| hasL:false ?", has_false) try: key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Typora") sl, _ = winreg.QueryValueEx(key, "SLicense") log("SLicense len:", len(sl) if isinstance(sl, str) else sl) if isinstance(sl, str) and sl.startswith("#0#"): log("[!] SLicense 看起来已损坏(以 #0# 开头)") except Exception as e: log("SLicense: (none)", e) for ln in logtxt.splitlines()[-40:]: if any(k in ln for k in ["hasL", "pure", "no info", "pass", "SLicense", "Integrity"]): log(ln) def cmd_restore(p: Paths): restore(p) def main(): ap = argparse.ArgumentParser(description="Typora 本机激活脚本(研究用)") ap.add_argument( "--typora-dir", default=str(DEFAULT_TYPORA), help=r'Typora 安装目录,例如 "E:\Program Files\Typora"', ) ap.add_argument( "--skip-fuse", action="store_true", help="不修改 Typora.exe 的 OnlyLoadAppFromAsar Fuse", ) ap.add_argument( "command", choices=["activate", "status", "restore"], help="activate | status | restore", ) args = ap.parse_args() typora_dir = Path(args.typora_dir) p = Paths(typora_dir) WORK.mkdir(parents=True, exist_ok=True) log("Typora 目录:", p.root) log("工作目录 :", WORK) if args.command == "activate": cmd_activate(p, skip_fuse=args.skip_fuse) elif args.command == "status": cmd_status(p) elif args.command == "restore": cmd_restore(p) if __name__ == "__main__": main() ``` 2. 执行命令: ```pwsh python .\typora_crack.py --typora-dir "D:\Program Files\Typora" {activate|status|restore} ``` **注: 记得取消自动更新** Loading... ### 前置条件 | 软件 | 用途 | 如何检查 | | ----------------- | -------------- | --------------------------------- | | Python 3.9 及以上 | 运行脚本 | 打开终端输入 `python --version` | | Node.js | 解压/打包 asar | 输入 `node -v` 和 `npx --version` | | Typora | 目标程序 | 确认安装目录存在 | 1. 新增 typora_crack.py 文件,内容如下: ```python #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Typora 1.13.x 本机激活脚本(研究/学习用) ===================================== 已验证路径(Windows / Typora 1.13.7): - crypto.publicDecrypt 返回伪造许可 JSON - 自动 license.machineCode + offlineActivation - 完整性:app -> app.bak 文件重定向 + launch sha256 equals 放行 - renew 接口进程内返回 {success:true} - 默认安静日志、快速 fs hook、SLicense 正常时跳过重复 offline 不需要部署在线站点 / 本地 HTTP 服务。 用法: python typora_crack.py --typora-dir "E:\\Program Files\\Typora" activate python typora_crack.py --typora-dir "E:\\Program Files\\Typora" status python typora_crack.py --typora-dir "E:\\Program Files\\Typora" restore 环境变量: CTF_HOOK_VERBOSE=1 开启 hook 文件日志(默认关闭以提速) """ from __future__ import annotations import argparse import base64 import hashlib import json import os import shutil import subprocess import sys import time import winreg from datetime import date from pathlib import Path DEFAULT_TYPORA = Path(r"E:\Program Files\Typora") WORK = Path(os.environ.get("TEMP", r"C:\Users\Public")) / "typora_crack_work" ORIG_LAUNCH_SHA_FALLBACK = ( "3ec9df885d96feaa030b2e34f02007a1b0659971624f043726ce12ce8d97b81b" ) def log(*args): print(*args, flush=True) def _resolve_cmd(cmd): """Windows: prefer .cmd for npx/npm (CreateProcess cannot run .ps1).""" if not isinstance(cmd, (list, tuple)) or not cmd: return cmd exe = str(cmd[0]) if os.name == "nt" and exe.lower() in ("npx", "npm", "node"): for candidate in (exe + ".cmd", exe + ".exe", exe): path = shutil.which(candidate) if path and not path.lower().endswith(".ps1"): return [path, *list(cmd)[1:]] return subprocess.list2cmdline(list(cmd)) return cmd def run(cmd, **kwargs): cmd2 = _resolve_cmd(cmd) if isinstance(cmd2, str): return subprocess.run(cmd2, shell=True, **kwargs) return subprocess.run(cmd2, shell=False, **kwargs) def kill_typora(): run("taskkill /F /IM Typora.exe >NUL 2>&1", check=False) time.sleep(0.4) run("taskkill /F /IM Typora.exe >NUL 2>&1", check=False) def require_admin_hint(path: Path): try: test = path / ".write_test_tmp" test.write_text("ok", encoding="utf-8") test.unlink() except Exception: log("[!] 无法写入:", path) log(" 请用【管理员】PowerShell/CMD 重新运行。") sys.exit(1) class Paths: def __init__(self, typora_dir: Path): self.root = typora_dir self.exe = typora_dir / "Typora.exe" self.exe_bak = typora_dir / "Typora.exe.crackbak" self.res = typora_dir / "resources" self.asar = self.res / "app.asar" self.asar_bak = self.res / "app.asar.crackbak" self.app = self.res / "app" self.app_bak = self.res / "app.bak" self.launch = self.app / "launch.dist.js" self.launch_clean = self.app_bak / "launch.dist.js" self.hook_log = WORK / "Typora_HookLog.txt" self.result = WORK / "last_result.txt" self.backup_dir = WORK / "backup_before_crack" self.forge_cache = WORK / "forge_cache.json" self.appdata_log = Path(os.environ["APPDATA"]) / "Typora" / "typora.log" def get_machine_guid() -> str: with winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography" ) as k: guid, _ = winreg.QueryValueEx(k, "MachineGuid") return str(guid) def compute_fp_full(guid: str) -> str: return base64.b64encode( hashlib.sha256((guid + "typora").encode()).digest() ).decode() def compute_fp_short(guid: str) -> str: # machineCode field "i" is first 10 chars of base64(sha256(guid+"typora")) return compute_fp_full(guid)[:10] def guess_device_id() -> str: """Best-effort deviceId before machineCode is available.""" import socket host = socket.gethostname() or "PC" user = os.environ.get("USERNAME") or os.environ.get("USER") or "user" return f"{host} | {user} | Windows" def today_str() -> str: d = date.today() return f"{d.month}/{d.day}/{d.year}" def is_placeholder_slicense(sl: str) -> bool: """BwcH... is Buffer.alloc(128,7) base64 placeholder used by old buggy hooks.""" if not sl: return True if sl.startswith("#0#"): return True # pure placeholder blob (all 0x07) if sl.startswith("BwcHBwcH") and "#0#" in sl: return True return False def backup(p: Paths): kill_typora() b = p.backup_dir b.mkdir(parents=True, exist_ok=True) if p.asar.exists(): shutil.copy2(p.asar, b / "app.asar") log("[+] 已备份 app.asar") if p.exe.exists() and not (b / "Typora.exe").exists(): shutil.copy2(p.exe, b / "Typora.exe") log("[+] 已备份 Typora.exe") try: run( f'reg export "HKCU\\Software\\Typora" "{b / "typora.reg"}" /y', check=False, capture_output=True, ) except Exception: pass log("[+] 备份目录:", b) def restore(p: Paths): kill_typora() b = p.backup_dir if (b / "app.asar").exists(): shutil.copy2(b / "app.asar", p.asar) log("[+] 已还原 app.asar") if (b / "Typora.exe").exists(): shutil.copy2(b / "Typora.exe", p.exe) log("[+] 已还原 Typora.exe") for d in (p.app, p.app_bak): if d.exists(): shutil.rmtree(d, ignore_errors=True) if (b / "typora.reg").exists(): run(f'reg import "{b / "typora.reg"}"', check=False) try: key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, r"Software\Typora") winreg.SetValueEx(key, "SLicense", 0, winreg.REG_SZ, "") winreg.CloseKey(key) except Exception: pass log("[+] 还原完成") def extract_clean(p: Paths): kill_typora() require_admin_hint(p.res) src = p.asar if not src.exists() and p.asar_bak.exists(): src = p.asar_bak if not src.exists() and (p.backup_dir / "app.asar").exists(): src = p.backup_dir / "app.asar" if not src.exists(): raise SystemExit(f"找不到 app.asar: {p.res}") if not p.asar_bak.exists() and p.asar.exists(): shutil.copy2(p.asar, p.asar_bak) log("[+] 创建", p.asar_bak) # always rebuild pristine from cleanest source clean_src = p.asar_bak if p.asar_bak.exists() else src if p.app_bak.exists(): shutil.rmtree(p.app_bak) p.app_bak.mkdir(parents=True) log("[*] 解压干净 app.bak ...") r = run( ["npx", "--yes", "@electron/asar", "extract", str(clean_src), str(p.app_bak)], capture_output=True, ) if r.returncode != 0: log(r.stderr.decode("utf-8", "replace")) raise SystemExit("asar extract 失败(请安装 Node.js 并确保 npx 可用)") if p.app.exists(): shutil.rmtree(p.app) shutil.copytree(p.app_bak, p.app) log("[+] app + app.bak 就绪") def pack_asar(p: Paths): log("[*] 打包 app.asar ...") r = run( ["npx", "--yes", "@electron/asar", "pack", str(p.app), str(p.asar)], capture_output=True, ) if r.returncode != 0: log(r.stderr.decode("utf-8", "replace")) raise SystemExit("asar pack 失败") log("[+] 已打包", p.asar, "size", p.asar.stat().st_size) def original_launch_sha(p: Paths) -> str: f = p.launch_clean if not f.exists(): return ORIG_LAUNCH_SHA_FALLBACK return hashlib.sha256(f.read_bytes()).hexdigest() def try_flip_fuse(p: Paths): work = WORK / "fuses" work.mkdir(parents=True, exist_ok=True) script = work / "flip.cjs" script.write_text( f""" const fs = require('fs'); async function main() {{ let flipFuses, FuseV1Options, FuseVersion; try {{ ({{ flipFuses, FuseV1Options, FuseVersion }} = require('@electron/fuses')); }} catch (e) {{ console.log('FUSES_NOT_INSTALLED'); process.exit(0); }} const fullPath = {json.dumps(str(p.exe))}; if (!fs.existsSync(fullPath + '.crackbak')) {{ fs.copyFileSync(fullPath, fullPath + '.crackbak'); console.log('EXE_BACKUP_OK'); }} try {{ await flipFuses(fullPath, {{ version: FuseVersion.V1, [FuseV1Options.OnlyLoadAppFromAsar]: false, }}); console.log('FUSE_OK'); }} catch (e) {{ console.log('FUSE_FAIL', e && e.message); }} }} main(); """, encoding="utf-8", newline="\n", ) run( ["npm", "install", "@electron/fuses", "--no-fund", "--no-audit", "--prefix", str(work)], capture_output=True, ) r = run(["node", str(script)], cwd=str(work), capture_output=True) msg = (r.stdout + r.stderr).decode("utf-8", "replace").strip() log("[fuse]", msg or "(no output)") def build_hook_js(p: Paths, forge_list: list, launch_sha: str) -> str: """Fast + stable hook (quiet by default).""" cache_path = str(p.forge_cache).replace("\\", "/") log_path = str(p.hook_log).replace("\\", "/") return f'''"use strict"; (function () {{ const fs = require("fs"); const crypto = require("crypto"); const Module = require("module"); const LOG_PATH = {json.dumps(log_path)}; const CACHE_PATH = {json.dumps(cache_path)}; const VERBOSE = process.env.CTF_HOOK_VERBOSE === "1"; function writeLog() {{ if (!VERBOSE) return; try {{ const parts = []; for (let i = 0; i < arguments.length; i++) {{ const x = arguments[i]; try {{ parts.push(typeof x === "string" ? x : JSON.stringify(x)); }} catch (e) {{ parts.push(String(x)); }} }} fs.appendFileSync(LOG_PATH, "[" + new Date().toLocaleString() + "] " + parts.join(" ") + "\\n"); }} catch (e) {{}} }} // CRITICAL: // - Placeholder SLicense (BwcH...#0#date from Buffer.alloc(128,7)) is NOT a real license. // - Never set "activated" / skip offline based on that placeholder. // - Only offlineActivation success marks real activation (slGood=true). let slGood = false; let needOffline = true; function isPlaceholderSL(sl) {{ if (!sl) return true; if (sl.charAt(0) === "#") return true; // "#0#7/17/2026" if (sl.indexOf("BwcHBwcH") === 0) return true; // old buggy placeholder if (sl.length < 80) return true; if (sl.indexOf("#") <= 20) return true; return false; }} // Early SLicense cleanup only (do NOT write fake success placeholder) (function cleanupSLicenseEarly() {{ try {{ const {{ execFileSync }} = require("child_process"); function regQuery(name) {{ try {{ const out = execFileSync("reg", ["query", "HKCU\\\\Software\\\\Typora", "/v", name], {{ encoding: "utf8" }}); const m = out.match(new RegExp(name + "\\\\s+REG_SZ\\\\s+(.+)$", "mi")); return m ? m[1].trim() : ""; }} catch (e) {{ return ""; }} }} function regAdd(name, value) {{ execFileSync("reg", ["add", "HKCU\\\\Software\\\\Typora", "/v", name, "/t", "REG_SZ", "/d", value, "/f"], {{ encoding: "utf8" }}); }} let sl = regQuery("SLicense"); writeLog("[SL] current", sl ? (sl.slice(0, 48) + " len=" + sl.length) : "(empty)"); if (isPlaceholderSL(sl)) {{ // clear corrupt/placeholder so LM starts clean; offlineActivation will write real value regAdd("SLicense", ""); needOffline = true; slGood = false; writeLog("[SL] cleared placeholder/corrupt; needOffline=true"); }} else {{ // looks non-placeholder; still run machineCode once, but may skip offline if decrypt path passes // Safer default: still needOffline once per cold start unless env forces skip needOffline = true; slGood = false; writeLog("[SL] non-placeholder present, still will try offline once"); }} }} catch (e) {{ writeLog("[SL] cleanup fail", e && e.message); needOffline = true; slGood = false; }} }})(); // Fast fs redirect for integrity function redir(filePath) {{ if (typeof filePath !== "string" || filePath.length < 12) return filePath; let i = filePath.indexOf("resources"); if (i < 0) {{ i = filePath.indexOf("Resources"); if (i < 0) return filePath; }} const rest = filePath.slice(i + 9); if (!(rest.startsWith("\\\\app\\\\") || rest.startsWith("/app/") || rest.startsWith("\\\\App\\\\") || rest.startsWith("/App/") || rest.startsWith("\\\\app/") || rest.startsWith("/app\\\\"))) {{ return filePath; }} const lower = filePath.toLowerCase(); let idx = lower.indexOf("resources\\\\app\\\\"); if (idx >= 0) {{ return filePath.slice(0, idx) + "resources\\\\app.bak\\\\" + filePath.slice(idx + "resources\\\\app\\\\".length); }} idx = lower.indexOf("resources/app/"); if (idx >= 0) {{ return filePath.slice(0, idx) + "resources/app.bak/" + filePath.slice(idx + "resources/app/".length); }} return filePath; }} function wrapFs(obj, names) {{ for (let n = 0; n < names.length; n++) {{ const key = names[n]; if (!obj[key]) continue; const orig = obj[key].bind(obj); obj[key] = function (filePath) {{ if (arguments.length === 0) return orig.apply(obj, arguments); if (arguments.length === 1) return orig(redir(filePath)); if (arguments.length === 2) return orig(redir(filePath), arguments[1]); if (arguments.length === 3) return orig(redir(filePath), arguments[1], arguments[2]); const args = Array.prototype.slice.call(arguments); args[0] = redir(filePath); return orig.apply(obj, args); }}; }} }} wrapFs(fs, ["readFileSync", "readFile", "statSync", "stat", "open", "openSync", "realpathSync", "realpath"]); if (fs.promises) wrapFs(fs.promises, ["readFile", "open", "stat", "realpath"]); // Integrity equals bypass (silent) try {{ const _eq = Buffer.prototype.equals; const EXPECTED = Buffer.from({json.dumps(launch_sha)}, "hex"); Buffer.prototype.equals = function (other) {{ try {{ if (this && other && this.length === 32 && other.length === 32) {{ if (_eq.call(this, EXPECTED) || _eq.call(other, EXPECTED)) return true; }} }} catch (e) {{}} return _eq.call(this, other); }}; }} catch (e) {{}} const FORGES = {json.dumps(forge_list, ensure_ascii=False)}; let forgeIndex = 0; function currentForge() {{ return FORGES[forgeIndex % FORGES.length]; }} try {{ if (fs.existsSync(CACHE_PATH)) {{ const cached = JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); if (cached && cached.fingerprint && cached.deviceId) {{ for (let i = 0; i < FORGES.length; i++) {{ if (cached.fingerprint) FORGES[i].fingerprint = cached.fingerprint; if (cached.deviceId) FORGES[i].deviceId = cached.deviceId; if (cached.version) FORGES[i].version = cached.version; }} }} }} }} catch (e) {{}} let payloadCache = FORGES.map(function (f) {{ return Buffer.from(JSON.stringify(f), "utf8"); }}); function refreshPayloadCache() {{ payloadCache = FORGES.map(function (f) {{ return Buffer.from(JSON.stringify(f), "utf8"); }}); }} crypto.publicDecrypt = function (key, buffer) {{ return payloadCache[forgeIndex % payloadCache.length]; }}; function jsDecryptBridge() {{ return currentForge(); }} try {{ Object.defineProperty(crypto, "jsDecrypt", {{ configurable: true, enumerable: true, get: function () {{ return jsDecryptBridge; }}, set: function () {{}} }}); }} catch (e) {{ crypto.jsDecrypt = jsDecryptBridge; }} global.jsDecrypt = jsDecryptBridge; const handlerMap = {{}}; function hookElectron() {{ try {{ const electron = require("electron"); if (!electron || !electron.app) return false; if (electron.ipcMain && electron.ipcMain.handle) {{ const origHandle = electron.ipcMain.handle.bind(electron.ipcMain); electron.ipcMain.handle = function (channel, listener) {{ const ch = String(channel); handlerMap[ch] = listener; if (!/license|activ|machine|offline/i.test(ch)) {{ return origHandle(channel, listener); }} return origHandle(channel, async function (event) {{ return listener.apply(null, arguments); }}); }}; }} electron.app.whenReady().then(function () {{ try {{ if (electron.protocol && electron.protocol.handle) {{ electron.protocol.handle("https", async function (request) {{ const url = String(request.url); if (url.indexOf("/api/client/renew") >= 0) {{ return new Response(JSON.stringify({{ success: true }}), {{ status: 200, headers: {{ "content-type": "application/json" }} }}); }} return electron.net.fetch(request, {{ bypassCustomProtocolHandlers: true }}); }}); }} }} catch (e) {{}} setTimeout(async function () {{ // Always capture real machineCode first (deviceId/fingerprint must match this PC) try {{ if (handlerMap["license.machineCode"]) {{ const mc = await handlerMap["license.machineCode"]({{ sender: {{ id: 0 }} }}); writeLog("[auto] machineCode", mc); try {{ const o = JSON.parse(Buffer.from(String(mc), "base64").toString("utf8")); writeLog("[auto] decoded", JSON.stringify(o)); for (let i = 0; i < FORGES.length; i++) {{ if (o.i) FORGES[i].fingerprint = o.i; if (o.l) FORGES[i].deviceId = o.l; if (o.v) FORGES[i].version = o.v; }} refreshPayloadCache(); try {{ fs.writeFileSync(CACHE_PATH, JSON.stringify({{ fingerprint: o.i, deviceId: o.l, version: o.v }})); writeLog("[forge-cache] saved"); }} catch (e) {{ writeLog("[forge-cache] save fail", e && e.message); }} }} catch (e) {{ writeLog("[auto] machine decode fail", e && e.message); }} }} else {{ writeLog("[auto] no license.machineCode handler"); }} }} catch (e) {{ writeLog("[auto] machineCode err", e && e.message); }} // MUST run offlineActivation unless already succeeded this session if (!handlerMap["offlineActivation"]) {{ writeLog("[auto] offlineActivation handler missing"); return; }} if (slGood && !needOffline) {{ writeLog("[auto] skip offline, already activated this session"); return; }} for (let i = 0; i < FORGES.length; i++) {{ forgeIndex = i; try {{ writeLog("[auto] offline try", i, JSON.stringify(currentForge())); // dummy ciphertext; publicDecrypt is forged so content ignored const dummy = Buffer.alloc(128, 9).toString("base64"); const res = await handlerMap["offlineActivation"]({{ sender: {{ id: 0 }} }}, dummy); writeLog("[auto] offline res", i, JSON.stringify(res)); if (Array.isArray(res) && res[0] === true) {{ slGood = true; needOffline = false; writeLog("[SUCCESS] offlineActivation", i); break; }} }} catch (e) {{ writeLog("[auto] offline err", i, e && e.message); }} }} }}, 2000); }}); return true; }} catch (e) {{ return false; }} }} if (!hookElectron()) {{ const origReq = Module.prototype.require; Module.prototype.require = function (id) {{ const exp = origReq.apply(this, arguments); if (id === "electron") {{ try {{ hookElectron(); }} catch (e) {{}} }} return exp; }}; }} }})(); ''' def inject_launch(p: Paths, forge_list: list): if not p.launch_clean.exists(): raise SystemExit("缺少 app.bak/launch.dist.js,请先 extract") clean = p.launch_clean.read_text(encoding="utf-8") if not clean.startswith('"use strict";'): raise SystemExit("launch.dist.js 格式异常") body = clean[len('"use strict";') :] sha = original_launch_sha(p) log("[*] 原始 launch sha256:", sha) # apply forge cache into list if present if p.forge_cache.exists(): try: cached = json.loads(p.forge_cache.read_text(encoding="utf-8")) for f in forge_list: if cached.get("fingerprint"): f["fingerprint"] = cached["fingerprint"] if cached.get("deviceId"): f["deviceId"] = cached["deviceId"] if cached.get("version"): f["version"] = cached["version"] log("[*] 已加载 forge_cache") except Exception: pass hook = build_hook_js(p, forge_list, sha) text = hook + body p.launch.write_text(text, encoding="utf-8") r = run(["node", "--check", str(p.launch)], capture_output=True) if r.returncode != 0: log(r.stderr.decode("utf-8", "replace")) raise SystemExit("launch.dist.js 语法错误") log("[+] 已注入 launch.dist.js, size", p.launch.stat().st_size) def set_registry_for_activation(): key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, r"Software\Typora") # Always clear before activate so LM + offline path run cleanly winreg.SetValueEx(key, "SLicense", 0, winreg.REG_SZ, "") winreg.SetValueEx(key, "IDate", 0, winreg.REG_SZ, today_str()) winreg.CloseKey(key) def start_typora(p: Paths): if p.hook_log.exists(): try: p.hook_log.unlink() except Exception: pass try: if p.appdata_log.exists(): p.appdata_log.write_text("", encoding="utf-8") except Exception: pass # Force verbose hooks during activate so last_result/hook logs are useful env = os.environ.copy() env["CTF_HOOK_VERBOSE"] = "1" subprocess.Popen([str(p.exe)], env=env, cwd=str(p.root)) log("[*] 已启动 Typora (CTF_HOOK_VERBOSE=1)") def _read_logs(p: Paths): hook = p.hook_log.read_text(encoding="utf-8", errors="replace") if p.hook_log.exists() else "" logtxt = ( p.appdata_log.read_text(encoding="utf-8", errors="replace") if p.appdata_log.exists() else "" ) return hook, logtxt def _print_highlights(hook: str, logtxt: str): log("---- hook 摘要 ----") for ln in hook.splitlines(): if any( k in ln for k in [ "SUCCESS", "offline", "machineCode", "decoded", "forge", "SL", "publicDecrypt", ] ): log(ln) log("---- typora.log 摘要 ----") for ln in logtxt.splitlines(): if any( k in ln for k in [ "hasL", "no info", "pass", "pure", "Integrity", "SLicense", "start LM", "offline", ] ): log(ln) def wait_and_check(p: Paths, seconds: int = 12) -> bool: time.sleep(seconds) hook, logtxt = _read_logs(p) p.result.write_text(hook + "\n\n==== TYPORA LOG (run1) ====\n" + logtxt, encoding="utf-8") _print_highlights(hook, logtxt) has_true = "hasL: true" in logtxt success_marker = "[SUCCESS] offlineActivation" in hook log("[*] run1 hasL:true ?", has_true, "| offline SUCCESS ?", success_marker) # Even if slGood skipped offline, restart once if first read was empty then later wrote # Offline success always needs restart because hasL is set at LM start. need_restart = success_marker or ( not has_true and p.appdata_log.exists() and "SLicense" in logtxt ) # If already true, good. If offline succeeded, restart. if success_marker and not has_true: need_restart = True elif not has_true: # check registry for written SLicense try: key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Typora") sl, _ = winreg.QueryValueEx(key, "SLicense") if isinstance(sl, str) and len(sl) >= 80 and not sl.startswith("#0#"): need_restart = True except Exception: pass if need_restart and not has_true: log("[*] 许可可能已写入,重启 Typora 验证 hasL ...") kill_typora() time.sleep(1) try: if p.appdata_log.exists(): p.appdata_log.write_text("", encoding="utf-8") except Exception: pass start_typora(p) time.sleep(8) hook2, logtxt2 = _read_logs(p) p.result.write_text( hook + "\n\n==== RUN1 LOG ====\n" + logtxt + "\n\n==== RUN2 LOG ====\n" + logtxt2 + "\n\n==== HOOK2 ====\n" + hook2, encoding="utf-8", ) log("---- run2 typora.log 摘要 ----") for ln in logtxt2.splitlines(): if any( k in ln for k in ["hasL", "no info", "pass", "pure", "Integrity", "SLicense", "start LM"] ): log(ln) has_true = "hasL: true" in logtxt2 log("[*] run2 hasL:true ?", has_true) log("[*] 完整日志:", p.result) return has_true def build_default_forges(guid: str) -> list: fp = compute_fp_short(guid) # Real machineCode.l looks like: "HOST | USER | Windows" # GUID-based guess is wrong and causes offlineActivation failure on fresh PCs. device = guess_device_id() ver = "win|1.13.7" d = today_str() base = { "deviceId": device, "fingerprint": fp, "email": "ctf@local.test", "license": "CTF_LOCAL_LICENSE", "version": ver, "date": d, } return [ {**base, "type": "pro"}, {**base, "type": "DreamNya"}, {**base, "type": "SUCCESS"}, {**base, "type": ""}, dict(base), ] def cmd_activate(p: Paths, skip_fuse: bool = False): WORK.mkdir(parents=True, exist_ok=True) if not p.exe.exists(): raise SystemExit(f"找不到 Typora.exe: {p.exe}") # ensure license.html is original (do NOT rewrite page-dist integrity files) lic = p.res / "page-dist" / "license.html" lic_bak = p.res / "page-dist" / "license.html.ctfbak" if lic_bak.exists() and lic.exists(): # if current looks like auto-close stub, restore try: txt = lic.read_text(encoding="utf-8", errors="replace") if "window.close" in txt and lic.stat().st_size < 1500: shutil.copy2(lic_bak, lic) log("[+] 已从 ctfbak 恢复原始 license.html") except Exception: pass backup(p) extract_clean(p) if not skip_fuse: try_flip_fuse(p) else: log("[*] 跳过 fuse 修改") guid = get_machine_guid() log("[*] MachineGuid:", guid) log("[*] fp_full:", compute_fp_full(guid)) log("[*] fp_short(i):", compute_fp_short(guid)) WORK.mkdir(parents=True, exist_ok=True) # drop stale forge cache so first run always re-captures this machine if p.forge_cache.exists(): try: p.forge_cache.unlink() log("[*] 已删除旧 forge_cache.json") except Exception: pass forges = build_default_forges(guid) log("[*] 初始 deviceId 猜测:", forges[0]["deviceId"]) log("[*] 将通过 license.machineCode 覆盖为真实值") inject_launch(p, forges) pack_asar(p) set_registry_for_activation() kill_typora() start_typora(p) ok = wait_and_check(p, seconds=12) kill_typora() if ok: log("") log("=" * 60) log(" SUCCESS: hasL:true") log(" 保持当前 resources/app + app.asar + 注册表 SLicense") log(" Hook 日志:", p.hook_log) log(" 结果文件:", p.result) log(" 调试日志: 设置环境变量 CTF_HOOK_VERBOSE=1 后启动 Typora") log("=" * 60) else: log("") log("=" * 60) log(" 尚未确认 hasL:true") log(" 请查看:", p.result) log(" 常见处理:") log(" - 管理员运行") log(" - 安装 Node.js / 确认 npx 可用") log(" - 无法启动时: python typora_crack.py restore") log("=" * 60) return ok def cmd_status(p: Paths): logtxt = "" if p.appdata_log.exists(): logtxt = p.appdata_log.read_text(encoding="utf-8", errors="replace") has_true = "hasL: true" in logtxt has_false = "hasL: false" in logtxt log("typora.log:", p.appdata_log) log("hasL:true ?", has_true, "| hasL:false ?", has_false) try: key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Typora") sl, _ = winreg.QueryValueEx(key, "SLicense") log("SLicense len:", len(sl) if isinstance(sl, str) else sl) if isinstance(sl, str) and sl.startswith("#0#"): log("[!] SLicense 看起来已损坏(以 #0# 开头)") except Exception as e: log("SLicense: (none)", e) for ln in logtxt.splitlines()[-40:]: if any(k in ln for k in ["hasL", "pure", "no info", "pass", "SLicense", "Integrity"]): log(ln) def cmd_restore(p: Paths): restore(p) def main(): ap = argparse.ArgumentParser(description="Typora 本机激活脚本(研究用)") ap.add_argument( "--typora-dir", default=str(DEFAULT_TYPORA), help=r'Typora 安装目录,例如 "E:\Program Files\Typora"', ) ap.add_argument( "--skip-fuse", action="store_true", help="不修改 Typora.exe 的 OnlyLoadAppFromAsar Fuse", ) ap.add_argument( "command", choices=["activate", "status", "restore"], help="activate | status | restore", ) args = ap.parse_args() typora_dir = Path(args.typora_dir) p = Paths(typora_dir) WORK.mkdir(parents=True, exist_ok=True) log("Typora 目录:", p.root) log("工作目录 :", WORK) if args.command == "activate": cmd_activate(p, skip_fuse=args.skip_fuse) elif args.command == "status": cmd_status(p) elif args.command == "restore": cmd_restore(p) if __name__ == "__main__": main() ``` 2. 执行命令: ```pwsh python .\typora_crack.py --typora-dir "D:\Program Files\Typora" {activate|status|restore} ``` **注: 记得取消自动更新** 最后修改:2026 年 08 月 04 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏
此处评论已关闭