初步准备

在准备对之前文章中内容复现时,发现快捷键无法打开开发人员工具,手动打开发现网页被关闭并退回到历史页面中,疑是使用了disable-dev-tools,需要参考之前文章解决。
参考: disable-devtool绕过

本次目标主要针对的是普通翻译接口,如下所示,输入任意文字请求一次翻译结果:

观察到接口:

[POST]https://dict.youdao.com/webtranslate

因为翻译无需登录等直接可用,先不关注Cookies。先来看表单和响应:

表单:

data = {
"i": "test",
"from": "auto",
"to": "",
"useTerm": "false",
"dictResult": "true",
"keyid": "webfanyi",
"sign": "e5a63b21f58ca6e683cbc1653220cdfe",
"client": "fanyideskweb",
"product": "webfanyi",
"appVersion": "1.0.0",
"vendor": "web",
"pointParam": "client,mysticTime,product",
"mysticTime": "1728357715844",
"keyfrom": "fanyi.web",
"mid": "1",
"screen": "1",
"model": "1",
"network": "wifi",
"abtest": "0",
"yduuid": "abcdefg"
}

关注到以下关键点:

  • i:待翻译的内容
  • sign:一个待逆向的加密参数
  • mysticTime:时间戳

因此重心放在逆向sign参数上。

响应结果:

Z21kD9ZK1ke6ugku2ccWu-MeDWh3z252xRTQv-wZ6jddVo3tJLe7gIXz4PyxGl73nSfLAADyElSjjvrYdCvEP4pfohVVEX1DxoI0yhm36ytQNvu-WLU94qULZQ72aml6QXrDrL2UC7W-I47X2SWjwq-lU7m2G5m3XGZymhNnxcR6vxjEfz4Kh8Wg7ANIC_O-BAvm8qErp4bS9BvQAx1Rcw-axcijJpsZU1WHG31O5s9V0-TXs13v3ipN96G3KeBL_11kPUWn99CVj4rUDHayVBhP0Hza1r8hyJV0O7oC2hYmJmjhl59yDkmigpVl9Rplx2qqJfsXjz_OadIHQFInaBuqTJGXhZJ-ixXvzNX2U16BWkIXXxsIAER0O9MuH6yUHemG-HbfmiMSyt1KjIHTeWMt-EyTg38pvAT8cwTrsAb_mI7FyuwB_RUT1UF_Q4PtlTHhusWKqIzEz2npf69lOiy_Kkv81uVOmAtsk7EuIzngXEvQBX1wkuJxT9LUHXCEMiZnKUJfzIpe-HbYwgSQR-Mua67Ekiqfiihn1mMSPiVDb-BfW5oSKgz87i0kWbqXguddIlrWK9UgOAPbyYk5bAI_qm9TDOGdU46YnYdwD03ydI-Hv3O-DZ4FhUQwZ-GLqr0-OVszZpyvMhV2Rjoy25U250BB5sbJadmuSeCqtNwAiH9KbWs2upNiBXiv4oeGmTMaF3y6rf8I3cCfWvLl4J2GUJdZAAlK_Yxh5BrKr7B1ELb8om2a1vvmQumeUs7JVUImuaq8qCC47oyR-HlYr082QGbqdyALjn1RL9qKgb9nNKMWzd7LkVMBR5ld9r3EIEaEwMagYcq5hqFUF3lQATBNZlr3dMQxmP6_m_TB9LkFPtmdYsCq_ZVll-jSUmVWVE2WGfvwOMyE_nxzUXtDmHq65mos5ZdYB5qyxsh2twMHjWmEx06U_U9_JwKMqQZjjn4MH7aWpjqP8cysdcImQ0PWAgAaSiVoCT0WIRo_bGAz4Zs1GkSmhqSI-S5GmR9A-DWLISkhSULWS1QinJpF7BEwgjyWLitz_VUtAM27LaWvXG8Vgykcq-2 NEyE6-ojt49OZxo-xIGKzaNh1mYAkria-4 tAfcDhGzzI9XwO0NZErlfgkO_MNelgnCxgDubQW_ciVC0zFgTXLpHBwxcIbVZSiTHVI96EOHdBTPDDqPXuMLL7e4KDtS8IFAE1xGnKnho6doVpaSYyB1sw0UdOPBjJi0cIhvsvxVrNnlFTW_Lt_GZIrVdxmV71hIONuqp3lhhlRX7S6RVUUyWANF7YQh-5_2 cbjABqW8LD6xEx5LbY3-I8qDv-99 fEN07OrM9cqJnAqXJtUSx_bnmadqGmc2gvQwN6SNwGugcfTLAMB--RKifTomuXgOGf1fDTlCcwDXtv2m5e4MDYZl13zHEH8sRnEPiod0o7D-Yl2Ngj2JcOqhCTaGkyHAlRuv6eyjF4c3a0CuiR8tg_vjZkb9l5qKEqjj7_sRqft6zXQ2KGg7pRFXpjlgN8NexpwOqFmUqtk9-5 sSyvLKJ3d_VJ942KKAAUZ-0 fJVF17zxSkIxhxmSgrgsUTNz-oZZVrYsKqCqyuxqM38tXst8Vr1foe6-QoWQ==

