JS逆向实战:解密有道翻译接口的MD5签名与AES加密机制

📅 2026/7/6 9:00:25
JS逆向实战:解密有道翻译接口的MD5签名与AES加密机制
1. 项目概述从翻译接口到加密算法的逆向之旅最近在整理爬虫逆向的实战笔记发现“有道翻译”这个案例常被提及但很多资料要么过时要么只讲了个大概。正好结合最新的网络请求和前端代码重新走一遍这个经典的JS逆向流程。这个案例之所以经典是因为它同时涉及了两种常见的加密方式MD5和AES。对于想入门JS逆向的朋友来说这是一个绝佳的练习场能让你一次性搞懂签名Sign生成和请求体Payload加密这两个核心环节。整个过程就像在解一个设计精巧的谜题你需要找到前端JavaScript中藏匿的密钥和算法然后用Python在服务端完美复现。今天我们就来彻底拆解有道翻译最新版的接口看看它的“防御”是如何构建我们又该如何“破解”。2. 核心思路与目标拆解逆向一个接口最怕的就是一头扎进代码里看花了眼也不知道从哪里开始。我的习惯是先明确目标理清思路。对于有道翻译我们的最终目标是用Python程序模拟浏览器成功调用其翻译接口并获取正确的翻译结果。要实现这个目标我们需要解决两个核心问题如何构造一个能被服务器接受的请求这通常意味着你需要找到所有必需的参数尤其是那些经过加密或动态生成的参数。如何解析服务器返回的加密数据服务器返回的数据可能也是加密的需要解密后才能得到明文结果。通过浏览器开发者工具F12对有道翻译网页进行抓包分析我们可以快速定位到这两个问题的关键。通常一个翻译请求会包含在XHR或Fetch请求中。找到它查看其Headers和Payload逆向工作就成功了一半。2.1 接口定位与初步分析打开有道翻译网页输入一个测试词比如“hello”在Network面板中筛选XHR请求。很快你会发现一个名为类似translate_o的POST请求注意接口名可能随版本更新而变化但模式类似。点击查看其详情Request URL: 一个包含keyfrom和model等参数的地址这是接口的入口。Request Headers: 重点关注Content-Type通常是application/x-www-form-urlencoded以及是否有自定义的Cookie或User-Agent要求。对于有道翻译头部信息相对常规主要难点在请求体。Request Payload (Form Data): 这是我们需要攻坚的核心。你会看到一堆参数例如i: 待翻译的文本如“hello”。from/to: 语言方向。smartresult: 固定值如dict。client: 固定值如fanyideskweb。salt: 一个时间戳加上一个随机数形如17151634567890。sign: 一个32位的MD5哈希字符串看起来像是a7b3c8d9e0f1...这明显是加密签名的结果。lts: 一个13位的时间戳如1715163456789。bv: 另一个MD5值经分析是浏览器User-Agent的MD5。doctype: 固定值如json。version: 固定值如2.1。keyfrom: 固定值如fanyi.web。action: 固定值如FY_BY_REALTlME。初步观察i翻译文本、salt盐值、sign签名和bv浏览器指纹是动态的且sign是加密的关键。我们的任务就是找出生成sign和salt的规则以及处理可能加密的响应。2.2 逆向策略制定面对这样的参数列表一个高效的逆向策略是全局搜索关键参数在开发者工具的Sources面板使用CtrlShiftF进行全局搜索。优先搜索sign、salt、md5、encrypt、decrypt、AES等关键词。因为sign是MD5格式所以搜索md5很可能直接定位到加密函数。断点调试如果搜索不直接或者代码被混淆Obfuscated就需要在可能生成这些参数的代码行设置断点。比如可以在发起网络请求XMLHttpRequest.send或fetch的地方打上XHR/fetch Breakpoints然后单步执行F10/F11观察调用栈Call Stack和变量值的变化一步步回溯到参数生成的位置。算法还原找到生成sign和salt的JavaScript函数后分析其逻辑。通常sign是由翻译文本、salt、一个固定的密钥等字符串按特定顺序拼接后再进行MD5哈希得到的。我们需要在Python中复现这个拼接和哈希过程。响应解密查看服务器返回的响应Response。如果返回的是一串看似乱码的字符串而不是直接的JSON那么很可能响应体也被加密了常见的是AES加密。此时我们需要在JS代码中找到对应的解密函数并同样在Python中复现。注意逆向工程应仅用于学习、测试或获取公开数据的自动化务必遵守网站的robots.txt协议和服务条款不得用于恶意爬取、攻击或侵犯他人权益。3. 关键加密逻辑深度解析按照上述策略我们很快就能在JS代码中找到核心逻辑。现代前端工程化可能会将代码打包到多个chunk文件中但通过搜索md5我们大概率能定位到一个核心的加密函数集。3.1 MD5签名Sign生成机制sign参数是接口验证的核心它确保了请求的完整性和来源合法性。逆向发现有道翻译的sign生成算法相对稳定其核心公式可以概括为sign md5(“fanyideskweb” 待翻译文本 salt “Ygy_4cr#e#4EX^NUGUc5”)让我们拆解这个公式固定前缀fanyideskweb这是一个硬编码在JS中的字符串作为签名的第一部分。待翻译文本即参数i的值例如hello。盐值Salt即参数salt的值。它的生成规则通常是salt 当前时间戳 一个随机整数(0-9)。例如时间戳1715163456789加上随机数2得到salt “17151634567892”。这个随机数增加了签名的不可预测性。固定密钥Ygy_4cr#e#4EX^NUGUc5这是另一个硬编码的密钥字符串。请注意这个密钥可能会随着网站更新而改变你需要根据你逆向时的实际JS代码来确定。MD5哈希将上述四个部分按顺序拼接成一个长字符串然后计算这个字符串的MD5值32位小写十六进制最终得到sign。在Python中我们可以使用hashlib库轻松复现这个过程import hashlib import time import random def generate_salt_and_sign(word): # 生成13位时间戳 lts str(int(time.time() * 1000)) # 生成0-9的随机数作为盐值的一部分 random_num random.randint(0, 9) salt lts str(random_num) # 拼接签名原始字符串 (注意密钥需根据实际JS代码确认) secret_key Ygy_4cr#e#4EX^NUGUc5 # 示例密钥请替换为实际值 sign_str fanyideskweb word salt secret_key # 计算MD5 m hashlib.md5() m.update(sign_str.encode(utf-8)) sign m.hexdigest() return lts, salt, sign # 测试 lts, salt, sign generate_salt_and_sign(hello) print(flts: {lts}, salt: {salt}, sign: {sign})3.2 盐值Salt与时间戳lts的关联salt和lts都基于时间戳。lts是纯粹的13位毫秒级时间戳Date.now()。而salt是在这个时间戳后面追加了一个随机数字。这种设计使得每次请求的签名都不同即使翻译相同的文本也能有效防止重放攻击Replay Attack。在Python模拟时我们必须保证lts和salt的逻辑一致性即salt的前13位必须等于lts。3.3 响应体的AES解密成功发送请求后我们可能会发现服务器返回的Response不是清晰的JSON而是一串像“U2FsdGVkX1...”这样的Base64编码字符串。这通常是经过AES加密后再进行Base64编码的结果。要解密它我们需要在JS代码中找到解密函数。通常可以通过搜索AES、decrypt、CryptoJS一个常用的前端加密库等关键词来定位。逆向后典型的解密逻辑如下密钥Key和偏移量IVAES加密通常需要密钥和初始向量。在有道翻译的案例中密钥和IV往往是固定的并且可以从JS代码中提取。例如密钥可能是一个16/24/32字节的字符串IV可能是一个16字节的字符串。加密模式与填充最常见的是AES-128-CBC模式使用PKCS7填充。这也是CryptoJS的默认模式之一。解密过程先用Base64解码响应字符串然后用指定的Key和IV进行AES-128-CBC解密最后将解密后的字节数据转换为UTF-8字符串就得到了原始的JSON数据。在Python中我们可以使用pycryptodome库来完成解密import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import json def decrypt_response(encrypted_data_b64, key, iv): 解密有道翻译返回的AES加密数据。 :param encrypted_data_b64: Base64编码的加密字符串 :param key: 密钥 (bytes) :param iv: 偏移量 (bytes) :return: 解密后的JSON字典 # 1. Base64解码 encrypted_bytes base64.b64decode(encrypted_data_b64) # 2. 创建AES解密器 (模式为CBC) cipher AES.new(key, AES.MODE_CBC, iv) # 3. 解密并去除PKCS7填充 decrypted_bytes unpad(cipher.decrypt(encrypted_bytes), AES.block_size) # 4. 转换为字符串并解析JSON decrypted_text decrypted_bytes.decode(utf-8) return json.loads(decrypted_text) # 示例使用 (Key和IV需要从JS代码中提取) # key 和 iv 必须是16/24/32字节的bytes对象 # 假设从JS中提取的key是 1234567890123456 iv是 1234567890123456 key_from_js b1234567890123456 # 请替换为实际值 iv_from_js b1234567890123456 # 请替换为实际值 # 假设 resp_json 是requests.post返回的响应对象其.text是加密的Base64字符串 # encrypted_b64 resp_json.text # result decrypt_response(encrypted_b64, key_from_js, iv_from_js) # print(result[translateResult][0][0][tgt]) # 输出翻译结果实操心得寻找AES的Key和IV时不要只看变量名。有时它们可能被编码如Hex、被拼接、或被隐藏在某个对象的属性里。在JS调试器中在解密函数处设置断点然后观察传入解密函数的参数值是最直接有效的方法。4. Python完整实现与代码详解理解了所有加密解密逻辑后我们就可以用requests库编写一个完整的、可稳定运行的Python翻译函数了。这里的关键是确保所有参数都与浏览器生成的一模一样。4.1 请求参数组装我们需要构建一个字典包含所有在Form Data中看到的参数。其中动态参数通过我们编写的函数生成。import requests import time import random import hashlib import json class YoudaoTranslator: def __init__(self): self.session requests.Session() # 设置一个合理的浏览器User-Agentbv参数由其MD5生成 self.user_agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 self.session.headers.update({ User-Agent: self.user_agent, Referer: https://fanyi.youdao.com/, Content-Type: application/x-www-form-urlencoded; charsetUTF-8 }) # 这些是相对固定的参数从抓包中提取 self.fixed_params { client: fanyideskweb, doctype: json, version: 2.1, keyfrom: fanyi.web, action: FY_BY_REALTlME, smartresult: dict } # 重要以下密钥需根据实际JS代码更新 self.sign_secret_key Ygy_4cr#e#4EX^NUGUc5 self.aes_key b1234567890123456 # AES密钥示例 self.aes_iv b1234567890123456 # AES偏移量示例 def _generate_bv(self): 生成bv参数即User-Agent的MD5值 m hashlib.md5() m.update(self.user_agent.encode(utf-8)) return m.hexdigest() def _generate_salt_and_sign(self, word): 生成salt和sign参数 lts str(int(time.time() * 1000)) salt lts str(random.randint(0, 9)) sign_str fanyideskweb word salt self.sign_secret_key m hashlib.md5() m.update(sign_str.encode(utf-8)) sign m.hexdigest() return lts, salt, sign def translate(self, word, from_langAUTO, to_langAUTO): 翻译单个词语或短句。 :param word: 待翻译文本 :param from_lang: 源语言默认自动检测 :param to_lang: 目标语言默认自动检测 :return: 翻译结果字符串失败返回None # 1. 生成动态参数 lts, salt, sign self._generate_salt_and_sign(word) bv self._generate_bv() # 2. 组装所有请求参数 data { i: word, from: from_lang, to: to_lang, lts: lts, salt: salt, sign: sign, bv: bv, } data.update(self.fixed_params) # 合并固定参数 # 3. 目标URL (需从抓包中获取最新URL) url https://fanyi.youdao.com/translate_o?smartresultdictsmartresultrule try: resp self.session.post(url, datadata, timeout10) resp.raise_for_status() # 检查HTTP错误 # 4. 尝试解析响应 # 先尝试直接当作JSON解析如果未加密 try: result_json resp.json() except json.JSONDecodeError: # 如果直接解析失败可能是AES加密了尝试解密 result_json self._decrypt_response(resp.text) # 5. 从结果结构中提取翻译文本 # 有道翻译的返回结构通常是{translateResult: [[{src:...,tgt:...}]]} if result_json and translateResult in result_json: tgt_text result_json[translateResult][0][0][tgt] return tgt_text else: print(f解析响应失败原始响应: {resp.text[:200]}) return None except requests.exceptions.RequestException as e: print(f网络请求失败: {e}) return None except Exception as e: print(f翻译过程发生错误: {e}) return None def _decrypt_response(self, encrypted_b64): 解密AES加密的响应 # 这里使用之前定义的decrypt_response函数逻辑 # 为保持类完整将解密逻辑内联 try: from Crypto.Cipher import AES from Crypto.Util.Padding import unpad encrypted_bytes base64.b64decode(encrypted_b64) cipher AES.new(self.aes_key, AES.MODE_CBC, self.aes_iv) decrypted_bytes unpad(cipher.decrypt(encrypted_bytes), AES.block_size) decrypted_text decrypted_bytes.decode(utf-8) return json.loads(decrypted_text) except Exception as e: print(f解密响应失败: {e}, 加密文本: {encrypted_b64[:100]}...) return None # 使用示例 if __name__ __main__: translator YoudaoTranslator() result translator.translate(hello world) if result: print(f翻译结果: {result}) else: print(翻译失败)4.2 请求头Headers的注意事项除了User-Agent和Referer有时网站还会验证Cookie。对于有道翻译初始请求可能不需要特定Cookie但如果你需要维持会话或应对更复杂的验证可能需要处理Cookie。requests.Session()对象会自动管理Cookie在首次访问翻译页面https://fanyi.youdao.com/后再发起翻译请求是一个更稳健的做法。def __init__(self): self.session requests.Session() self.user_agent ... self.session.headers.update({...}) # 先访问一次首页获取可能的初始Cookie self.session.get(https://fanyi.youdao.com/, timeout5)4.3 错误处理与健壮性网络请求和逆向接口天生不稳定完善的错误处理至关重要。网络超时使用timeout参数避免程序长时间挂起。HTTP状态码使用resp.raise_for_status()或在条件判断中检查resp.status_code。JSON解析异常响应结构可能变化用try...except包裹resp.json()。解密异常AES解密可能因密钥错误或数据损坏而失败。结果提取异常API返回的数据结构可能微调提取字段时做好判断。5. 常见问题排查与实战技巧即使代码逻辑完全正确在实际运行中也可能遇到各种问题。下面是我在多次逆向和调试中总结的一些常见坑点和解决技巧。5.1 签名Sign验证失败这是最常见的问题表现为服务器返回错误码提示签名无效。可能原因1密钥Secret Key错误。排查这是最可能的原因。你从JS代码中提取的sign_secret_key可能已经过期。重新抓包并仔细检查JS代码。在生成sign的代码行设置断点直接查看运行时拼接的完整字符串和计算出的MD5值与你Python代码计算的结果进行逐位对比。技巧在Python中将拼接的sign_str打印出来与在浏览器调试器中捕获的sign_str进行比对。必须保证完全一致包括空格和不可见字符。可能原因2盐值Salt生成规则不符。排查确认salt的生成规则。是时间戳随机数还是时间戳本身随机数的范围是0-9还是1-10时间戳是10位还是13位确保你的Python逻辑与JS逻辑百分百一致。技巧在浏览器中连续发起两次翻译请求观察salt和lts的变化规律。然后在Python中模拟相同的规律。可能原因3待翻译文本i的处理不一致。排查JS端是否对输入文本进行了URL编码encodeURIComponent或其他预处理通常i参数是原始文本。但如果你翻译的文本包含特殊字符如空格、中文、、需要确认JS是如何处理的。抓包查看Form Data中i的实际值。技巧对于包含特殊字符的文本在Python中可以使用requests库自动处理它会在发送data字典时进行适当的编码。确保你传入data字典的i值是字符串即可。5.2 响应解密失败如果响应是一串Base64但你的解密函数报错或解出乱码。可能原因1AES密钥Key或偏移量IV错误。排查同sign的密钥一样AES的Key和IV也必须从当前有效的JS代码中提取。它们可能不是明文字符串而是通过某个函数计算或转换得来的。在JS解密函数入口设置断点查看传入的key和iv变量的实际值。技巧确保Key和IV的长度符合AES要求如AES-128需要16字节。在Python中它们必须是bytes类型。如果JS中是字符串可能需要直接编码.encode(‘utf-8’)或进行Hex解码。可能原因2加密模式或填充方式不匹配。排查JS中使用的可能是CryptoJS它默认使用CBC模式和PKCS7填充在CryptoJS中叫Pkcs7。但需要确认。查看JS代码中CryptoJS.AES.decrypt的调用方式或者搜索mode、padding等关键词。技巧pycryptodome的AES.new默认是ECB模式且无IV。必须显式指定AES.MODE_CBC和iv参数。解密后使用unpad去除PKCS7填充。可能原因3响应数据本身不是AES加密。排查随着版本更新有道翻译可能取消了响应加密。先尝试直接resp.json()。如果成功就不需要解密步骤。技巧编写代码时可以先尝试直接解析JSON失败后再尝试解密这样代码兼容性更强。5.3 请求被拒绝或返回空数据可能原因1请求频率过高。解决在请求间增加随机延时如time.sleep(random.uniform(1, 3))模拟人类操作。使用Session对象保持会话有时比短时间大量独立请求更友好。可能原因2缺少必要的Cookie或Token。解决在发起翻译请求前先用Session访问一下翻译主页让服务器设置初始Cookie。检查浏览器抓包中翻译请求的Headers里是否携带了特定的Cookie值尝试在代码中手动添加。可能原因3User-Agent或Referer被检测。解决使用常见的、更新的浏览器User-Agent字符串。正确设置Referer为翻译页面地址。5.4 代码调试技巧对比大法将你的Python程序生成的所有请求参数尤其是salt,sign,bv,lts与浏览器开发者工具中捕获的一次成功请求的参数进行逐一对比。任何一个字符的差异都会导致失败。日志输出在关键函数如_generate_salt_and_sign中打印出中间变量拼接前的字符串、计算后的MD5便于比对。使用线上Python环境快速验证对于MD5、Base64、AES等算法可以先用浏览器的Console或者在线工具快速验证你的Python计算逻辑是否正确。关注网络库更新确保你使用的requests、pycryptodome等库是最新稳定版。逆向工程是一个需要耐心和细致观察的过程。有道翻译这个案例麻雀虽小五脏俱全搞定了它你对Web爬虫中常见的参数加密和响应解密就有了扎实的理解。记住核心思路永远是观察抓包- 定位搜索/断点- 分析逻辑梳理- 复现代码实现- 验证对比调试。随着前端技术发展混淆和加固手段会越来越复杂但这个基本的方法论是通用的。