多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

JS逆向实战:从有道翻译案例掌握参数签名与AES解密全流程

JS逆向实战:从有道翻译案例掌握参数签名与AES解密全流程 1. 项目概述从浏览器到脚本理解JS逆向的核心价值做爬虫或者数据采集的朋友肯定都遇到过这样的场景打开浏览器在某个翻译网站上输入一个单词页面瞬间就给出了结果。但当你兴冲冲地打开开发者工具准备用Python的requests库模拟这个请求时却发现返回的数据是一堆看不懂的乱码或者干脆就是一个加密过的字符串。这时候常规的爬虫手段就失效了我们面对的就是一个典型的“JS逆向”场景。“JS逆向”这个词听起来有点黑客范儿但其实它的核心逻辑并不复杂。简单来说就是网站为了保护其数据接口不被轻易滥用会在前端也就是浏览器里运行的JavaScript代码对请求参数进行加密并对服务器返回的数据进行加密。我们的任务就是像侦探一样通过浏览器的开发者工具找到这些加密和解密的逻辑然后用我们自己的脚本通常是Python去复现这个过程从而拿到我们想要的数据。今天要拆解的“you道翻译”案例就是一个非常经典且教学意义十足的JS逆向实战。它几乎涵盖了JS逆向中你会遇到的所有核心环节参数签名生成、加密数据解密、以及如何将前端的JavaScript逻辑“翻译”成后端的Python代码。通过这个案例你不仅能学会搞定一个具体的网站更能掌握一套通用的分析方法和解决思路。2. 逆向目标分析与抓包定位在开始动手之前我们得先明确目标。我们的目标是模拟有道翻译网页版的翻译功能输入一个英文单词如“apple”能通过程序获取到其中文翻译“苹果”。这意味着我们不能简单地去请求一个静态页面而是要找到网页与服务器之间进行数据交换的那个“动态请求”。2.1 网络请求抓包与关键接口识别首先我们打开有道翻译的网页版fanyi.youdao.com按F12打开开发者工具切换到“网络”Network面板并勾选“保留日志”Preserve log。在输入框里输入“apple”并触发翻译。这时网络面板里会刷出一堆请求。我们的目标不是那些加载图片、CSS、JS的请求而是真正携带了翻译结果返回的那个XHR异步请求。经过筛选你会发现一个名为webtranslate的POST请求它的请求URL是https://dict.youdao.com/webtranslate。这个请求的“响应”Response标签页里内容看起来像是一长串毫无规律的字符这明显就是加密后的数据。同时查看这个请求的“负载”Payload或“请求体”Request Body你会发现里面有一大堆参数比如i,sign,client,mysticTime等等。注意网站的前端代码可能会更新接口地址或参数名可能会有变动。但核心的分析思路是不变的寻找那个返回加密数据的POST请求并仔细研究其请求参数。2.2 核心参数变化规律观察逆向的第一步永远是观察。我们多翻译几个不同的单词或者连续翻译同一个单词几次然后对比每次请求的负载参数。你会发现大部分参数如client、product、keyfrom等都是固定不变的。但有少数几个参数每次都在变i: 这个很明显就是我们输入的待翻译文本。mysticTime: 看起来像是一个13位的时间戳精确到毫秒。sign: 这个参数看起来像是一串MD5哈希值32位16进制字符串而且每次请求都不同它显然是整个请求安全性的关键很可能就是由其他参数包括mysticTime通过某种规则计算出来的。我们的任务由此明确找出sign参数的生成算法以及服务器返回的加密数据的解密算法。3. 逆向工程核心定位并分析加密函数知道了关键参数是sign接下来就要在前端浩如烟海的JavaScript代码中找到生成它的那一行。这是整个逆向过程中最考验耐心和技巧的环节。3.1 搜索与断点调试法在开发者工具的“源代码”Sources面板中我们可以使用全局搜索CtrlShiftF。搜索关键词的选择很有讲究直接搜索参数名比如搜索sign:或sign。但这种方法可能命中太多无关的代码。搜索关键常量从请求负载中找一个比较独特的固定字符串比如client的值fanyideskweb或者product的值webfanyi进行搜索。搜索网络请求特征搜索请求URL的一部分如webtranslate。搜索序列化操作前端发送POST请求时常会用JSON.stringify处理数据搜索这个函数名有时能快速定位到发送请求的代码附近。以搜索webtranslate为例我们可能会找到一段包含fetch或axios.post的代码。在这段代码附近就能看到sign被赋值的地方例如sign: k(t)。这说明sign的值是函数k传入参数t后返回的结果。接下来就是经典的断点调试。在sign: k(t)这一行左侧的行号上点击设置一个断点。然后回到网页再次触发翻译。代码执行会在断点处暂停。这时我们可以把鼠标悬停在变量t上或者在下方的控制台Console里输入console.log(t)来查看传入函数k的参数到底是什么。同时我们可以按F11Step into键进入k函数内部。3.2 核心签名函数解析进入k函数后我们终于看到了sign的生成逻辑。经过格式化后代码逻辑通常非常清晰function k(t) { // t 可能是一个对象或字符串包含了 client, mysticTime, product 等信息 var n t.client t.mysticTime t.product; var r n fsdsogkndfokasodnaso; // 一个固定的密钥字符串 return md5(r); // 对拼接后的字符串进行MD5哈希 }或者更常见的是它被压缩成一行但结构不变function k(e) { return b.md5(client e.client mysticTime e.mysticTime product e.product keyfsdsogkndfokasodnaso) }核心逻辑拆解拼接字符串将client、mysticTime、product这三个参数以keyvalue的形式用连接起来形成一个字符串。例如clientfanyideskwebmysticTime1719821234567productwebfanyi。追加固定密钥在这个字符串的末尾再拼接上一个固定的、写死在JS代码里的密钥key如keyfsdsogkndfokasodnaso。这个密钥是逆向的关键是网站用于验证请求合法性的“盐”。MD5哈希对最终拼接完成的整个字符串进行MD5加密得到的32位16进制字符串就是sign的值。至此sign的生成算法我们已经完全掌握。mysticTime就是一个标准的13位毫秒级时间戳在Python中可以用int(time.time() * 1000)轻松获取。实操心得在分析这类函数时一定要留意代码中是否使用了浏览器环境特有的对象或方法比如window.btoa,document.xxx。我们这个案例中用的是通用的md5算法是标准算法在任何语言中都有对应实现这为后续的Python复现降低了难度。如果遇到更复杂的加密库如CryptoJS分析会稍微复杂一些。4. 响应数据解密算法剖析搞定了请求参数的加密只是成功了一半。服务器返回的那一串“乱码”才是我们想要的数据现在需要破解它。4.1 寻找解密入口我们继续利用断点。在发送请求的代码附近通常会有一个处理响应的逻辑例如.then(response response.json())或.then(function(r){...})。我们在处理响应的回调函数里设置断点。当代码执行到处理响应的断点时查看r或response变量它应该就是那个乱码字符串。顺着代码往下执行你会看到这个字符串被传递给了一个解密函数比如decodeData(r)或者a.decrypt(r)。进入这个解密函数我们就能看到完整的解密流程。以有道翻译为例其解密函数核心部分如下function decodeData(e) { var t ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl; var o ydsecret://query/iv/ClZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WNbYpJ4; if (!e) return null; var a Buffer.alloc(16, md5(t)); // 生成16字节的密钥 var n Buffer.alloc(16, md5(o)); // 生成16字节的初始化向量(IV) var r crypto.createDecipheriv(aes-128-cbc, a, n); // 创建AES-CBC解密器 var l r.update(e, base64, utf-8); // 解密 l r.final(utf-8); return l; }4.2 解密算法拆解与Python复现这个解密过程可以分解为以下几个步骤我们逐一用Python实现Base64解码变种服务器返回的数据e看起来像Base64但标准Base64字符集是A-Za-z0-9/而这里包含了-和_。这是一种URL安全的Base64变种。在Python中我们需要使用base64.b64decode并指定altchars参数来处理。import base64 encrypted_data base64.b64decode(e.encode(), altcharsb-_)生成AES密钥和IV密钥和IV都是由一个固定的字符串通过MD5哈希生成的。注意这里需要的不是MD5的16进制字符串hexdigest而是原始的16字节数据digest。import hashlib key_str ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl iv_str ydsecret://query/iv/ClZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WNbYpJ4 key hashlib.md5(key_str.encode()).digest() # 16字节 iv hashlib.md5(iv_str.encode()).digest() # 16字节AES-128-CBC解密使用生成的key和iv采用AES-128算法和CBC模式进行解密。Python中常用pycryptodome库。from Crypto.Cipher import AES cipher AES.new(key, AES.MODE_CBC, iv) # 解密并去除可能的填充PKCS7 decrypted_data cipher.decrypt(encrypted_data) # 通常需要去除填充但有时解密后直接就是正确结果可以先尝试打印 print(decrypted_data.decode(utf-8))如果解密后的文本末尾有不可见字符可能需要使用unpad函数。但根据经验有道翻译的解密数据直接解码就是正确的JSON字符串。注意事项pycryptodome库不是Python标准库需要安装pip install pycryptodome。在导入时注意是from Crypto.Cipher import AES而不是旧的pycrypto。5. 完整Python代码实现与封装将上述分析的所有步骤整合起来我们就得到了一个完整的、可以模拟有道翻译网页请求的Python脚本。5.1 核心功能函数封装首先我们把生成签名和解密这两个核心功能封装成函数提高代码的可读性和复用性。import requests import time import base64 import hashlib from Crypto.Cipher import AES class YoudaoTranslator: def __init__(self): self.session requests.Session() # 固定参数从JS代码和网络请求中提取 self.client fanyideskweb self.product webfanyi self.key fsdsogkndfokasodnaso # 用于生成sign的密钥 self.key_salt ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl self.iv_salt ydsecret://query/iv/ClZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WNbYpJ4 self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://fanyi.youdao.com/, Content-Type: application/x-www-form-urlencoded, # Cookie有时不是必须的但如果遇到403等问题可以尝试添加从浏览器复制的最新Cookie # Cookie: 你的Cookie, } def _get_timestamp(self): 生成13位毫秒时间戳 return str(int(time.time() * 1000)) def _generate_sign(self, timestamp): 复现JS中的签名生成算法 sign_str fclient{self.client}mysticTime{timestamp}product{self.product}key{self.key} m hashlib.md5() m.update(sign_str.encode(utf-8)) return m.hexdigest() def _decrypt_data(self, encrypted_text): 复现JS中的数据解密算法 # 1. 处理变种Base64 try: encrypted_bytes base64.b64decode(encrypted_text, altcharsb-_) except Exception as e: print(fBase64解码失败: {e}) return None # 2. 生成AES密钥和IV key hashlib.md5(self.key_salt.encode()).digest() iv hashlib.md5(self.iv_salt.encode()).digest() # 3. AES-128-CBC解密 cipher AES.new(key, AES.MODE_CBC, iv) decrypted_bytes cipher.decrypt(encrypted_bytes) # 4. 解码并清理可能的填充或空白字符 # 直接解码AES CBC解密后可能包含PKCS7填充但有道翻译的数据通常直接可读 # 如果末尾有乱码可以尝试去除填充这里提供一个通用处理 try: # 首先尝试直接UTF-8解码 result decrypted_bytes.decode(utf-8, errorsignore).strip() # 有时结果末尾会有一些填充字符如 \x06, \x04 等可以尝试去除不可打印字符 # 更稳健的做法是使用 pkcs7_unpad但需要知道块大小这里简单处理 result .join(char for char in result if char.isprintable() or char in \n\r\t) return result except UnicodeDecodeError: print(解密后数据UTF-8解码失败) return None def translate(self, word, from_langauto, to_lang): 主翻译函数 url https://dict.youdao.com/webtranslate timestamp self._get_timestamp() sign self._generate_sign(timestamp) # 构建请求体参数需要和浏览器发送的完全一致 payload { i: word, from: from_lang, to: to_lang, useTerm: false, dictResult: true, keyid: self.product, sign: sign, client: self.client, product: self.product, appVersion: 1.0.0, vendor: web, pointParam: client,mysticTime,product, mysticTime: timestamp, keyfrom: fanyi.web, mid: 1, screen: 1, model: 1, network: wifi, abtest: 0, yduuid: abcdefg, # 这个值可以是任意字符串 } try: response self.session.post(url, datapayload, headersself.headers, timeout10) response.raise_for_status() # 检查HTTP错误 # 解密响应 decrypted_result self._decrypt_data(response.text) if decrypted_result: # 解密成功返回的通常是JSON字符串 return decrypted_result else: return f解密失败。原始响应: {response.text[:200]} except requests.exceptions.RequestException as e: return f网络请求失败: {e}5.2 脚本使用与结果解析现在我们可以使用这个封装好的类来进行翻译了。if __name__ __main__: translator YoudaoTranslator() result translator.translate(apple) print(翻译结果原始JSON:, result)运行后你会得到一个JSON字符串它包含了丰富的翻译结果。例如{ translateResult: [[{tgt:苹果,src:apple}]], errorCode:0, type:en2zh-CHS, smartResult:{entries:[n. 苹果苹果公司苹果树\r\n],type:1} }你可以用json.loads()解析这个字符串然后提取你需要的字段比如translateResult[0][0][tgt]就是“苹果”。6. 常见问题排查与进阶技巧在实际操作中你几乎不可能一帆风顺。下面是我在多次逆向过程中总结的一些常见坑点和解决技巧。6.1 请求失败403/412等状态码问题直接运行脚本返回403 Forbidden或412 Precondition Failed。排查Headers检查确保你的请求头Headers尽可能和浏览器一致。特别是User-Agent,Referer,Content-Type,Origin。Sec-开头的浏览器特有头如Sec-Ch-Ua通常不是必须的但Referer和Origin很重要。Cookie检查有些接口需要登录态或简单的会话Cookie。从浏览器复制最新的Cookie字符串到请求头中。注意Cookie可能会过期需要定期更新。参数完整性确保POST请求的data字典包含了浏览器发送的所有参数一个都不能少即使它的值是空字符串或固定值。缺少参数可能导致签名校验失败。签名算法验证这是最可能出问题的地方。强烈建议在Python中生成sign后与浏览器当次请求的sign值进行对比。你可以在浏览器开发者工具的“网络”面板中找到刚才的请求点击查看“负载”复制sign值。然后在你的Python脚本中打印出生成的sign看是否一致。如果不一致请仔细检查拼接字符串的顺序、格式、以及是否有多余的空格或换行。6.2 解密失败或得到乱码问题能拿到加密响应但解密后是乱码或报错。排查Base64解码确认使用了正确的变种Base64解码方式altcharsb-_。可以先尝试用Python解码一段已知的密文看是否能还原成字节。密钥/IV生成确认用于生成密钥和IV的盐Salt字符串完全正确包括大小写和特殊符号。确认使用的是.digest()返回字节而不是.hexdigest()返回16进制字符串。AES参数确认加密模式是AES.MODE_CBC密钥和IV长度都是16字节AES-128。pycryptodome的new方法参数顺序是(key, mode, iv)。数据填充AES CBC模式要求数据长度是16字节的倍数。服务器返回的数据在加密前已经处理好了填充。我们解密后有时末尾会有填充字节如PKCS7填充。如果直接解码UTF-8失败可以尝试手动去除末尾的非打印字符。一个更通用的方法是使用Crypto.Util.Padding.unpad。from Crypto.Util.Padding import unpad decrypted_bytes cipher.decrypt(encrypted_bytes) # 假设是PKCS7填充块大小为16 try: decrypted_bytes unpad(decrypted_bytes, 16) except ValueError: # 如果没有填充或填充不正确则忽略 pass result decrypted_bytes.decode(utf-8)6.3 使用execjs直接调用JS代码有时加密算法非常复杂或者使用了前端特有的、难以在Python中找到完美替代的库比如某些JS特有的位操作或编码函数。这时一个“偷懒”但高效的方法是直接把关键的JS函数抠出来用Python的execjs库去执行它。步骤将找到的JS加密/解密函数例如我们上面的generateSign和decodeData函数保存到一个.js文件中。注意要补全它依赖的全局对象或函数比如md5函数、Buffer、crypto等。对于Node.js环境crypto和Buffer是内置模块。在Python中安装execjs库 (pip install PyExecJS) 并确保系统安装了Node.js环境。在Python中读取JS文件编译并调用函数。示例 (youdao.js):const crypto require(crypto); function md5Hash(input) { return crypto.createHash(md5).update(input).digest(hex); } function md5Digest(input) { return crypto.createHash(md5).update(input).digest(); } function generateSign(timestamp, client, product, key) { const str client${client}mysticTime${timestamp}product${product}key${key}; return md5Hash(str); } function decodeData(encryptedText) { const keySalt ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl; const ivSalt ydsecret://query/iv/ClZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WNbYpJ4; if (!encryptedText) return null; const key Buffer.alloc(16, md5Digest(keySalt)); const iv Buffer.alloc(16, md5Digest(ivSalt)); const decipher crypto.createDecipheriv(aes-128-cbc, key, iv); let decrypted decipher.update(encryptedText, base64, utf-8); decrypted decipher.final(utf-8); return decrypted; } // 导出函数供Node.js环境或execjs调用 module.exports { generateSign, decodeData };Python调用import execjs import requests # 读取JS代码 with open(youdao.js, r, encodingutf-8) as f: js_code f.read() # 创建JS执行环境 ctx execjs.compile(js_code) # 调用JS函数生成签名 timestamp str(int(time.time() * 1000)) sign ctx.call(generateSign, timestamp, fanyideskweb, webfanyi, fsdsogkndfokasodnaso) print(fJS生成的sign: {sign}) # ... 发送请求获取加密数据 encrypted_response ... # 调用JS函数解密 decrypted_result ctx.call(decodeData, encrypted_response) print(f解密结果: {decrypted_result})注意事项execjs的性能比纯Python实现要慢因为它需要启动一个JS运行时环境。但对于算法复杂或依赖特定JS环境的情况这是最稳妥、最省事的方案可以确保和浏览器端100%一致。7. 逆向思路总结与安全边界通过这个完整的案例我们可以梳理出一套通用的JS逆向分析流程抓包定位使用浏览器开发者工具找到目标数据对应的动态请求接口。参数分析对比多次请求找出变化的、尤其是看起来像加密哈希如MD5, SHA的关键参数。搜索断点在源代码中搜索关键参数名、接口路径或固定字符串定位到参数生成和数据处理的相关函数。逻辑分析通过断点调试理清加密请求参数和解密响应数据的具体算法步骤。重点关注字符串拼接、哈希函数调用、加密库的使用。代码复现将分析出的JS逻辑用Python或其他语言的对应库进行复现。优先寻找标准算法如MD5, AES的库复杂情况可考虑使用execjs。测试验证用脚本发起请求将生成的参数与浏览器抓包结果对比将解密结果与网页显示结果对比确保完全一致。最后必须强调安全与合规的边界。JS逆向技术是一把双刃剑。我们学习它是为了理解Web应用的工作原理解决正当的数据获取需求例如个人学习、分析公开数据。绝对禁止将其用于绕过付费墙大量盗取受版权保护的内容。对目标网站进行超出合理频率的请求造成服务器压力DDoS攻击。获取用户的个人隐私数据。破解、干扰或破坏网站的正常功能。在实际项目中如果确有数据需求应首先查看目标网站是否提供公开、免费的API并严格遵守其使用条款。技术探索应在法律和道德允许的范围内进行。
返回列表