同样是加密的结果。

现在确定逆向的目标:

  • sign参数逆向
  • 响应结果解密

sign参数逆向

为了方便使用XHR断点,断点sign,在此处找到疑似加密的地方:

成功断住:

需要两个参数ae,其中a就在上面,是时间戳。

根据堆栈追溯e,可以在此处找到:

但事情可能没有这么简单。尝试在下面断住,重新请求一次,会发现它并没有直接请求翻译接口,而是请求了https://dict.youdao.com/webtranslate/key

该接口的表单:

响应结果:

继续一轮请求后,才是翻译接口,从前接口的表单结构以及下面代码也能发现,请求接口需要上面的接口中的响应结果:

并且发现e来自前一接口的响应结果。因为时间戳已固定a,所以可以手动生成下加密值,便于对比观察:

可以直接放掉断点执行,观察请求表单对比,也可以解析下这里:

上面的代码简单梳理下就是:

(0,n.H)(
"https://dict.youdao.com/webtranslate",
(0,o.A)((0,o.A)({}, e), k(t)),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
})

很明显(0,n.H)是用于发送请求的函数,括号后跟随了三个参数,为请求地址,请求表单,请求头参数。

可以在控制台中输入(0,n.H)执行并转到函数定义处,下断点:

如图,点击即可跳转到函数定义处,下断点,执行一下,可以看到表单值即为t,输出t

最终全部放行,验证下:

可以看到是完全一致的。整理下思路:

首先需要获取KEY,生成SIGN获取新的KEY,而生成这个SIGN的KEY值是固定写死的。

获得新KEY值后,用该KEY值生成SIGN值,请求翻译接口获得翻译结果。

那么接下里就是关注生成SIGN值的问题了。用于生成SIGN值的语句为S(a,e)a为时间戳,e为KEY,而S函数的定义就在上面:

其相关变量值也在上方定义:

所以上面的代码基本可以视为对client=fanyideskweb&mysticTime=[时间戳]product=webfanyi&key=a[KEY值]进行MD5加密。

现在来验证下是否为标准的MD5加密:

根据上面的ae拼接得到client=fanyideskweb&mysticTime=1728362863068&product=webfanyi&key=asdjnjfenknafdfsdfsd,进行MD5加密:

可以发现加密的结果一致。

所以SIGN值还是与之前一样,是MD5加密的结果。

响应结果解密

这里之前下的一处断点就有了用处:

根据代码,可以判断响应结果是交给了o函数处理,打个断点,尝试进入o。但实际上无法进入,只好继续执行代码,直到来到此处:

很明显,解密的代码就在此处了。

现在可以得出,解密函数即da.A.decodeData,其中o是原始响应结果,ha.A.state.text.decodeKeyha.A.state.text.decodeIv同样来自之前请求KEY值的请求响应结果之中。

进入解密函数内,可以看到有AES-128-CBC的字样

交给AI解释下:

其中T函数定义:

最终利用AI加修改后的脚本如下:

const crypto = require('crypto');
function T(e) {
return crypto.createHash("md5").update(e).digest();
}
const info = {
data:'Z21kD9ZK1ke6ugku2ccWuwRmpItPkRr5XcmzOgAKD0GcaHTZL9kyNKkN2aYY6yiOyWcBwBY7DpCnYHrwYC8hpzl_rnikm7i3xAZF3Wex6szm_MFVxOF_V3QHOtl2Ekhn-TomGU1MCLLXXF3HpNNmAGnLLd7PqJP6C1Ru5SO6US0=',
key:'ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl',
iv:'ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4'
}
// 示例密钥和初始向量(IV),需要确保它们为 16 字节
const key = Buffer.from(T(info.key));
const iv = Buffer.from(T(info.iv));

function decrypt(encryptedData) {
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
let decrypted = decipher.update(encryptedData, 'base64', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}


try {
const decryptedData = decrypt(info.data);
console.log('解密后的数据:', decryptedData);
} catch (error) {
console.error('解密失败:', error.message);
}

执行结果:

至此解密部分的逆向也结束了。

结语

本篇文章最终关注在了两处的加解密上,并没有进行实际的模拟请求,所以自然就忽略了Cookies可能带来的反爬问题,但篇幅有限,请自行测试并解决。