引言
今天本英语渣用了下谋道翻译,惊讶地发现谋道返回的接口数据是加密的。想着我已经很久没碰逆向了,那就来研究一下吧,顺便水篇入门文。PS:整个过程没有用到动态调试。
作者:hans774882968以及hans774882968以及hans774882968
本文52pojie:www.52pojie.cn/thread-1769…
本文juejin:juejin.cn/post/721848…
本文CSDN:blog.csdn.net/hans7748829…
webtranslate接口返回加密数据的解密过程
- 谋道翻译webtranslate接口:dict.moudao.com/webtranslat…
- 谋道翻译webtranslate获取
secretKey
接口:dict.moudao.com/webtranslat…
抓包,找到附近代码:
nn["a"].getTextTranslateResult({
i: e.data.keyword,
from: e.data.from,
to: e.data.to,
...n,
dictResult: !0,
keyid: "webfanyi"
}, o).then(o=>{
nn["a"].cancelLastGpt();
const n = nn["a"].decodeData(o, an["a"].state.text.decodeKey, an["a"].state.text.decodeIv)
, a = n ? JSON.parse(n) : {};
console.log("解密后的接口数据:", a), // 谋道故意放水,直接给答案?
0 === a.code ? e.success && t(e.success)(a) : e.fail && t(e.fail)(a)
}
复制代码
o
就是接口加密数据,主要需要确定decodeKey, decodeIv
。因为某道翻译前端是webpack
打包的应用,所以可以这么找nn, an
的位置:
首先看到
var nn = o("8139")
, an = o("4360")
, sn = o("bc3a");
复制代码
打开Chrome Devtools的Search Tab,搜索8139
,很快找到(fanyi.moudao.com/js/app.e4e9…
8139: function(e, t, o) {
"use strict";
// ...
},
8393: //...
复制代码
于是可知decodeData
内容:
T = (t,o,n)=>{
if (!t)
return null;
const a = e.alloc(16, f(o))
, i = e.alloc(16, f(n))
, r = c.a.createDecipheriv("aes-128-cbc", a, i);
let s = r.update(t, "base64", "utf-8");
return s += r.final("utf-8"),
s
}
复制代码
不过decodeKey
和decodeIv
并不是很好定位……还是通过Chrome Devtools的Search Tab直接搜到的。
const i = {
secretKey: "",
dictResult: {},
decodeKey: "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl",
decodeIv: "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4",
allowStroke: !1,
showPjm: !1,
showRomanPronunciation: !1,
showWordsNumber: !0
}
复制代码
知道这两个变量的值以后,也可以马后炮地倒推一下4360
这个模块做的事:
4360: function(e, t, o) {
"use strict";
o("13d5");
var n = o("5502");
const a = []
, c = o("c653")
, i = c.keys().reduce((e,t)=>{ // c.keys()取出的是["./domain.js"]数组
const o = t.replace(/^\.\/(.*)\.\w+$/, "$1");
a.push(o);
const n = c(t);
return e[o] = n.default,
e
}
, {});
t["a"] = Object(n["a"])({
modules: i
})
},
复制代码
i
变量可以猜测是导出的模块,c = o("c653")
似乎在做模块整合。
c653: function(e, t, o) {
var n = {
"./domain.js": "d2a7",
"./language.js": "c083",
"./login.js": "b5ce",
"./text.js": "1a68"
};
function a(e) {
var t = c(e);
return o(t)
}
function c(e) {
if (!o.o(n, e)) {
var t = new Error("Cannot find module '" + e + "'");
throw t.code = "MODULE_NOT_FOUND",
t
}
return n[e]
}
a.keys = function() {
return Object.keys(n)
}
,
a.resolve = c,
e.exports = a,
a.id = "c653"
},
复制代码
看到./text.js
,结合an["a"].state.text.decodeKey
可以猜测1a68
就是关键模块。
"1a68": function(e, t, o) {
"use strict";
o.r(t);
var n = o("8139")
, a = o("8544")
, c = o("c34f");
const i = {
secretKey: "",
dictResult: {},
decodeKey: "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl",
decodeIv: "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4",
allowStroke: !1,
showPjm: !1,
showRomanPronunciation: !1,
showWordsNumber: !0
}
, r = {
secretKey: e=>e.secretKey,
dictResult: e=>e.dictResult
}
, s = {
fetchTextTranslateSecretKey: ({commit: e},t)=>{
const o = "webfanyi-key-getter"
, a = "asdjnjfenknafdfsdfsd";
n["a"].getTextTranslateSecretKey({
keyid: o
}, a).then(t=>{
0 === t.code && t.data.secretKey && e("UPDATE_SECRET_KEY", t.data.secretKey)
}
).catch(e=>{}
)
}
,
setDictResult: ({commit: e},t)=>{
e("SET_DICTRESULT", t)
}
,
initTextTranslateSettingStore: ({commit: e},t)=>{
const o = a["a"].get("allowStroke")
, n = a["a"].get("showPjm")
, c = a["a"].get("showRomanPronunciation")
, i = a["a"].get("showWordsNumber");
e("SET_ALLOW_STROKE", null !== o && o),
e("SET_SHOW_PJM", null !== n && n),
e("SET_SHOW_ROMAN_PRONUNCICATION", null !== c && c),
e("SET_SHOW_WORDS_NUMBER", null === i || i)
}
}
, l = {
UPDATE_SECRET_KEY(e, t) {
e.secretKey = t
},
SET_DICTRESULT(e, t) {
e.dictResult = t
},
SET_ALLOW_STROKE(e, t) {
e.allowStroke = t,
a["a"].set("allowStroke", t),
Object(c["b"])(t)
},
SET_SHOW_PJM(e, t) {
e.showPjm = t,
a["a"].set("showPjm", t)
},
SET_SHOW_ROMAN_PRONUNCICATION(e, t) {
e.showRomanPronunciation = t,
a["a"].set("showRomanPronunciation", t)
},
SET_SHOW_WORDS_NUMBER(e, t) {
e.showWordsNumber = t,
a["a"].set("showWordsNumber", t)
}
};
t["default"] = {
state: i,
getters: r,
mutations: l,
actions: s
}
},
复制代码
显然1a68
是一个vuex
模块?⌨️?,这个模块在后文《webtranslate接口的sign
参数生成过程分析》还会用到。
至此,decodeData
3个参数都知道了,分析下其内容:
T = (t,o,n)=>{
if (!t)
return null;
const a = e.alloc(16, f(o))
, i = e.alloc(16, f(n))
, r = c.a.createDecipheriv("aes-128-cbc", a, i);
let s = r.update(t, "base64", "utf-8");
return s += r.final("utf-8"),
s
}
复制代码
e
是什么?它出现在chunk-vendors
里,根据webpack
常识,chunk-vendors
一般就是标准库。注释里有一句The buffer module from node.js, for the browser.
,所以e
就是node.js Buffer
的polyfill。c.a
是什么?createDecipheriv
也出现在chunk-vendors
,所以也属于标准库,网上搜一下createDecipheriv
可知c.a
是node.js
自带的crypto
模块。f
就是同一个模块定义的函数:
function f(e) {
return c.a.createHash("md5").update(e).digest()
}
复制代码
至此,我们已经可以写出解密代码:
const crypto = require('crypto');
function getMd5(e) {
return crypto.createHash('md5').update(e).digest();
}
function decryptData(encryptedText) {
const algo = 'aes-128-cbc';
const decodeKey = 'ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl';
const decodeIv = 'ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4';
const md5Key = Buffer.alloc(16, getMd5(decodeKey));
const md5Iv = Buffer.alloc(16, getMd5(decodeIv));
const decipher = crypto.createDecipheriv(algo, md5Key, md5Iv);
let res = decipher.update(encryptedText, 'base64', 'utf-8');
res += decipher.final('utf-8');
return res;
}
function getTranslateResult(text) {
const o = JSON.parse(text);
return o.translateResult[0].reduce((res, item) => {
return res + item.tgt;
}, '');
}
const encryptedText = '<接口的返回值>';
const text = decryptData(encryptedText);
const translateResult = getTranslateResult(text);
console.log(translateResult);
复制代码
题外话:为什么某道翻译会有一句console.log("解密后的接口数据", a)
,并且a
比我们得到的解密结果多了几个参数?因为这个对象在接口数据解密后被追加了一些属性。
webtranslate接口的sign参数生成过程分析
首先还是考虑搜关键词:sign
不仅要在前端生成,还要在后端校验,所以一般来说,sign
要求能通过这个接口的其他参数生成,因此我们挑选了mysticTime
。我们搜到一个名为8139
的模块:
const l = "fanyideskweb"
, d = "webfanyi"
, u = "client,mysticTime,product"
, m = "1.0.0"
, p = "web"
, b = "fanyi.web";
function f(e) {
return c.a.createHash("md5").update(e).digest()
}
function g(e) {
return c.a.createHash("md5").update(e.toString()).digest("hex")
}
function v(e, t) {
return g(`client=${l}&mysticTime=${e}&product=${d}&key=${t}`)
}
function h(e) {
const t = (new Date).getTime();
return {
sign: v(t, e),
client: l,
product: d,
appVersion: m,
vendor: p,
pointParam: u,
mysticTime: t,
keyfrom: b
}
}
复制代码
根据上文分析结果,c.a
就是node.js
自带的crypto
模块。于是只剩一个疑点了:h
函数的e
参数。往下可以翻到h
的调用方式:
const A = (e,t)=>Object(n["a"])("https://dict.moudao.com/webtranslate/key", {
...e,
...h(t)
})
, O = (e,t)=>Object(n["d"])("https://dict.moudao.com/webtranslate", {
...e,
...h(t)
}, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
})
复制代码
我们只需要确定箭头函数第二个参数t
。对应的模块导出代码:
t["a"] = {
getTextTranslateSecretKey: A,
getTextTranslateResult: O,
getTextTranslateKeyword: y,
decodeData: T,
feedback: x,
getAigcEntrance: w,
getAigcStyle: k,
getAigcTran: C,
fanyiFeedback: E,
cancelLastGpt: j
}
复制代码
以O
为例,我们搜索getTextTranslateResult
,找到:
const o = an["a"].state.text.secretKey;
// ...
nn["a"].getTextTranslateResult({
i: e.data.keyword,
from: e.data.from,
to: e.data.to,
...n,
dictResult: !0,
keyid: "webfanyi"
}, o)
复制代码
看到熟悉的an["a"].state.text
,所以答案就在上文分析提到的vuex
模块。
const i = {
secretKey: ""
}
复制代码
难道secretKey
就是空串?我们写代码,在node中运行,发现是错的。随后我在上述vuex
模块中发现了修改secretKey
的函数UPDATE_SECRET_KEY
。我们搜一下:
fetchTextTranslateSecretKey: ({commit: e},t)=>{
const o = "webfanyi-key-getter"
, a = "asdjnjfenknafdfsdfsd";
n["a"].getTextTranslateSecretKey({
keyid: o
}, a).then(t=>{
0 === t.code && t.data.secretKey && e("UPDATE_SECRET_KEY", t.data.secretKey)
}
).catch(e=>{}
)
}
复制代码
发现8139
模块出现过getTextTranslateSecretKey
这个关键词,所以我们需要从https://dict.moudao.com/webtranslate/key
这个接口中拿到secretKey
。
综上,我们可以写出代码:
const crypto = require('crypto');
// mysticTime = (new Date).getTime();
function getSign(mysticTime, secretKey) {
const l = "fanyideskweb"
, d = "webfanyi"
, u = "client,mysticTime,product"
, m = "1.0.0"
, p = "web"
, b = "fanyi.web";
function g(e) {
return crypto.createHash("md5").update(e.toString()).digest("hex")
}
return g(`client=${l}&mysticTime=${mysticTime}&product=${d}&key=${secretKey}`);
}
console.log(getSign(1680688241299, 'fsdsogkndfokasodnaso') === '7c5dbf08b8e0ecdf6895f623f335a320');
复制代码
结束了吗?还没!接下来看getTextTranslateSecretKey
接口的请求参数,发现也有一个sign
参数,我们还需要继续分析。我们很容易搜到以下代码:
fetchTextTranslateSecretKey: ({commit: e},t)=>{
const o = "webfanyi-key-getter"
, a = "asdjnjfenknafdfsdfsd";
n["a"].getTextTranslateSecretKey({
keyid: o
}, a).then(t=>{
0 === t.code && t.data.secretKey && e("UPDATE_SECRET_KEY", t.data.secretKey)
}
).catch(e=>{}
)
}
复制代码
其密钥就是'asdjnjfenknafdfsdfsd'
。至此,分析也就结束了。
const crypto = require('crypto');
// mysticTime = (new Date).getTime();
function getSign(mysticTime, secretKey) {
const l = "fanyideskweb"
, d = "webfanyi"
, u = "client,mysticTime,product"
, m = "1.0.0"
, p = "web"
, b = "fanyi.web";
function g(e) {
return crypto.createHash("md5").update(e.toString()).digest("hex")
}
return g(`client=${l}&mysticTime=${mysticTime}&product=${d}&key=${secretKey}`);
}
// getTextTranslateResult
console.log(getSign(1680688241299, 'fsdsogkndfokasodnaso') === '7c5dbf08b8e0ecdf6895f623f335a320');
// getTextTranslateSecretKey
console.log(getSign(1680688236068, 'asdjnjfenknafdfsdfsd') === 'f01914c8a1e374094258ed80b94d9abb')
复制代码
梳理一下~
由'asdjnjfenknafdfsdfsd'
获取getTextTranslateSecretKey
所需的sign
参数,请求获取secretKey
→由secretKey
获取getTextTranslateResult
所需的sign
参数,请求获取翻译结果→aes-128-cbc
获取解密后的翻译结果数据。
相比于去年,难度提升太多了。
谋道翻译用到的vuex
我们回过头来看4360
模块:
4360: function(e, t, o) {
"use strict";
o("13d5");
var n = o("5502");
const a = []
, c = o("c653")
, i = c.keys().reduce((e,t)=>{ // c.keys()取出的是["./domain.js"]数组
const o = t.replace(/^\.\/(.*)\.\w+$/, "$1");
a.push(o);
const n = c(t);
return e[o] = n.default,
e
}
, {});
t["a"] = Object(n["a"])({
modules: i
})
},
复制代码
这相当于
export default new Vuex.Store({
modules: {
domain: {},
language: {},
login: {},
text: {},
}
})
复制代码
而每个模块都是{ actions, getters, mutations, state }
的结构,以./text.js
为例:
{
actions: ...,
getters: ...,
mutations: ...,
state: {
"secretKey": "",
"dictResult": {},
"decodeKey": "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl",
"decodeIv": "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4",
"allowStroke": false,
"showPjm": false,
"showRomanPronunciation": false,
"showWordsNumber": true
}
}
复制代码
使用时,an["a"].state.text.decodeKey
相当于
import store from '@/store.js';
store.state.text.decodeKey;