去年,某个 markdown 编辑器停止免费试用,开始收费了。我一直使用着免费版本,但是,突然有一天,免费版也不能用了,,,,, 其实,不激活也能使用,不过每次启动都有一个激活弹窗,就暂时用了几天,发现丢了一些笔记,也不明白什么情况,但是,,新手上路,啥都想研究一下。 先上研究成果 Screenshot 2022-04-01 001049.png (100.5 KB, 下载次数: 0) 下载附件 2022-4-1 00:11 上传 代码经过混淆,又没有经验,分析了好几天,,,, 详情看置顶帖 编辑器, 几天
所需文件链接在最后[捂嘴笑] 成品,,, 嗯,自己动手,丰衣足食 打开软件目录,一看这个结构就是 electron ,app.asar ,研究如何解包的时候,很不巧,github 有一个 patch typora 的项目,写好了解包的脚本,拿来主义,直接用好了。 但是,发现这个项目,并不发布补丁,也不对破解提供支持。还是得自己动手。 使用该脚本将文件解包,得到 atom.js ,我们将修改这个文件,达到 patch 的目的。 代码经过压缩,分析过程较为冗长,就不细说。但是,作为保姆级教程,每一步操作,我都写出来。# 1. 解压 app.asar 使用 github 的脚本,地址https://github.com/Mas0nShi/typoraCracker pip install -r requirments.txt # 安装依赖 python typora.py -f [安装目录,app.asar 所在路径] ./out ]然后,你会在当前目录得到一个 out/dec_app 文件夹,里面有 atom.js . patch 先进行字符串搜索,进行校验的地方,在源码中搜索 activate 字符串,搜索到 7 处,其他 5 处 都是字符串常量,其中两处较为明显,属于校验逻辑, 分别是激活和取消激活,其中一处代码如下 根据此处代码上下文中的字符串很容易确定,此处属于 校验逻辑,看到 await R("api/client/activate", t, !0); 一个异步函数,感觉像是一个封装的网络请求,跟进代码看一下,找到函数实现 R = async (t, n, o) => { ee(), console.log(`request ${E}/` + t); const i = a(225).post; try { var r = await i(E + "/" + t, n, { timeout: 3e4, headers: { "Cache-Control": "no-cache" } }); return r } catch (e) { if (console.warn(e.stack), e.response) throw e; if (o && "zh-Hans" == s.setting.getUserLocale() && !s.setting.get("useMirrorInCN")) { o = (await h.dialog.showMessageBox(null, { message: "链接服务器失败,使用尝试访问国内域名进行激活?", buttons: ["确认", "取消"] }))["response"]; if (console.log("click " + o), o) throw e; return s.setting.put("useMirrorInCN", !0), R(t, n, !1) } if (!s.setting.get("useMirrorInCN")) throw e; o = e; try { console.log("request to typora.com.cn"), r = await i("https://typora.com.cn/store/" + t, n) } catch (e) { throw console.warn(e.stack), o } } } 十分明显,这就是一个网络请求校验激活的过程 所以,修改逻辑很简单咯,直接拦截这个网络请求,返回一个假数据咯。(闲话) o 是返回数据,只要伪造一个 o 就好了。 (本来不知道,伪造的数据是什么,之前浏览代码的时候,发现一串密钥,想必是非对称加密的数据,于是,想摸清楚请求数据之后,在研究数据解密,忙就放了几天)但是,后来,发现 github 仓库,有人上传了一份 license.js ,本着,先用上再说的精神,我直接把代码拿来观摩,好家伙,难度直线下降。 以下数据结构来自 license.js 不过,它是 1.02 版本的,我的是 1.1.5 版本,里面需要多加一个 type 参数(不过,其实影响不大,后面写注册表的时候,可以直接删除校验代码,实际上,date 也很多余); 主要修改以下地方 // “var o = await R("api/client/activate", t, !0);” // 替换为 const o = { data: { code: 0, msg: Buffer.from(JSON.stringify({ deviceId: t.u, fingerprint: t.f, email: t.email, license: t.license, version: t.v, type: t.type, date: (new Date).toLocaleDateString("en-US"), }), "utf-8").toString("base64") } }; ~~将代码打包运行测试,发现,提示 许可证不可用 这玩意,我也没法动态调试,于是,自己加了个土断点,,,,直接加一个文件写出,,,,,~~ 经过多次尝试,确认问题出在 if (JSON.stringify(o.data), console.log("[License] response code is " + o.data.code), o.data.code == D.SUCCESS) return await Y(o.data.msg) ? [!0, ""] : [!1, "Please input a valid license code"]; 中的 Y()函数, 这句代码操作是,检验服务器返回的状态码,如果成功,则执行 返回Y(o.data.msg),否则,返回许可证非法 看一下 Y() 的函数实现 async function Y(e) { try { var { fingerprint: t, email: n, license: o, type: i } = I(e) || {}; return t == await M() && n && o ? (H(n, o, i), d().put("SLicense", e + "#0#" + (new Date).toLocaleDateString("en-US")), l = !0) : (console.log("[License] validate server return fail"), V(), !1) } catch (e) { throw console.error(e.stack), new Error("WriteActivationInfoFail") } } 4月1日,凌晨,昨天睡晚了,累得慌,明日在写吧 4.1 睡醒了 这段代码是将传入参数e进行解码,然后解析参数,如果参数齐全,则将注册信息写入注册表。 开始的时候,写注册表一直失败,后来,意识到,传入参数 e 是 base64 编码过的,此处 e 经过函数 I() 解码 (因为传入参数,是来自 license.js 所以,忘了这茬,写半天,重启之后,授权消失)开始以为是注册表写出错了,检查写注册表的函数 ,就是那个 H()很久,后来,发现没有问题。 看一下 I() 的函数实现 const I = e => { if (!e) return e; var t; try { t = Buffer.from(e, "base64"); const n = a(289).publicDecrypt(`-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nVoGCHqIMJyqgALEUrc 5JJhap0+HtJqzPE04pz4y+nrOmY7/12f3HvZyyoRsxKdXTZbO0wEHFIh0cRqsuaJ PyaOOPbA0BsalofIAY3mRhQQ3vSf+rn3g+w0S+udWmKV9DnmJlpWqizFajU4T/E4 5ZgMNcXt3E1ips32rdbTR0Nnen9PVITvrbJ3l6CI2BFBImZQZ2P8N+LsqfJsqyVV wDkt3mHAVxV7FZbfYWG+8FDSuKQHaCmvgAtChx9hwl3J6RekkqDVa6GIV13D23LS qdk0Jb521wFJi/V6QAK6SLBiby5gYN6zQQ5RQpjXtR53MwzTdiAzGEuKdOtrY2Me DwIDAQAB -----END PUBLIC KEY----- `, t); return JSON.parse(n.toString("utf8")) } catch (e) { return null } }, T = function() { var e = Array.from(arguments); const t = a(289).createHash("sha256"); return e.forEach(e => { t.update(e) }), t.digest("base64") }, W = () => { const e = d().get("SLicense"); if (!e) return null; var [t, n, o] = e.split("#"), t = I(t); return t && t.fingerprint == i ? (Object.assign(t, { failCounts: n, lastRetry: new Date(o) }), t) : null }, _ = async e => { console.log("writeInstallDate fromBTime=" + e); var t = new Date; if (e) try { var n = await a(728).stat(s.getPath("userData") + "/profile.data"), t = new Date(n.birthtime); n.birthtime } catch (e) {} e = (u = t).toLocaleDateString("en-US"); return d().put("IDate", e), u }; 此处验证了开始的猜想,返回的数据,经过一次非对称加密(私钥加密,公钥解密?emmmm,看上去像签名,但是,他确实是解密),收到返回消息以后,此处需进行一次解密,但是但,可是可 我们刚才写返回数据的时候,直接写了一串base64编码,所以,此处使用公钥publicDecrypt出来的数据,是错的。 很简单,直接修改 这些 过程通通不要,在函数开头,直接返回就好了,将I()的函数实现改成这样 const I = e => { if (!e) return e; return JSON.parse(Buffer(e, 'base64').toString()); // 后面的代码,可以全部不要了。 var t; try { t = Buffer.from(e, "base64"); const n = a(289).publicDecrypt(`-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nVoGCHqIMJyqgALEUrc 5JJhap0+HtJqzPE04pz4y+nrOmY7/12f3HvZyyoRsxKdXTZbO0wEHFIh0cRqsuaJ PyaOOPbA0BsalofIAY3mRhQQ3vSf+rn3g+w0S+udWmKV9DnmJlpWqizFajU4T/E4 5ZgMNcXt3E1ips32rdbTR0Nnen9PVITvrbJ3l6CI2BFBImZQZ2P8N+LsqfJsqyVV wDkt3mHAVxV7FZbfYWG+8FDSuKQHaCmvgAtChx9hwl3J6RekkqDVa6GIV13D23LS qdk0Jb521wFJi/V6QAK6SLBiby5gYN6zQQ5RQpjXtR53MwzTdiAzGEuKdOtrY2Me DwIDAQAB -----END PUBLIC KEY----- `, t); return JSON.parse(n.toString("utf8")) } catch (e) { return null } }, T = function() { var e = Array.from(arguments); const t = a(289).createHash("sha256"); return e.forEach(e => { t.update(e) }), t.digest("base64") }, W = () => { const e = d().get("SLicense"); if (!e) return null; var [t, n, o] = e.split("#"), t = I(t); return t && t.fingerprint == i ? (Object.assign(t, { failCounts: n, lastRetry: new Date(o) }), t) : null }, _ = async e => { console.log("writeInstallDate fromBTime=" + e); var t = new Date; if (e) try { var n = await a(728).stat(s.getPath("userData") + "/profile.data"), t = new Date(n.birthtime); n.birthtime } catch (e) {} e = (u = t).toLocaleDateString("en-US"); return d().put("IDate", e), u }; 此时,就可以发现,随便填一个邮箱就注册成功了,但是问题来了,在重启软件之后,注册状态就消失了,需要重新激活。 开始以为是每次启动软件时,都会重新联网校验激活状态,想必,联网需要使用那个封装的 请求 函数,也需要 URL,所以在代码里搜索了一阵,但是,只发现两处,网络请求的操作,一处是激活,一处是取消激活。转而去查看注册表,发现一个问题,在注册以后,注册表成功写入了注册信息,但是,在重启软件以后,刷新注册表,发现注册表被覆盖了。 不管是什么原因,如果能阻止他覆盖注册表......嗯... 在开始的查找中,找到一处取消激活的代码,实现如下 function V(e) { l || (e = ""), c = x = "", l = !1, d().put("SLicense", ""), e && $(p.getPanelString("Typora is now deactivated"), p.getPanelString(e)), ae() } 直接清空,但是,发现,并不起作用。 我想,在注册过程中,注册日期始终是 new date,但是,如果他判断许可日期的方式,很可能不是注册日期加一个固定的时间,那么这个日期会在哪里写入呢,在注册表,看到写入的信息结构是这样的 licenses # date 于是尝试手动修改注册表日期,发现,重启后,激活状态仍在,到此,算是全部结束了 大意了,在刚才写注册表的地方,就是那个 Y()函数,修改代码如下 async function Y(e) { try { var { fingerprint: t, email: n, license: o, type: i } = I(e) || {}; return t == await M() && n && o ? (H(n, o, i), d().put("SLicense", e + "#0#" + (new Date(2055,1,1)).toLocaleDateString("en-US")), l = !0) : (console.log("[License] validate server return fail"), V(), !1) } catch (e) { throw console.error(e.stack), new Error("WriteActivationInfoFail") } } 到此,patch 全部完成,将代码重新打包,复制回去,完美 总结一下 解包 pip install -r requirments.txt # 安装依赖 python typora.py -f [安装目录,app.asar 所在路径] ./out 篡改请求 // “var o = await R("api/client/activate", t, !0);” // 替换为 const o = { data: { code: 0, msg: Buffer.from(JSON.stringify({ deviceId: t.u, fingerprint: t.f, email: t.email, license: t.license, version: t.v, type: t.type, date: (new Date).toLocaleDateString("en-US"), }), "utf-8").toString("base64") } }; 修改解密函数 const I = e => { if (!e) return e; return JSON.parse(Buffer(e, 'base64').toString()); }; 修改写注册表时的日期 async function Y(e) { try { var { fingerprint: t, email: n, license: o, type: i } = I(e) || {}; return t == await M() && n && o ? (H(n, o, i), d().put("SLicense", e + "#0#" + (new Date).toLocaleDateString("en-US")), l = !0) : (console.log("[License] validate server return fail"), V(), !1) } catch (e) { throw console.error(e.stack), new Error("WriteActivationInfoFail") } } 修改为 async function Y(e) { try { var { fingerprint: t, email: n, license: o, type: i } = I(e) || {}; return t == await M() && n && o ? (H(n, o, i), d().put("SLicense", e + "#0#" + (new Date(2055,1,1)).toLocaleDateString("en-US")), l = !0) : (console.log("[License] validate server return fail"), V(), !1) } catch (e) { throw console.error(e.stack), new Error("WriteActivationInfoFail") } } 修改取消激活时的联网请求 直接搜索注释一下代码 await R("api/client/deactivate", { license: c, l: e, sig: T(x, await M(), c) }, !1) 打包回去 python typora.py -f /out/dec_app/ ./pkg 把 pkg 目录下的 app.asar 复制到安装目录下,再次打开,随意输入邮箱和注册码,注册成功! 完美 你们想要的东西在这里 2510241 没有 CB ? 阿里云盘连接 直接点击运行安装即可。 https://www.aliyundrive.com/s/EhhrnXg3f3U 看帖回帖是一种美德 typora 1.1.5 Windows x64 安装包: https://download.typora.io/windows/typora-setup-x64-1.1.5.exe 我今天一看,上次打包的成品可能有误,,,,,所以我直接再补发一个吧 ~~链接:https://pan.baidu.com/s/1bTVdRXWjozyKkKVRzZpw1A ~~ ~~提取码:626w ~~ --来自百度网盘超级会员V3的分享 解压密码 j1sdaxi 使用激活码激活:JTVKNC-NQZRJN-GWR7SM-PH9SBT 完工,睡觉,