Typora破解复现

Typora破解复现

最近在逛论坛的时候看到有人写了一份Typora的破解复现,看了下破解过程感觉不是很难,又因为实验室招新活动开始,刚好手里缺技术演示的材料,于是便想自己着手看看这个一直在用的工具是怎么被破解的,记录一下破解过程

1. 找到app.asar文件解包得到License.js加密代码

根据火绒剑在系统日志里监控Typore的启动,找到可疑的asar文件,程序访问并读取了该文件,并启动了其中的main.node模块,疑似开发者对加密源代码的解密过程

image-20230916115115006

找到app.asar文件,根据日志显示,可以猜测这个类似压缩包,问Bing得知

asar是一种用于打包Electron应用程序源代码的文件格式,它可以让应用程序更容易分发和运行。asar文件的工作原理是将所有文件连接在一起,不进行压缩,但支持随机访问。asar文件可以使用asar命令行工具或者electron-packager等工具来创建或解压

根据提示内容进行解压, asar extract app.asar unpacked

得到unpacked文件,貌似找到关于许可证的有关文件–License.js,Winhex打开看一眼

image-20230916115804959

有种base64编码的感觉,难道只是base64编码加密?拖进CyberChef解密一下

image-20230916115944322

可能还有一层或几层加密

2. 从main.node文件找License.js的解密逻辑

根据火绒剑日志显示,在app.asar解包后运行了main.node模块,又根据前面License.js被加密了,可以推测main.node有可能运行时解密了License.js文件,先DIE看一眼main.node,DLL64位,IDA64打开,先不管其他的,shift+f12查找字符串

image-20230916120618916

疑似有base64解密过程,交叉引用找到引用处伪代码

image-20230916120721264

根据Bing得知

napi_get_named_property 是一个 Node-API 的函数,它的作用是从一个对象中获取一个指定名称的属性,并返回它的值。

1
napi_status napi_call_function(napi_env env, napi_value recv, napi_value func, size_t argc, const napi_value* argv, napi_value* result);

其中,参数的含义是:

  • env: 当前调用 N-API 的环境。
  • recv: 要作为函数接收者的对象,通常是 this 的值。
  • func: 要调用的函数对象。
  • argc: 要传递的参数的个数。
  • argv: 要传递的参数的数组。
  • result: 用于接收函数返回值的指针。

分析得知,这段代码类似给v40进行解码,v40 = base64.from(v38),v38又来自函数的第三个参数a3+8,猜测这个a3+8指向的就是密文的地址,动调验证下,x64dbg直接拖Typora,手动下一个main.node的断点,再根据IDA中给出的偏移计算出调试器里解密函数的地址,根据64位下寄存器默认传参顺序是ECX,EDX,R8,R9知道a3的值储存在R8中,所以R8+8指向密文地址,验证:

image-20230923112047338

image-20230923112119409

image-20230923112137343

这里的数据刚好对应的是atom.js里的密文

image-20230923112252136

验证成功,继续分析

image-20230916133915462

找到类似加密算法的密钥初始化,用findcrypt看一眼发现了AES的S盒和逆S盒,推测这里应该是AES的密钥,而且长度为32个字节,猜测是256位模式

image-20230916134109117

接着找AES解密线索,找到F4E0偏移处函数

image-20230916134740069

里面有S盒和异或的继续分析,确定下ECB模式还是CBC模式

image-20230916140550963

进入3A8F偏移处函数分析,在这里先猜测block应该是密文(void 指针数组)

image-20230916141555652

函数里面又出现了一个循环异或,可以肯定这是个CBC模式下的异或偏移量了,那偏移量又是怎么来的呢,追踪a2(密文地址)进行判断的话,函数里面是复制了一份a2给v3,也就是说v3可能指向了密文

image-20230916142530938

那么result就是偏移量了,偏移量来自密文地址+v5处,进行16次循环,也就是说偏移量可能来自密文的0-15个字节位?

3. CyberChef验证猜测

依据之前的猜测,main.node通过base64解码,再AES,CBC模式解码,密钥IDA动调得到

