大喜 发表于 2022-4-1 00:16

某收费 Markdown 编辑器的破解分析

本帖最后由 大喜 于 2022-4-1 00:25 编辑

去年,某个 markdown 编辑器停止免费试用,开始收费了。我一直使用着免费版本,但是,突然有一天,免费版也不能用了,,,,,
其实,不激活也能使用,不过每次启动都有一个激活弹窗,就暂时用了几天,发现丢了一些笔记,也不明白什么情况,但是,,新手上路,啥都想研究一下。
先上研究成果


代码经过混淆,又没有经验,分析了好几天,,,,
详情看置顶帖

大喜 发表于 2022-4-1 00:25

本帖最后由 大喜 于 2022-6-16 22:16 编辑



# 所需文件链接在最后[捂嘴笑]

成品,,,

嗯,自己动手,丰衣足食

打开软件目录,一看这个结构就是 electron ,app.asar ,研究如何解包的时候,很不巧,github 有一个 patch typora 的项目,写好了解包的脚本,拿来主义,直接用好了。
但是,发现这个项目,并不发布补丁,也不对破解提供支持。还是得自己动手。

使用该脚本将文件解包,得到 atom.js ,我们将修改这个文件,达到 patch 的目的。
代码经过压缩,分析过程较为冗长,就不细说。但是,作为保姆级教程,每一步操作,我都写出来。#


# 1. 解压 app.asar
使用 github 的脚本,地址https://github.com/Mas0nShi/typoraCracker
```bash
pip install -r requirments.txt # 安装依赖
python typora.py -f [安装目录,app.asar 所在路径] ./out
```


]然后,你会在当前目录得到一个 out/dec_app 文件夹,里面有 atom.js

# . patch

先进行字符串搜索,进行校验的地方,在源码中搜索 activate 字符串,搜索到 7 处,其他 5 处 都是字符串常量,其中两处较为明显,属于校验逻辑, 分别是激活和取消激活,其中一处代码如下
![](https://attach.52pojie.cn//forum/202204/01/003230hni1zp1ahn33sann.png)


根据此处代码上下文中的字符串很容易确定,此处属于 校验逻辑,看到 await R("api/client/activate", t, !0); 一个异步函数,感觉像是一个封装的网络请求,跟进代码看一下,找到函数实现


```javascript

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 也很多余);

主要修改以下地方

```javascript
// “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")
    }
};

