Tern字幕翻译软件逆向记录

2020-12-22
#re

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

提取

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

npm -g install asar
asar e app.asar app

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

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

分析

在源代码里乱翻找到了js/webpak/src/config/index.js,好家伙,免费额度都直接写在这里了。

// 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文件,翻了一下人傻了,测试用的订单号写注释里可还行,试了一下真可以用。(就放一部分出来给你们馋馋)

// 一些测试用的订单号
// 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函数。

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,这里也记录一下。

如有侵权,联系删除。