0x4E, 0xE1, 0xB3, 0x82, 0x94, 0x9A, 0x02, 0x4B, 0x80, 0x2F, 0x52, 0xB4, 0xB4, 0xFE, 0x57, 0xF1, 0xBE, 0xF4, 0x08, 0x53, 0x10, 0x92, 0x56, 0xE2, 0xC2, 0x0D, 0xEC, 0xA3, 0xDD, 0x8D, 0xD5, 0x6D

偏移量为密文的前16个字节,带入CyberChef检验

image-20230916151516731

这下应该是得到了License.js源码,下载得到文件,找在线js美化工具美化

4. 最终的Patch

简单审计了下代码,不懂的找Bing问就是。首先应该patch掉网络验证类似request的东西,找到了几处

patch后的License.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
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
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
LL50Du2bPNkYky7Grequire("Env.js");
var Dict = require("Dict.js"),
electron = require("electron"),
shell = electron.shell,
app = electron.app,
ipc = electron.ipcMain,
BrowserWindow = electron.BrowserWindow,
isWin = "win32" == process.platform,
isMac = "darwin" == process.platform,
isLinux = "linux" == process.platform,
WindowController = require("WindowController.js"),
Raven = require("raven"),
errorShown = !1;
const mustRequire = function(e) {
try {
return require(e)
} catch (e) {
if (errorShown) return;
errorShown = !0;
var n = e.message;
setTimeout(() => {
errorShown = !1, dialog = require("electron").dialog, dialog.showMessageBox(null, {
type: "error",
buttons: ["OK"],
defaultId: 0,
cancelId: 0,
title: "A required module cannot be loaded by Typora",
message: n.split("\n")[0] + "\n\nPlease check if that file exists or reinstall Typora to fix."
}).then(({
response: e
}) => {
process.exit(1)
})
}, 1500)
}
};
var installDate, lastShown, hasLicense = null,
email = "",
licenseCode = "",
fingerPrint = "";
const ActiveResponseCode = {
SUCCESS: 0,
OUT_OF_LIMIT: 1,
INVALIDATE: -1,
WRONG_USER: -2
},
PUB_KEY = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nVoGCHqIMJyqgALEUrc\n5JJhap0+HtJqzPE04pz4y+nrOmY7/12f3HvZyyoRsxKdXTZbO0wEHFIh0cRqsuaJ\nPyaOOPbA0BsalofIAY3mRhQQ3vSf+rn3g+w0S+udWmKV9DnmJlpWqizFajU4T/E4\n5ZgMNcXt3E1ips32rdbTR0Nnen9PVITvrbJ3l6CI2BFBImZQZ2P8N+LsqfJsqyVV\nwDkt3mHAVxV7FZbfYWG+8FDSuKQHaCmvgAtChx9hwl3J6RekkqDVa6GIV13D23LS\nqdk0Jb521wFJi/V6QAK6SLBiby5gYN6zQQ5RQpjXtR53MwzTdiAzGEuKdOtrY2Me\nDwIDAQAB\n-----END PUBLIC KEY-----\n\n",
DAY_IN_MS = 864e5;
var HOST = "https://store.typora.io";
const decrypt = e => {return JSON.parse(Buffer.from(e, "base64").toString("utf8"))},
makeHash = function() {
var e = Array.from(arguments);
const n = require("crypto").createHash("sha256");
return e.forEach(e => {
n.update(e)
}), n.digest("base64")
},
readLicenseInfo = () => {
const e = getLicenseLocalStore().get("SLicense");
if (!e) return null;
const [n, t, i] = e.split("#"), a = decrypt(n);
return a.fingerprint != fingerPrint ? null : (Object.assign(a, {
failCounts: t,
lastRetry: new Date(i)
}), a)
},
writeInstallDate = async e => {
console.log(`writeInstallDate fromBTime=${e}`);
var n = new Date;
if (e) try {
var t = await require("fs-extra").stat(app.getPath("userData") + "/profile.data");
n = new Date(t.birthtime), t.birthtime
} catch (e) {}
installDate = n;
const i = n.toLocaleDateString("en-US");
return getLicenseLocalStore().put("IDate", i), installDate
};
var licenseLocalStoreInstance = null;
const getLicenseLocalStore = function() {
if (null == licenseLocalStoreInstance)
if (isWin) licenseLocalStoreInstance = WindowsLicenseLocalStore();
else {
var e = app.setting.prepDatabase(fingerPrint);
licenseLocalStoreInstance = {
put: function(n, t) {
console.log(`ls put ${n}`), e.getState()[n] = t, e.write()
},
get: function(n) {
return e.getState()[n]
}
}
}
return licenseLocalStoreInstance
};

