Python逆向JSVMP加密:实战音乐平台数据解密与爬虫实现

📅 2026/7/18 15:41:44
Python逆向JSVMP加密:实战音乐平台数据解密与爬虫实现
1. 项目概述今天我们来聊聊一个在Python爬虫领域里既让人头疼又充满挑战的实战案例模拟某音乐平台的数据解密。如果你曾经尝试过爬取一些主流音乐App的数据大概率会遇到过这种情况——明明在浏览器开发者工具里能看到清晰的JSON数据但用requests或aiohttp直接请求接口返回的却是一堆看不懂的乱码或者干脆就是加密后的密文。这背后往往是平台为了反爬虫而部署的JavaScript虚拟机保护JSVMP技术。这个项目就是带你一步步拆解这个“黑盒”用Python完整复现其数据解密流程最终拿到我们想要的、结构清晰的歌曲、歌手、专辑等信息。简单来说这就是一场“逆向工程”的实战。目标不是简单地获取公开数据而是理解平台如何在前端保护数据并在Python环境中完整模拟这一保护机制实现自动化、稳定地解密。这对于需要批量获取音乐元数据比如做音乐推荐分析、歌单研究、市场趋势洞察的朋友来说是必须跨过的一道坎。整个过程会涉及网络抓包、JavaScript逆向分析、Python模拟执行等多个环节我会把每个环节的“坑”和技巧都摊开来讲清楚。2. 核心思路与技术选型解析2.1 为什么是JSVMP以及我们面对的是什么首先得明白对手是谁。传统的反爬手段比如验证码、IP频率限制、请求头校验都属于“关卡型”防御我们在请求阶段想办法绕过就行。而JSVMPJavaScript Virtual Machine Protection属于“数据型”防御它允许你正常请求接口服务器也正常响应但返回的关键数据如歌曲列表、歌词、播放链接被一层甚至多层由JavaScript动态生成的加密逻辑包裹着。解密的核心算法和密钥都隐藏在经过混淆、压缩、虚拟化处理的JavaScript代码中。这带来了几个难点算法隐匿你无法像分析简单AES、RSA那样直接找到一个叫decrypt的函数。真正的解密逻辑被拆散成无数个碎片化的操作码opcode在一个自定义的虚拟机中顺序执行。环境依赖解密过程往往严重依赖浏览器环境比如window、document对象或者一些特定的浏览器API。纯Python的execjs有时无法提供完整的模拟环境。动态变化加密逻辑和密钥可能随着时间或用户会话动态变化增加了静态分析的难度。面对JSVMP通常有几种思路完整模拟JS执行环境使用PyExecJS、PyMiniRacer或Node.js子进程直接执行扣出来的解密JS代码。优点是直接缺点是环境补全麻烦且执行效率可能较低。算法还原通过静态分析和动态调试理解其虚拟机的指令集和加密算法本质如可能是变种的AES、DES或自定义的位运算然后用Python重新实现。这是最优雅、效率最高的方式但难度也最大。自动化浏览器使用Selenium或Playwright让真正的浏览器去执行JS并获取结果。这是“降维打击”简单粗暴且有效但资源消耗大速度慢不适合大规模爬取。我们的选择是算法还原为主辅以关键JS代码模拟。原因在于音乐数据爬取通常对时效性要求不是毫秒级但需要稳定、可批量执行。完全依赖自动化浏览器成百上千个任务会把机器拖垮。而如果能把核心解密逻辑用Python实现我们的爬虫就能轻装上阵。当然在逆向分析阶段我们依然会重度依赖浏览器开发者工具和Node.js调试环境。2.2 工具链准备你的“手术刀”和“显微镜”工欲善其事必先利其器。以下是本次实战的核心工具栈我会解释为什么选它们抓包与分析工具Charles / Fiddler / 浏览器开发者工具 (Network面板)作用捕获应用发出的所有HTTP/HTTPS请求找到目标数据接口。重点关注XHR和Fetch类型的请求。选择理由Charles和Fiddler能查看更底层的流量包括可能被前端代码隐藏的请求。浏览器DevTools则更便捷可以直接看到请求的调用栈Initiator这对于逆向追踪加密参数生成位置至关重要。JavaScript逆向与分析工具Chrome DevTools (Sources面板) Node.js作用Sources面板用于设置断点、单步调试、查看调用堆栈、监控变量变化是动态分析的灵魂。Node.js环境则用于在本地隔离地执行和测试我们扣出来的JS代码片段。实操技巧在Network面板找到目标请求后右键选择Copy - Copy as cURL然后可以粘贴到命令行或Postman里重放方便测试。在Sources面板搜索关键词如encrypt、decrypt、sign、token或者直接搜索接口返回的密文特征串。Python核心库requests, execjs, pycryptodomerequests毋庸置疑HTTP请求库。用于模拟请求携带我们逆向出来的参数。execjs一个执行JavaScript代码的Python库。当我们无法完全用Python还原算法或者需要执行一些环境初始化代码时用它来调用我们整理好的JS函数。注意它依赖本地安装的JS运行时如Node.js。pycryptodome一个强大的密码学库。当逆向发现加密算法是标准算法如AES、RSA的变种时我们需要用它来实现对应的加解密。它比Python自带的crypto模块更全面、更活跃。辅助工具AST解析器、代码格式化工具作用面对高度混淆的JS代码变量名全是_0xabc123代码挤成一团我们需要先格式化然后可能借助抽象语法树AST分析工具来简化代码流识别出关键的控制结构和函数调用关系。在线工具如jsnice.org或本地库esprima可以帮上忙。注意整个分析过程请在法律允许和个人授权范围内进行仅用于学习交流和技术研究切勿用于侵犯他人合法权益或商业牟利。3. 逆向工程实战定位与剖析加密逻辑3.1 接口定位与参数初探我们以某音乐平台的歌曲列表接口为例。首先打开平台网页版进入一个歌单页面打开开发者工具的Network面板刷新页面。筛选请求在筛选框输入api、music或list等关键词找到返回歌单JSON数据的请求。通常这种请求的Response看起来是一大串毫无规律的字符或者是一个结构里包含data字段且其值为加密字符串。分析请求参数点击该请求查看Headers和Payload。除了常见的cookie、user-agent你需要特别关注Query String Parameters或Form Data中那些看起来是随机生成的长字符串参数比如params、encSecKey、signature等。这些很可能就是加密后的参数。追踪参数生成在该请求的Initiator列点击调用的JS文件它会带你进入Sources面板对应的代码位置。这里就是加密发生的起点。3.2 深入JSVMP断点调试与逻辑追踪进入加密JS文件后你大概率会看到面目全非的代码。别慌按以下步骤来格式化代码在Sources面板点击左下角的{}Pretty-print按钮让混淆的代码变得可读一些。搜索关键点在格式化后的代码中搜索上一步找到的加密参数名如encSecKey。或者搜索可能包含加密逻辑的函数名如window.asrsea这是某平台曾用的函数名。下断点在找到的疑似加密函数入口处或是在包含参数赋值操作的行号前点击设置一个断点。触发断点回到网页进行一次能触发该接口的操作如点击“加载更多”。此时代码执行会在断点处暂停。单步执行与观察Step Into (F11)进入当前函数内部。Step Over (F10)执行当前行不进入函数内部。Step Out (ShiftF11)跳出当前函数。在右侧的Scope面板观察Local和Closure作用域中变量的值变化。在Call Stack面板查看函数调用链。核心目标追踪传入加密函数的原始参数明文是什么以及加密后的输出密文是什么。记录下这个映射关系。一个典型的JSVMP结构你可能看到如下代码// 经过混淆和虚拟化处理的代码示例 var _0x5c8d26 function(_0x12b5e8, _0x2f5c7a) { // ... 一大堆位操作和数组操作 ... // 关键这里可能是一个虚拟机的解释器循环 for (var _0x3a4b2c 0; _0x3a4b2c _0x12b5e8[length]; _0x3a4b2c) { var _0x1a2b3c _0x12b5e8[_0x3a4b3c]; // 根据 _0x1a2b3c (操作码) 执行不同的操作 switch (_0x1a2b3c) { case 0x1: ... // 操作1可能是加法 case 0x2: ... // 操作2可能是异或 // ... 更多case } } return _0x2f5c7a; // 返回处理后的结果 }; // 调用处 var encryptedData _0x5c8d26(vmOpcodeArray, rawData);你的任务就是理清这个_0x5c8d26函数或者它内部调用的其他函数到底对rawData做了哪些操作。3.3 关键代码提取与简化通过断点我们定位到了核心加密函数。接下来需要把它“扣”出来。复制函数及其依赖在Sources面板右键点击函数所在的行选择Store as global variable。然后在Console中输入copy(temp1)假设存储的变量是temp1将这个函数定义复制到剪贴板。但这通常不够因为这个函数内部可能依赖了外部的其他变量或函数。回溯依赖在断点暂停时查看Scope里该函数闭包中包含哪些外部变量。在Console中尝试执行这个函数如果报错说某个变量未定义就根据错误信息去源代码中找到这个变量或函数的定义一并复制出来。构建独立执行环境将复制出来的所有代码片段整理到一个单独的.js文件中。开头需要补全一些浏览器环境下的对象但只补用到的部分。例如// 补全环境 var window this; var navigator {userAgent: }; var document {cookie: }; // 这里放置你扣出来的所有函数和变量定义 function coreEncryptFunc(rawData) { // ... 扣出来的加密逻辑 } // 导出函数方便Node.js或execjs调用 module.exports { coreEncryptFunc };本地测试在Node.js环境中运行这个文件传入一个已知的明文看输出是否与浏览器中加密的结果一致。这个过程需要反复调试和补全缺失的环境或函数直到能成功复现加密结果。4. Python模拟实现从JS到Python的跨越4.1 方案一使用execjs调用扣出的JS代码这是最直接的方法适用于加密逻辑复杂但我们已经成功扣出完整JS函数的情况。安装依赖确保安装了Node.js然后在Python中安装PyExecJS。pip install PyExecJS准备JS文件将上一步整理好的、包含coreEncryptFunc函数的JS文件保存为encrypt_logic.js。Python调用import execjs import os # 获取JS文件路径 current_dir os.path.dirname(os.path.abspath(__file__)) js_file_path os.path.join(current_dir, encrypt_logic.js) # 读取JS代码 with open(js_file_path, r, encodingutf-8) as f: js_code f.read() # 创建执行上下文 ctx execjs.compile(js_code) # 调用函数 raw_data {id: 123456, limit: 30} # 模拟请求参数 # 注意JS函数可能需要将参数转为特定格式如JSON字符串 encrypted_params ctx.call(coreEncryptFunc, raw_data) print(f加密后的参数: {encrypted_params})优点开发快能100%还原前端逻辑。缺点执行效率相对较低涉及进程间通信依赖Node.js环境如果JS代码依赖复杂的浏览器对象如Canvas补全会非常困难。4.2 方案二算法还原与Python原生实现这是更高级、更优雅的方案。通过分析我们发现某音乐平台的加密核心其实是两次AES加密并用RSA加密了AES的密钥。虽然被JSVMP包装得很复杂但本质可以还原。逆向分析结论以某平台历史版本为例仅作演示明文需要发送的请求参数一个JSON对象。过程将明文JSON字符串进行第一次AES加密模式CBC填充PKCS7密钥key1和初始向量iv1是固定的16位字符串。将上一步的结果密文1作为明文进行第二次AES加密使用另一个固定的密钥key2和iv2。得到最终的params。生成一个随机的16字节字符串作为本次会话的AES密钥secretKey。用固定的RSA公钥加密这个secretKey得到encSecKey。最终请求携带params和encSecKey发送请求。Python实现import json from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad import base64 import random import string class MusicDecryptor: def __init__(self): # 这些key和iv是通过逆向分析得到的固定值示例值非真实 self.first_aes_key b0CoJUm6Qyw8W8jud self.first_aes_iv b0102030405060708 self.second_aes_key ba8LWv2uAtXjzSfk8 self.second_aes_iv b0102030405060708 # RSA公钥示例 self.rsa_public_key -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...很长一串 -----END PUBLIC KEY----- def _aes_encrypt(self, data: str, key: bytes, iv: bytes) - str: AES CBC PKCS7加密 cipher AES.new(key, AES.MODE_CBC, iv) encrypted_bytes cipher.encrypt(pad(data.encode(utf-8), AES.block_size)) return base64.b64encode(encrypted_bytes).decode(utf-8) def generate_params(self, raw_data: dict) - tuple: 生成加密后的params和encSecKey :param raw_data: 原始请求参数字典 :return: (params, encSecKey) # 1. 将原始数据转为JSON字符串 text json.dumps(raw_data) # 2. 第一次AES加密 params self._aes_encrypt(text, self.first_aes_key, self.first_aes_iv) # 3. 第二次AES加密 params self._aes_encrypt(params, self.second_aes_key, self.second_aes_iv) # 4. 生成随机AES密钥并RSA加密模拟过程实际平台可能更复杂 # 生成16位随机字符串作为本次的secretKey secret_key .join(random.choices(string.ascii_letters string.digits, k16)).encode() # 加载RSA公钥 rsa_key RSA.import_key(self.rsa_public_key) cipher_rsa PKCS1_v1_5.new(rsa_key) # 加密secret_key得到encSecKey enc_seckey base64.b64encode(cipher_rsa.encrypt(secret_key)).decode(utf-8) return params, enc_seckey def decrypt_response(self, encrypted_data: str) - dict: 解密服务器返回的数据如果返回数据也被加密 需要逆向分析其解密逻辑这里假设是AES解密 # 示例假设返回数据是base64编码的AES加密数据 # key和iv可能需要从请求上下文或固定值获取 decrypt_key bxxx # 需要通过逆向获取 decrypt_iv bxxx cipher AES.new(decrypt_key, AES.MODE_CBC, decrypt_iv) decrypted_bytes cipher.decrypt(base64.b64decode(encrypted_data)) # 去除PKCS7填充 unpadded decrypted_bytes[:-decrypted_bytes[-1]] return json.loads(unpadded.decode(utf-8)) # 使用示例 if __name__ __main__: decryptor MusicDecryptor() raw_request {ids: [123456789], level: standard} params, enc_seckey decryptor.generate_params(raw_request) print(fParams: {params}) print(fEncSecKey: {enc_seckey}) # 然后用requests发送请求 # import requests # resp requests.post(api_url, data{params: params, encSecKey: enc_seckey}) # data decryptor.decrypt_response(resp.json()[data]) # 如果返回数据加密优点纯Python执行效率高不依赖外部环境部署方便。缺点逆向分析难度极大需要深厚的密码学和代码分析功底。一旦平台更新加密逻辑需要重新分析。4.3 整合与请求模拟无论采用哪种方案最终都要整合到爬虫流程中import requests from your_decrypt_module import MusicDecryptor # 或使用execjs方案 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., Referer: https://music.xxx.com/, Content-Type: application/x-www-form-urlencoded, # 可能还需要Cookie可以通过模拟登录或手动获取后填入 } def get_playlist_detail(playlist_id): decryptor MusicDecryptor() # 1. 构造原始请求参数 raw_data { id: playlist_id, limit: 100, offset: 0, total: True, n: 1000, csrf_token: # 可能需要 } # 2. 生成加密参数 params, enc_seckey decryptor.generate_params(raw_data) # 3. 发送请求 api_url https://music.xxx.com/weapi/v3/playlist/detail form_data { params: params, encSecKey: enc_seckey } response requests.post(api_url, dataform_data, headersheaders) resp_json response.json() # 4. 处理响应如果响应数据也加密了需要解密 # if resp_json[code] 200: # real_data decryptor.decrypt_response(resp_json[data]) # return real_data # else: # print(f请求失败: {resp_json}) # return None # 假设该平台此接口返回未加密数据 return resp_json # 测试 detail get_playlist_detail(123456789) if detail and detail[code] 200: for track in detail[playlist][tracks]: print(f歌曲: {track[name]}, 歌手: {track[ar][0][name]})5. 常见问题、排查技巧与优化策略5.1 逆向分析阶段常见问题断点无法命中或代码被动态加载现象在Sources面板找到的代码文件里打了断点但请求发生时断点不触发。排查代码可能是通过eval或Function构造函数动态生成的。在Network面板过滤js文件寻找在页面加载后新请求的、较小的JS文件它们很可能包含核心逻辑。或者在开发者工具设置中开启JavaScript source maps支持如果有的话。更通用的方法是使用“事件监听器断点”Event Listener Breakpoints在Script类别下勾选Script First Statement当有新脚本执行时会自动暂停。扣出来的代码在Node.js中运行报错现象提示window is not defined,document is not defined等。解决这是环境缺失。需要在你的JS文件顶部模拟这些全局对象。但原则是“按需补全”只补用到的属性和方法。例如// 在Node.js环境头部添加 global.window global; global.navigator { userAgent: Mozilla/5.0 ... }; // 如果代码用了btoaNode.js没有需要polyfill global.btoa (str) Buffer.from(str).toString(base64); global.atob (b64Encoded) Buffer.from(b64Encoded, base64).toString();技巧在浏览器Console中执行console.log(typeof window.xxx)来检查代码依赖了哪些不存在的属性然后针对性补全。加密结果与浏览器不一致现象同样的输入你的JS代码或Python代码输出的密文和浏览器里抓包看到的不一样。排查步骤输入一致性确保你传入加密函数的原始字符串包括空格、引号格式与浏览器中完全一致。在浏览器断点处使用JSON.stringify()查看变量的精确值。密钥/IV一致性确认使用的密钥、初始向量是否完全正确包括字节长度和值。混淆代码里可能对字符串进行了额外的编码或转换。算法细节确认加密模式CBC, ECB、填充方式PKCS7, ZeroPadding、输出格式Base64, Hex。一个字符的差异都会导致结果不同。随机数/时间戳检查加密过程是否混入了随机数或时间戳。如果有需要在你的代码里用相同的方式生成。5.2 Python模拟阶段常见问题execjs报错RuntimeError或编码错误确保Node.js已安装且路径正确在命令行输入node -v和npm -v检查。检查JS代码语法将扣出的JS代码先在Node.js命令行里直接运行测试排除语法错误。编码问题确保读写JS文件时使用utf-8编码。execjs.compile()时也可以指定编码execjs.compile(js_code, encodingutf-8)。pycryptodome加解密结果不对对齐块大小AES是块加密明文长度必须是16字节的倍数。使用Crypto.Util.Padding.pad进行PKCS7填充解密后用unpad去除填充。密钥和IV类型确保传入AES.new()的key和iv是bytes类型而不是字符串。如果源数据是十六进制或Base64字符串需要正确解码。模式匹配确认加密模式。CBC模式需要IVECB模式不需要。GCM模式是认证加密用法不同。请求被服务器拒绝403/412等请求头不全仔细比对浏览器发送的请求头除了User-Agent、Referer、Content-Type可能还需要Origin、Accept、Accept-Language甚至一些自定义头部。Cookie失效或不全某些接口需要登录态。你需要维护一个有效的会话Cookie。可以通过模拟登录获取或者手动从浏览器复制一个短期可用的Cookie。注意Cookie的过期和更新。参数签名动态变化除了params和encSecKey可能还有其他参数如_token、timestamp、nonce需要动态生成并参与签名。这需要更全面的逆向分析。5.3 性能与稳定性优化策略缓存与复用如果加密算法的密钥是固定的或者RSA公钥长期不变那么MusicDecryptor类实例可以全局复用避免每次请求都重新初始化加密器。对于execjs方案execjs.compile()创建上下文的过程开销较大应该只执行一次然后多次调用ctx.call。错误重试与降级网络请求可能失败接口可能临时变更。代码中应加入重试机制如tenacity库和异常捕获。对于execjs方案可以准备一个降级策略比如当JS执行失败时尝试切换到算法还原的Python方案如果已实现。日志与监控详细记录每个请求的入参、加密结果、响应状态和内容。当解密失败时保存下原始的加密响应数据便于后续离线分析和算法更新。应对算法更新平台加密逻辑不会一成不变。最好的防御是主动监控。可以定期如每天运行一个简单的测试用例用已知的明文加密后与历史正确密文对比或者直接请求一个已知接口看返回数据是否还能正常解析。一旦发现不一致立即触发告警启动重新分析流程。这个从JSVMP黑盒到Python透明解密的旅程本质上是一场耐心的较量。它没有一成不变的公式每一个平台、甚至同一个平台的不同接口都可能有所不同。但核心方法论是相通的抓包定位、断点调试、逻辑分析、代码提取、模拟实现。掌握这套流程再配合细致的观察和不断的试错绝大多数前端加密的“城墙”都能找到突破口。最后再次强调技术的学习是为了更好地理解系统与创造请务必在合法合规的范围内运用这些知识。