```

~~将代码打包运行测试,发现,提示 许可证不可用
这玩意,我也没法动态调试,于是,自己加了个土断点,,,,直接加一个文件写出,,,,,~~

经过多次尝试,确认问题出在
```javascript
if (JSON.stringify(o.data), console.log(" 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() 的函数实现

```javascript
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(" 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() 的函数实现

```javascript
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 = 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()的函数实现改成这样

```javascript

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 = 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,所以在代码里搜索了一阵,但是,只发现两处,网络请求的操作,一处是激活,一处是取消激活。转而去查看注册表,发现一个问题,在注册以后,注册表成功写入了注册信息,但是,在重启软件以后,刷新注册表,发现注册表被覆盖了。

不管是什么原因,如果能阻止他覆盖注册表......嗯...

在开始的查找中,找到一处取消激活的代码,实现如下

```javascript
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()函数,修改代码如下

```javascript
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(" validate server return fail"), V(), !1)
    } catch (e) {
      throw console.error(e.stack), new Error("WriteActivationInfoFail")
    }
}
```



到此,patch 全部完成,将代码重新打包,复制回去,完美

# 总结一下

## 解包

```bash
pip install -r requirments.txt # 安装依赖
python typora.py -f [安装目录,app.asar 所在路径] ./out
```

## 篡改请求

```javascript
// “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")
    }
};
```

## 修改解密函数

```javascript
const I = e => {
    if (!e) return e;
    return JSON.parse(Buffer(e, 'base64').toString());
      };
```

## 修改写注册表时的日期

```javascript
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(" validate server return fail"), V(), !1)
    } catch (e) {
      throw console.error(e.stack), new Error("WriteActivationInfoFail")
    }
}
```
修改为

```javascript
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(" validate server return fail"), V(), !1)
    } catch (e) {
      throw console.error(e.stack), new Error("WriteActivationInfoFail")
    }
}
```


## 修改取消激活时的联网请求

直接搜索注释一下代码

```javascript
await R("api/client/deactivate", {
    license: c,
    l: e,
    sig: T(x, await M(), c)
}, !1)
```

## 打包回去

```bash
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

完工,睡觉,

涛之雨 发表于 2022-4-1 19:07

本帖最后由 涛之雨 于 2022-6-15 18:05 编辑

|信息 | 状态 |
|:----:|:----:|
|🤔无效指令个数😒 | [!(https://img.shields.io/github/issues-closed-raw/taozhiyu/TyProAction/%F0%9F%A4%94invalid/%E6%97%A0%E6%95%88%E7%9A%84%F0%9F%98%92?color=d9534f&label=%F0%9F%A4%94invalid%20instructions&logo=github&style=flat-square)](https://github.com/taozhiyu/TyProAction/issues?q=label%3A%F0%9F%A4%94invalid%2F%E6%97%A0%E6%95%88%E7%9A%84%F0%9F%98%92) |
|🎉成功生成个数🎉 | [!(https://img.shields.io/github/issues-closed-raw/taozhiyu/TyProAction/%E2%98%91%EF%B8%8Fkeygen/%E6%B3%A8%E5%86%8C%E6%9C%BA%F0%9F%8E%89?color=5cb85c&label=%F0%9F%8E%89successful%20builds&logo=github&style=flat-square)](https://github.com/taozhiyu/TyProAction/issues?q=label%3A%E2%98%91%EF%B8%8Fkeygen%2F%E6%B3%A8%E5%86%8C%E6%9C%BA%F0%9F%8E%89) |
|😏附件总下载量😙 |!(https://img.shields.io/github/downloads/taozhiyu/TyProAction/total?label=%F0%9F%A4%A9Total%20downloads&color=5319E7&logo=github&style=flat-square)|
|🔧自动更新状态⚙️ |!(https://img.shields.io/github/workflow/status/taozhiyu/TyProAction/check%20update?label=%F0%9F%94%A7check%20update&logo=github&style=flat-square) |
|😎最新稳定版本🥳 | [!(https://img.shields.io/github/v/release/taozhiyu/TyProAction?label=%F0%9F%A5%B3Latest%20version&logo=windows&style=flat-square)](https://github.com/taozhiyu/TyProAction/releases/latest) |
|🌈最新测试版本🔬 | [!(https://img.shields.io/github/v/release/taozhiyu/TyProAction?include_prereleases&label=%F0%9F%8C%88%20dev%20version&style=flat-square)](https://github.com/taozhiyu/TyProAction/releases) |




离线注册算法更简单,我用action写了个机器人注册机

~~https://taozhiyu.github.io/TyProAction/~~ 终于没了,自动提醒我更新。。。

仪式感嘛。。。

![](https://pic.rmb.bdstatic.com/bjh/552764cbd771d08765e696234a946fc7.gif)

~~直接使用可以用别的大佬的~~

~~别的大佬的库都没了~~

如果不出意外应该会继续更新,但是不提供注册机了(反正是github page一个页面)

AiniWang 发表于 2022-4-1 16:31

感谢分享,最新版支持poj吗

yawnwang 发表于 2022-4-1 16:34

谢谢分享

大喜 发表于 2022-4-1 17:18

AiniWang 发表于 2022-4-1 16:31
感谢分享,最新版支持poj吗

大意了,在我开始搞得时候,这个还是最新版,我不太更新软件

Lemon1900 发表于 2022-4-1 19:59

还得是大佬,前几天用github上的解压不出来license.js

xiaoyouy 发表于 2022-4-1 19:59

涛之雨 发表于 2022-4-1 19:07
离线注册更简单
我用action写了个机器人注册机
https://github.com/taozhiyu/TyProAction


感谢分享,6666

applelittle 发表于 2022-4-1 21:57

先存着了,感谢~

studywin 发表于 2022-4-1 22:51

谢谢楼主分享
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 某收费 Markdown 编辑器的破解分析