Tern(官网https://zh.tern.best)是一个字幕翻译的软件,他通过调用主流翻译平台的接口(需要自己注册)进行字幕翻译,说白了就是写了个gui。但是他免费版每月是限制字符数的。因此。。。

提取

这是一个eletron应用,外面就是一层chrome的壳,真正逻辑在resources/app.asar,用npm安装asar工具对其解压。

1
2
npm -g install asar
asar e app.asar app

随便翻了翻,可以看到主要逻辑在js/app.cca879ab.js里,本来要读这个打包出来文件非常头疼,但是这个软件竟然把sourcemap也打包了出来,那就直接用工具恢复源文件吧。这里用的是reverse-sourcemap,用起来很方便,只要reverse-sourcemap <目标文件>就行了。

恢复之后就可以愉快的分析代码了(甚至还有详细的注释)。

分析

在源代码里乱翻找到了js/webpak/src/config/index.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
// js/webpak/src/config/index.js
// 配置

const one_minutes = 60
const one_hours = 60 * one_minutes

exports.config = {
free_plan_monthly_char_limit: 1000000, // 字幕翻译: 每月免费额度 (单位: 字符数)
// 这是100万字符
free_plan_monthly_duration_limit_in_seconds: one_hours * 10, // 语音转文字: 每月免费时长 (单位: 秒)
mianbaoduo_lifetime: "YZ6Vmps=",
mianbaoduo_7_day: "YZ6Vmpk=",
mianbaoduo_14_day: "YpyTl5s=",
mianbaoduo_30_day: "YZ2Zmpc=",
mianbaoduo_30_day_repeat: "Y5ubmJY=", // 30天可复购的版本
mianbaoduo_60_day: "YZ2ZmpY=",
mianbaoduo_90_day: "YZ2YmZ8=",
mianbaoduo_120_day: "YpWWm5s=",
mianbaoduo_valid_urlkey: [
"YZ6Vmps=", "YZ6Vmpk=", "YpyTl5s=", "YZ2Zmpc=", "YZ2ZmpY=", "YZ2YmZ8=", "YpWWm5s=", "Y5ubmJY="
],
mianbaoduo_developer_key: "77567:1iwLac:JEhBXIb4hcI5fl9_OeE8N5F2aBY",
doc_root: 'https://doc.tern.1c7.me/',
doc_root_cn: 'https://doc.tern.1c7.me/zh/'
}

但光提高免费额度肯定是不够的(目标当然是无限额),下面又定义了一些base64编码的字符串,目前还不知道是做什么用的,但是看名字就知道肯定和付费有关,那就全局搜索这些字符串的键,然后找到了js/webpack/src/lib/mianbaoduo.js文件,翻了一下人傻了,测试用的订单号写注释里可还行,试了一下真可以用。(就放一部分出来给你们馋馋)

1
2
3
4
5
6
7
8
9
10
// 一些测试用的订单号
// f0030d99307cdc1d98d007a45fa611** // 7天
// 6a922475f64c2012cfb9bb4303c109** // 14天
// d8f3ed76a0d9706fecdbf13b415d98** // 30天
// 084c3ad39ac24f2c242ab493daa380** // 60天
// 3dd86e87e3f5dd41f39c71d4c11793** // 90天
// b32a367006c807271fc89861c082d6** // 120天
// c5c0960a5126058c0ab452763e405d** // 永久
// 1156121ab3602bf9576413b3c48d81** // 30天(可复购) 8月25号过期 (我自己买的测试版)
// 2bc7e9a6734f00fe75a58394881e86** // 30天, 复购了一次的版本, 第一次是7月11号过期,然后复购了一个月8月12号过期

这我肯定还是不满意,继续往下看http_order_detail函数是在检验激活码,再往上找,找到调用它的函数,在js/webpack/src/mixin_plan.js里面的validate_mianbaoduo_order_id函数。

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
async validate_mianbaoduo_order_id() {
var that = this;
this.license_verifying_state = this.state_dict.verifying;
var order_id = this.mianbaoduo_order_id;

var mian_bao_duo = new MianBaoDuo({
order_id: order_id,
developer_key: config.mianbaoduo_developer_key,
valid_urlkeys: config.mianbaoduo_valid_urlkey
});
await mian_bao_duo.get_order_detail();

// 如果是个无效订单
if (mian_bao_duo.order_is_valid() == false) {
that.license_verifying_state = that.state_dict.invalid;
that.set_plan_free();
that.remove_license_code();
return;
}

// 如果 urlkey 不在有效数组内, 这里就直接返回了,无需执行后续判断
if (mian_bao_duo.urlkey_is_valid() == false) {
that.license_verifying_state = that.state_dict.invalid;
that.set_plan_free();
that.remove_license_code();
return;
}

var response = mian_bao_duo.order_info; // 订单的结果
var urlkey = response.result.urlkey;

// 如果是永久激活码
if (mian_bao_duo.is_lifetime_license()) {
that.license_verifying_state = that.state_dict.valid;
that.set_plan_pro();
that.set_license_code(order_id);
return;
}

var order_time = response.result.ordertime; // 支付时间的时间戳 timestamp like 1580205634
var orderamount = response.result.orderamount; // 支付金额
that.set_pay_time(order_time);
that.set_orderamount(orderamount);

var order_time_readable = moment
.unix(order_time)
.format("YYYY-MM-DD HH:mm:ss");
// 2020-01-28 18:00:34

// 7天/14天/30/60/90/120天
var valid_for_x_days = null;

switch (urlkey) {
case config.mianbaoduo_7_day:
valid_for_x_days = 7;
break;
case config.mianbaoduo_14_day:
valid_for_x_days = 14;
break;
case config.mianbaoduo_30_day:
valid_for_x_days = 30;
break;
case config.mianbaoduo_60_day:
valid_for_x_days = 60;
break;
case config.mianbaoduo_90_day:
valid_for_x_days = 90;
break;
case config.mianbaoduo_120_day:
valid_for_x_days = 120;
break;
default:
// 除非是30天复购版, 否则不会到 default 这里
// 这个复购版我们在后面单独处理
break;
}

if (valid_for_x_days != null) {
// 如果已过期
if (
that.now_is_after_ordertime_plus_day(order_time, valid_for_x_days)
) {
that.license_verifying_state = that.state_dict.expired;
that.expire_message = that.$t("plan.license_expired", {
date: order_time_readable,
day: valid_for_x_days,
expired_date: this.get_expired_datetime(
order_time,
valid_for_x_days
).format("YYYY-MM-DD HH:mm:ss")
});
that.set_plan_free();
that.remove_license_code();
return;
} else {
that.set_order_type(`${valid_for_x_days}天`);
that.set_expire_time(order_time, valid_for_x_days);
that.set_plan_limited();
that.set_license_code(order_id);
}
}

// 30天可复购版
if (urlkey == config.mianbaoduo_30_day_repeat) {
var valid = response.result.state == "success";
var re_exists = response.result.re != undefined; // 这个 re 字段代表是复购的,里面有复购的信息
if (valid) {
that.set_order_type("30天 (可复购)"); // 显示给用户看的
that.set_plan_limited();
that.set_license_code(order_id);

// 如果是复购的,过期时间和购买时间从 re 里面拿
if (re_exists) {
var expire_timestamp = response.result.re.expire_at;
that.set_expire_time_by_timestamp(expire_timestamp);
var order_timestamp = response.result.re.ordertime;
that.set_pay_time(order_timestamp);
} else {
that.set_expire_time_by_timestamp(response.result.expire_at);
}
} else {
that.license_verifying_state = that.state_dict.expired;

var expire_timestamp = null;
if (re_exists) {
expire_timestamp = response.result.re.expire_at;
} else {
expire_timestamp = response.result.expire_at;
}

var expired_date = moment
.unix(expire_timestamp)
.format("YYYY-MM-DD HH:mm:ss");
that.expire_message = that.$t("plan.license_expired_just_date", {
expired_date: expired_date
});

that.set_plan_free();
that.remove_license_code();
}
}
},

这是完整的校验逻辑,直接把这个改掉(改那个打包过的文件),换成永久激活的逻辑就行了。再用asar重新打包一份,替换原来的app.asar就完成了。

其它

逆向过程中还搜到了一个恢复chrome生成的pak文件的工具chrome-pak-customizer,这里也记录一下。

如有侵权,联系删除。