JS逆向实战-「有道翻译」AES-CBC响应解密与动态密钥获取全解析 1. 初识有道翻译接口加密机制第一次打开有道翻译的网页版输入你好点击翻译打开开发者工具查看网络请求时你会发现返回的是一串乱码。这其实是服务端对响应数据进行了AES加密的典型表现。作为前端开发者我们经常需要处理这类加密接口今天就带大家完整走一遍破解流程。我刚开始研究时也踩了不少坑比如直接搜索decrypt关键词却找不到解密函数后来发现是因为代码被混淆了。经过多次尝试总结出最有效的定位方法是使用XHR断点。在Chrome开发者工具的Sources面板中找到XHR/fetch Breakpoints添加包含webtranslate的URL断点这样每次发起翻译请求时就会自动暂停。2. 定位核心解密函数设置好断点后重新触发翻译请求代码会在send方法处暂停。这时候我们需要关注调用栈(Call Stack)从下往上逐个查看函数调用关系。在第三次暂停时会进入一个名为decodeData的函数这就是我们要找的解密入口。这个函数内部逻辑其实很清晰function decodeData(t) { const a Buffer.alloc(16, y(o)), r Buffer.alloc(16, y(n)), i crypto.createDecipheriv(aes-128-cbc, a, r); let s i.update(t, base64, utf-8); return s i.final(utf-8), s }可以看到它使用了Node.js的crypto模块采用AES-128-CBC模式进行解密。这里的关键是要找到三个参数加密数据t、密钥a和初始化向量r。3. 动态密钥生成机制解析密钥和IV的生成逻辑在y()函数中const o ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl; const n ydsecret://query/iv/ClZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WNbYpJ4; function y(e) { return crypto.createHash(md5).update(e).digest() }这里的设计很巧妙虽然看起来密钥是固定的字符串但实际上通过MD5哈希处理后才作为真正的密钥。这种设计既保证了密钥的一致性又增加了一定的破解难度。我实测发现这两个固定字符串在不同时间、不同IP请求下都保持不变说明服务端没有做动态变更。但要注意的是MD5哈希后的结果需要转换成16字节的Buffer正好符合AES-128的要求。4. Python实现完整解密流程理解了JS端的解密逻辑后我们用Python实现同样的功能。首先安装依赖pip install pycryptodome然后编写解密函数import base64 from Crypto.Cipher import AES import hashlib def decode_data(encrypted_text): # 密钥和IV的固定字符串 decode_key ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl decode_iv ydsecret://query/iv/ClZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WNbYpJ4 # 生成MD5哈希值 key hashlib.md5(decode_key.encode()).digest() iv hashlib.md5(decode_iv.encode()).digest() # 处理Base64数据注意替换URL安全的字符 encrypted_text encrypted_text.replace(-, ).replace(_, /) encrypted_data base64.b64decode(encrypted_text) # AES-CBC解密 cipher AES.new(key, AES.MODE_CBC, iv) decrypted cipher.decrypt(encrypted_data) # 去除填充 return decrypted.decode(utf-8).strip()5. 请求参数加密分析除了响应解密有道翻译的请求参数也有加密。通过分析可以发现关键参数是sign和mysticTime。其中mysticTime就是当前时间戳而sign的生成逻辑如下def generate_sign(timestamp): key fsdsogkndfokasodnaso raw fclientfanyideskwebmysticTime{timestamp}productwebfanyikey{key} return hashlib.md5(raw.encode()).hexdigest()这个签名机制相对简单就是把几个固定参数拼接后做MD5哈希。在实际请求中还需要注意以下几个关键点必须携带正确的Cookie特别是OUTFOX_SEARCH_USER_ID请求头需要包含Referer和Content-TypemysticTime需要和sign中的时间戳一致6. 完整请求示例结合以上分析我们可以用Python实现完整的翻译功能import requests import time import hashlib def youdao_translate(text): # 基础配置 url https://dict.youdao.com/webtranslate headers { User-Agent: Mozilla/5.0, Referer: https://fanyi.youdao.com/, Content-Type: application/x-www-form-urlencoded } cookies { OUTFOX_SEARCH_USER_ID: 1234561.1.1.1 } # 生成签名参数 timestamp str(int(time.time() * 1000)) sign generate_sign(timestamp) # 构建请求体 data { i: text, from: auto, to: , dictResult: true, keyid: webfanyi, sign: sign, client: fanyideskweb, product: webfanyi, appVersion: 1.0.0, vendor: web, pointParam: client,mysticTime,product, mysticTime: timestamp, keyfrom: fanyi.web } # 发送请求 resp requests.post(url, datadata, headersheaders, cookiescookies) # 解密响应 if resp.status_code 200: return decode_data(resp.text) return None7. 常见问题与解决方案在实际操作中可能会遇到以下问题返回数据仍是乱码检查Base64解码前是否做了字符替换-_替换为/确认密钥和IV的MD5哈希是否正确。请求返回403错误这通常是Cookie失效导致的需要更新OUTFOX_SEARCH_USER_ID的值。可以通过浏览器正常访问翻译页面然后从开发者工具中复制这个Cookie值。解密后出现乱码可能是填充(Padding)处理有问题。AES-CBC模式下数据长度必须是16字节的倍数如果不是会自动填充。在Python中可以使用unpad方法处理from Crypto.Util.Padding import unpad decrypted unpad(decrypted, 16)跨语言兼容性问题如果在Node.js和Python之间传递加密数据要确保双方使用相同的编码UTF-8和填充方式PKCS7。8. 安全防护建议虽然我们成功逆向了这个接口但从安全角度出发作为开发者应该注意密钥保护不要将密钥硬编码在客户端代码中应该通过服务端动态获取请求限流对频繁请求的IP进行限制参数验证服务端应该验证sign参数的有效性定期更换可以考虑定期更换密钥字符串增加破解难度这个案例也提醒我们任何客户端加密都只能增加破解难度无法完全阻止逆向工程。真正敏感的业务逻辑应该放在服务端实现。