function WindowsLicenseLocalStore() {
const e = mustRequire("native-reg");
return {
get: function(n) {
const t = e.openKey(e.HKCU, "Software\\Typora", e.Access.READ);
if (null == t) return "";
const i = e.getValue(t, null, n);
return e.closeKey(t), i
},
put: function(n, t) {
const i = e.createKey(e.HKCU, "Software\\Typora", e.Access.WRITE);
e.setValueSZ(i, n, t), e.closeKey(i)
}
}
}
const getFingerPrint = async() => {
if (!fingerPrint) {
if (isWin) {
const e = mustRequire("native-reg"),
n = e.openKey(e.HKEY.LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", e.Access.WOW64_64KEY | e.Access.READ);
fingerPrint = e.getValue(n, null, "MachineGuid"), e.closeKey(n)
} else fingerPrint = await require("node-machine-id").machineId({
original: !0
});
fingerPrint || Raven.captureMessage("[License] Failed to get fingerPrint"), fingerPrint = makeHash(fingerPrint, "typora").substr(0, 10).replace(/[/=+-]/g, "a"), isMac && (fingerPrint += "darwin")
}
return fingerPrint
};
var licenseInitialed = !1;
const watchLicense = async e => {
console.log("[watchLicense]"), firstValidateLicense(e), await validateTrail(), console.log(`[watchLicense] hasLicense: ${hasLicense}`), showLicensePanelIfNeeded(), addToAnalysis()
},
addToAnalysis = () => {
var e = Raven.getContext().tags;
e.hasLicense = hasLicense, Raven.mergeContext(e)
},
getOS = () => process.platform.replace(/\d+/, ""),
renewLicense = async(e, n) => {
const t = (new Date).toLocaleDateString("en-US");
const {
deviceId: i,
lastRetry: a
} = e || {};
if (!n && new Date - a < 432e5) return;
const s = {
v: getOS() + "|" + app.getVersion(),
license: licenseCode,
l: i,
u: app.setting.generateUUID(),
type: global.devVersion ? "dev" : ""
};
JSON.stringify(s);
},
getInstallDate = e => {
var n = new Date(getLicenseLocalStore().get("IDate"));
if (isNaN(n.getTime())) return e ? null : new Date;
if (e) return n;
var t = 1641734774922;
return isNaN(t) ? t = new Date("2021-10-01") : (t = new Date(t), isNaN(t.getTime()) && (t = new Date("2021-10-01"))), n < t ? t : n
},
getTrailRemains = (e, n) => {
n = n || 15;
var t = Math.floor((new Date - installDate) / 864e5),
i = Math.max(0, n - t);
return e && (i > n || isNaN(i)) && (i = n), i
},
validateTrail = async() => {
var e = (installDate = getInstallDate(!global.devVersion)) ? getTrailRemains(!1) : 100;
(e > 15 || isNaN(e)) && (console.log("[validateTrail] Read from incorrupted InstallDate"), await writeInstallDate(!0), e = 15), console.log(`[validateTrail] installDate is ${installDate.toLocaleDateString("en-US")}, trail remains: ${e} days`)
};

function fillLicense(e, n) {
licenseCode = n, (hasLicense = !(!(email = e) || !licenseCode)) && onFillLicense()
}

function unfillLicense(e) {
hasLicense || (e = ""), email = "", licenseCode = "", hasLicense = !1, getLicenseLocalStore().put("SLicense", ""), e && showDialog(Dict.getPanelString("Typora is now deactivated"), Dict.getPanelString(e)), onUnfillLicense()
}
const firstValidateLicense = e => {
console.log("[License] firstValidateLicense"), licenseInitialed = !0;
const n = readLicenseInfo(),
{
license: t,
email: i
} = n || {};
t && i ? (fillLicense(i, t), renewLicense(n, e), console.log("[License] pass validateLicenseInfoStr")) : unfillLicense()
},
showDialog = (e, n) => electron.dialog.showMessageBox(null, {
type: "error",
buttons: ["OK"],
defaultId: 0,
cancelId: 0,
title: e,
message: n
}),
endDevTest = function() {
app.expired = !0, showDialog(Dict.getPanelString("Error"), Dict.getPanelString("This beta version of Typora is expired, please download and install a newer version.")).then(() => {
shell.openExternal("https://typora.io/#download"), setTimeout(() => {
process.exit(1)
}, 1e3)
})
},
validateDevTest = function() {
if (!hasLicense && !isLinux && global.devVersion && global.PRODUCTION_MODE) {
var e = getInstallDate(),
n = new Date;
console.log("buildTime is 1641734774922"), (isNaN(1641734774922) || n - 1641734774922 > 20736e6) && endDevTest(), e -= 0, console.log("verInitTime is " + e), !isNaN(e) && n - e > 1728e7 && endDevTest()
}
},
showLicensePanelIfNeeded = function() {
shouldShowNoLicenseHint(!0) && !app.setting.inFirstShow && (isLinux && Math.random() < .95 || (!lastShown || new Date - lastShown > 36e5 * (isLinux ? 4 : 2) || getTrailRemains(!0, 20) <= 0) && showLicensePanel())
};
var licensePanel = null;
const showLicensePanel = async function(e) {
if (lastShown = new Date, null == licensePanel) return (licensePanel = WindowController.showPanelWindow({
width: 525,
height: 420,
path: `page-dist/license.html?dayRemains=${getTrailRemains(!0)}&index=${e?1:0}\n\t\t\t\t&hasActivated=${hasLicense||!1}&email=${email}&license=${licenseCode}&lang=${app.setting.getUserLocale()}&needLicense=${shouldShowNoLicenseHint()}`,
frame: !1,
alwaysOnTop: !errorShown
})).on("closed", function() {
licensePanel = null
}), void setTimeout(() => {
licensePanel && !licensePanel.isDestroyed() && licensePanel.setAlwaysOnTop(!1)
}, 5e3);
licensePanel.focus()
};
var welcomePanel = null;
const showWelcomePanel = async function() {
if (lastShown = new Date, null == welcomePanel) return (welcomePanel = WindowController.showPanelWindow({
width: 760,
height: 460,
path: `page-dist/welcome.html?lang=${app.setting.getUserLocale()}`,
frame: !1,
alwaysOnTop: !errorShown
})).on("closed", function() {
welcomePanel = null
}), void setTimeout(() => {
welcomePanel && !welcomePanel.isDestroyed() && welcomePanel.setAlwaysOnTop(!1)
}, 4e3);
welcomePanel.focus()
},
quickValidate = e => {
const n = "L23456789ABCDEFGHJKMNPQRSTUVWXYZ";
if (!/^([A-Z0-9]{6}-){3}[A-Z0-9]{6}$/.exec(e)) return !1;
var t = e.replace(/-/g, ""),
i = t.substr(22);
return !t.replace(/[L23456789ABCDEFGHJKMNPQRSTUVWXYZ]/g, "") && i == (e => {
for (var t = "", i = 0; i < 2; i++) {
for (var a = 0, s = 0; s < 16; s += 2) a += n.indexOf(e[i + s]);
t += n[a %= n.length]
}
return t
})(t)
},
getComputerName = async function() {
var e = process.env.USER;
switch (e || (e = require("os").userInfo().username), process.platform) {
case "win32":
return process.env.COMPUTERNAME + " | " + e + " | Windows";
case "darwin":
return new Promise(n => {
require("child_process").exec("scutil --get ComputerName", {
timeout: 5e3
}, (t, i) => {
n(!t && i ? i.toString().trim() + " | " + e + " | darwin" : require("os").hostname() + " | " + e + " | darwin")
})
});
default:
return require("os").hostname() + " | " + e + " | Linux"
}
},
doActivation = async function(e, n, t) {
if (e = (e || "").replace(/^[\s\u200b ]/g, "").replace(/[\s\u200b ]$/g, ""), n = (n || "").replace(/^[\s\u200b ]/g, "").replace(/[\s\u200b ]$/g, ""), ! function(e) {
return /^[^\s@'"/\\=?]+@[^\s@'"/\\]+\.[^\s@'"/\\]+$/.test(e)
}(e)) return [!1, "Please input a valid email address"];
//if (!quickValidate(n)) return [!1, "Please input a valid license code"];
const i = {
v: getOS() + "|" + app.getVersion(),
license: n,
email: e,
l: await getComputerName(),
f: await getFingerPrint(),
u: app.setting.generateUUID(),
type: global.devVersion ? "dev" : "",
force: t
};
JSON.stringify(i);
try {
const e = {
data: {
code: 0,
msg: Buffer.from(JSON.stringify(
{
deviceId: i.u,
fingerprint: i.f,
email: i.email,
license: i.license,
version: i.v,
date: 1

}),"utf8").toString("base64")
}
}
if (JSON.stringify(e.data), console.log(`[License] response code is ${e.data.code}`), e.data.code == ActiveResponseCode.SUCCESS) return await writeActivationInfo(e.data.msg) ? [!0, ""] : [!1, "Please input a valid license code"];
if (e.data.code == ActiveResponseCode.OUT_OF_LIMIT) return t ? await writeActivationInfo(e.data.msg) ? [!0, "Your license has exceeded the max devices numbers.\nThe oldest device was unregistered automatically."] : [!1, "Please input a valid license code"] : ["confirm", 'Your license has exceeded the max devices numbers.\nIf you click "Continue Activation", this device will be activated and the oldest device will be unregistered automatically.'];
if (e.data.code == ActiveResponseCode.INVALIDATE) return [!1, "Please input a valid license code"];
if (e.data.code == ActiveResponseCode.WRONG_USER) return [!1, "This license code has been used with a different email address."]
} catch (e) {
return e.response && e.response.code ? (console.warn(`[License] error from server ${e.response.code}`), [!1, "Unknown Error. Please contact hi@typora.io"]) : (Raven.captureException(e, {
level: "warning"
}), console.error(e.stack), [!1, "Failed to access the license server. Please check your network or try again later."])
}
},
writeActivationInfo = async function(e) {
const n = decrypt(e) || {},
{
deviceId: t,
fingerprint: i,
email: a,
license: s,
version: o,
date: r
} = n;
fillLicense(a, s);
getLicenseLocalStore().put("SLicense", `${e}#0#${(new Date).toLocaleDateString("en-US")}`);
hasLicense = !0;
return 1;
//return i == await getFingerPrint() && a && s ? (fillLicense(a, s), getLicenseLocalStore().put("SLicense", `${e}#0#${(new Date).toLocaleDateString("en-US")}`), hasLicense = !0, !0) : (console.log("[License] validate server return fail"), unfillLicense(), !1)
},
doDeactivation = async() => {
hasLicense && email && licenseCode || console.error("doDeactivation on unregistered device");
const {
deviceId: e
} = readLicenseInfo() || {};
unfillLicense()
};
ipc.handle("addLicense", async(e, {
email: n,
license: t,
force: i
}) => {
console.log("handle addLicense");
try {
return await doActivation(n, t, i)
} catch (e) {
console.error(e.stack)
}
}), ipc.handle("license.show", (e, n) => {
showLicensePanel(n || !1)
}), ipc.handle("license.show.debug", () => {
licensePanel && licensePanel.webContents.openDevTools()
}), ipc.handle("removeLicense", async e => {
console.log("handle removeLicense");
try {
return await doDeactivation()
} catch (e) {
console.error(e.stack)
}
});
start = async(e, n) => {
console.log(`start LM in devVersion=${global.devVersion||!1}`);
try {
await getFingerPrint(), !e && n || isLinux || (global.devVersion || !n || n.indexOf("dev") > -1) && (console.log("re-write InstallDate"), await writeInstallDate()), validateDevTest(), watchLicense(e)
} catch (e) {
Raven.captureException(e)
}
},
shouldShowNoLicenseHint = e => !hasLicense && (e || !isLinux) && !global.devVersion,
getHasLicense = () => hasLicense,
appendLicenseHintIfNeeded = e => {
licenseInitialed && shouldShowNoLicenseHint() && onUnfillLicense(e)
};

function genClassName() {
var e = (new Date).getTime();
return "txxxx-xxxx-xxxxy".replace(/[x]/g, function(n) {
var t = (e + 16 * Math.random()) % 16 | 0;
return e = Math.floor(e / 16), t.toString(16)
})
}
const className = genClassName(),
onFillLicense = () => {
BrowserWindow.getAllWindows().forEach(e => {
e.webContents.executeJavaScript(`try{document.querySelector(".${className}").remove();}catch(e){};File.option && (File.option.hasLicense = true);File.megaMenu && File.megaMenu.forceReload();0;`)
})
},
onUnfillLicense = async e => {
if (isLinux || global.devVersion) return;
await Dict.init();
const n = `.typora-sourceview-on .${className}{\n\t\tdisplay:none;\n\t}\n\t.${className} {\n\t\tposition: fixed;\n bottom: 2px;\n z-index: 9999;\n left: 70px;\n font-size: 12px;\n line-height: 24px;\n background: rgb(120 120 120 / 30%);\n padding: 0 12px;\n color: var(--text-color);\n border-radius: 4px;\n cursor: pointer;\n\t}\n\t.pin-outline .${className}{\n\t\tleft:calc(var(--sidebar-width) + 70px);\n\t}`,
t = `if(window.File.option){\n\t\tFile.option.hasLicense = false; \n\t\tFile.megaMenu && File.megaMenu.forceReload();\n\t\tif(!document.querySelector(".${className}")) {\n\t\t\tconst pos = Math.round(Math.random() * document.body.children.length);\n\t\t\tconst dom = document.createElement("DIV");\n\t\t\tdom.innerText = "${Dict.getPanelString("UNREGISTERED")} ×";\n\t\t\tdom.classList.add("${className}");\n\t\t\tdom.style = "position: fixed !important;bottom: 2px !important; display: block !important; opacity: 1 !important; height: auto !important; width: auto !important; z-index: 9999 !important;"\n\t\t\tdom.setAttribute("role", "button");\n\t\t\tdom.addEventListener("click", () => {\n\t\t\t\tdom.remove();\n\t\t\t\treqnode("electron").ipcRenderer.invoke("license.show");\n\t\t\t});\n\t\t\tdocument.body.insertBefore(dom, document.body.children[pos]);\n\t\t}\n\t};1;`;

function i(e) {
e.webContents.insertCSS(n), e.webContents.executeJavaScript(t)
}
e ? i(e) : BrowserWindow.getAllWindows().forEach(i)
};
exports.shouldShowNoLicenseHint = shouldShowNoLicenseHint, exports.start = start, exports.showLicensePanel = showLicensePanel, exports.showWelcomePanel = showWelcomePanel, exports.appendLicenseHintIfNeeded = appendLicenseHintIfNeeded, exports.getHasLicense = getHasLicense, exports.showLicensePanelIfNeeded = showLicensePanelIfNeeded;

patch掉网络验证,对doActivation的验证直接改成永恒真,使得邮箱和序列号随便填即可激活,再改写hasLicense的值,使得下次登录时访问许可证信息时得到软件已被激活的信息,再打包生成app.asar,打开软件随便输入序列号和邮箱即可

5. 破解成功