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

📅 发布时间:2026/7/17 8:57:47 👁️ 浏览次数:
Python逆向JSVMP加密:实战某音乐平台数据解密与爬虫构建
1. 项目概述与核心挑战今天我们来聊聊一个在数据采集领域里既经典又充满挑战的活儿模拟某音乐平台的数据解密。这个项目标题“py每日spider案例之某q music数据解密模拟jsvmp”已经点明了核心——用Python去逆向一个采用了JSVMPJavaScript Virtual Machine Protection即JavaScript虚拟机保护技术的音乐平台接口。这活儿听起来就挺硬核对吧它本质上不是简单的请求-响应而是涉及到了前端JavaScript代码的混淆、加密逻辑的虚拟化执行以及如何在Python环境中复现这套复杂的解密流程。为什么这个案例值得深挖因为JSVMP是目前前端反爬和代码保护中相当高级的一种手段。它不像简单的obfuscator混淆把变量名改成a、b、c或者用eval执行拼接的字符串。JSVMP是把一段关键的JavaScript逻辑比如生成签名、解密响应数据编译成一套自定义的字节码指令集然后由一个用JavaScript写的“虚拟机”来解释执行这些字节码。这样一来你直接看网络请求的JavaScript文件看到的是一大堆看似毫无意义的操作码opcode数组和对应的调度器真正的业务逻辑被深深地隐藏了起来。对于爬虫开发者来说这就意味着你不能简单地通过搜索关键词如encrypt、sign或者跟栈调试来定位关键函数必须深入理解这套虚拟机的运作机制。这个项目的目标很明确我们要写一个Python脚本能够模拟出这个JSVMP保护的接口生成请求参数比如sign、token或者解密返回数据比如歌曲列表、歌词的全过程。最终我们的脚本要能像正常客户端一样构造出合法的请求拿到明文的、可用的数据。这不仅仅是一个“爬虫”任务更是一次深度的JavaScript逆向工程与Python模拟执行的实战。2. JSVMP技术原理深度拆解要攻克这个堡垒首先得弄清楚敌人是怎么布防的。JSVMP我们可以把它理解为一套“自创”的编程语言和运行环境。2.1 虚拟机核心三要素一个典型的JSVMP实现通常包含三个核心部分我们可以类比一个真正的计算机指令集Opcode List这就是虚拟机的“机器语言”。开发者会定义一系列数字或字符串作为操作码每个操作码对应一个具体的原子操作。例如0x01- 从某个上下文context中加载一个变量。0x02- 将两个值压入栈并进行加法运算。0x03- 调用一个内置的或预定义的函数比如md5、base64。0x04- 条件跳转到另一条指令。 这些操作码本身没有意义它们的意义完全由虚拟机解释器赋予。字节码Bytecode这是被保护的核心逻辑编译后的产物。它就是一个由上述操作码和可能伴随的操作数operand组成的数组。比如[0x01, 0x0A, 0x02, 0x03, ...]。这个数组对于人类来说是不可读的它直接对应着原始JavaScript算法被“翻译”后的指令序列。虚拟机解释器VM Interpreter / Dispatcher这是一个用JavaScript写的while或for循环或者一个巨大的switch-case语句也可能是通过对象映射。它的工作就是像一个真正的CPU一样从头到尾读取字节码数组根据当前读取到的操作码跳转到对应的处理函数去执行。这个解释器就是整个JSVMP的“大脑”和“心脏”。2.2 保护机制与逆向难点JSVMP之所以难搞就在于它带来了几个层面的混淆逻辑隐匿真正的算法逻辑被编码成了字节码静态分析几乎无法直接还原。你看到的JS文件里只有解释器和一堆数据字节码数组、常量表、函数表等。控制流扁平化原始的if-else、for循环等结构被拆解成大量的条件跳转和无条件跳转指令使得代码的执行流程看起来像一张复杂的网状图难以理清。上下文依赖虚拟机在执行时会维护一个或多个“上下文”对象用来存储变量、函数、栈等信息。算法的输入、输出以及中间状态都依赖于这个上下文理解上下文的结构是模拟的关键。动态生成有时字节码或解释器逻辑可能是由另一段代码动态生成的或者每次运行都有些许变化增加了动态分析的复杂度。注意在逆向时我们经常会看到一个大数组字节码被一个函数循环处理这个函数内部有一个巨大的switch语句每个case对应一种操作码。这就是最经典的JSVMP解释器结构。我们的任务就是理解每个case做了什么以及它们如何操作上下文从而在Python里重建这套逻辑。3. 逆向分析与环境准备动手之前我们需要一套高效的逆向分析工作流。纯靠人眼和console.log在庞大的混淆代码里大海捞针效率太低。3.1 核心分析工具链浏览器开发者工具这是主战场。重点关注Sources面板和Network面板。Sources: 用于静态查看JS文件、设置断点、单步调试。学会使用“Pretty-print”美化功能来格式化混淆的代码。Network: 捕获所有网络请求筛选XHR/Fetch请求找到我们的目标API。查看请求头Headers、请求参数Payload和响应内容Response。重点关注那些看起来是加密的参数比如params、encSecKey、sign等。Node.js 调试对于复杂的、依赖Node环境或需要剥离浏览器环境干扰的JS代码我们可以将关键函数或整个虚拟机解释器代码提取出来在Node.js环境中运行和调试。使用node --inspect-brk your_script.js启动调试然后用Chrome DevTools的chrome://inspect连接进行远程调试功能非常强大。Python 模拟执行库这是我们将JS逻辑“移植”到Python的桥梁。PyExecJS/Js2Py这两个库可以直接在Python中执行JavaScript代码。对于不太复杂、没有严重环境依赖的JS片段这是最快捷的方式。PyExecJS通常需要一个本地JavaScript运行时如Node.js而Js2Py是纯Python实现的解释器。注意事项如果目标JS代码严重依赖浏览器特有的对象如window、document、location或者进行了激进的环境检测直接使用这些库可能会报错。这时就需要我们手动补全或模拟这些环境。代码格式化与抽象语法树工具AST Explorer在线工具可以直观展示JavaScript代码的抽象语法树。对于理解代码结构、定位特定语法节点如函数调用、变量声明非常有帮助尤其是在代码被混淆得面目全非时。Babel一个强大的JavaScript编译器工具链。我们可以编写Babel插件在AST层面进行代码转换比如尝试反控制流扁平化、常量传播等辅助我们理解原始逻辑。3.2 逆向实战第一步定位与抓取打开目标音乐网站播放一首歌或者查看某个歌单。在Network面板中过滤XHR请求寻找返回歌曲信息、歌词、播放地址的API。找到后重点观察请求URL它的路径和查询参数有什么规律请求头有没有自定义的头部比如X-Requested-With、Referer或者携带了Cookie请求体如果是POST请求它的form-data或payload里有哪些参数通常会有params和encSecKey这样成对出现的加密参数或者一个很长的、看起来像Base64的sign参数。初始化请求在页面加载时往往会有一些初始化请求返回一些关键的配置或密钥这些是后续加密解密的基础务必捕获。找到疑似加密参数生成的请求后在发起该请求的JavaScript调用栈上打上断点在Network面板中该请求的Initiator列点击可以跳转到发起请求的代码行这是逆向的入口点。4. 核心逆向过程解剖JSVMP虚拟机假设我们已经通过断点追踪到了生成加密参数sign的函数调用最终进入了一个充满巨大switch语句的函数——这就是我们的目标虚拟机解释器。4.1 静态分析与代码提取首先我们需要将这个解释器函数以及它依赖的所有“数据”提取出来。这包括解释器函数本身通常是一个名为$d、_0x开头或者叫vm、interpreter的函数。它的参数可能包含“字节码数组”、“初始上下文”等。字节码数组一个非常大的数组内容全是数字。它可能被赋值给一个变量如var _0x5c8c [123, 456, 789, ...]。常量池/函数表一些数组或对象里面存放着字符串常量、数字常量或者函数引用字节码会通过索引来引用它们。工具函数一些独立的、用于加密如CryptoJS.MD5、RSA、编码Base64或辅助计算的函数。这些函数可能在别的模块定义需要一并找到。将这些代码全部复制出来保存到一个本地的.js文件中。然后我们需要对其进行“精简”和“模拟环境补全”。4.2 动态调试与逻辑追踪仅仅静态看switch里的每个case在做什么是非常痛苦的。更有效的方法是动态追踪。在浏览器调试器中在解释器函数的入口和switch语句处设置断点。然后触发一次加密请求。当断点命中时你可以观察调用栈确认你是从正确的路径进来的。监控变量在Scope面板中查看局部变量、闭包和全局变量。重点关注那些作为参数传入的“上下文”对象、字节码索引指针比如叫ip、index、操作栈stack等。单步执行在switch语句里单步F10执行观察随着ip指令指针的变化stack栈和context上下文是如何被操作的。记录下关键的操作序列。例如case 0x10从context[某个索引]加载值到栈顶。case 0x20将栈顶两个值弹出相加结果压回栈顶。case 0x30弹出栈顶值作为参数调用一个函数可能从另一个函数表里查找将结果压栈。提炼算法我们的目标不是理解每一条指令而是理解这个虚拟机如何用这些基本指令组合出我们想要的加密逻辑。比如生成sign的算法可能是取当前时间戳、某个固定字符串、请求参数按特定顺序拼接然后进行MD5哈希。我们需要通过调试找出这个“拼接”和“哈希”的过程对应了哪一段字节码以及它们操作了哪些数据。一个实用的技巧是在调试时在控制台Console里打印关键变量的值。例如在解释器循环里插入条件打印语句需要临时修改JS代码当ip指向某个特定区域时打印出栈的内容和上下文的状态。4.3 构建Python模拟器理解了虚拟机的运作机制后我们就可以在Python中重建它。这通常有两种策略策略一直接移植解释器推荐用于复杂VM如果虚拟机逻辑非常复杂但指令集相对稳定最稳妥的方法是用Python重写整个解释器。class JSVMPInterpreter: def __init__(self, bytecode, constants, functions): self.bytecode bytecode # 字节码数组 self.constants constants # 常量池 self.functions functions # 函数映射表 self.ip 0 # 指令指针 self.stack [] # 操作数栈 self.context {} # 上下文/变量存储 def run(self): while self.ip len(self.bytecode): opcode self.bytecode[self.ip] self.ip 1 if opcode 0x01: # LOAD_CONST index self.bytecode[self.ip] self.ip 1 self.stack.append(self.constants[index]) elif opcode 0x02: # LOAD_CONTEXT key self.bytecode[self.ip] self.ip 1 self.stack.append(self.context.get(key)) elif opcode 0x03: # BINARY_ADD b self.stack.pop() a self.stack.pop() self.stack.append(a b) elif opcode 0x04: # CALL_FUNCTION func_id self.bytecode[self.ip] self.ip 1 arg_count self.bytecode[self.ip] self.ip 1 args [self.stack.pop() for _ in range(arg_count)] args.reverse() # 因为是从栈顶弹出顺序是反的 func self.functions[func_id] result func(*args) self.stack.append(result) # ... 处理更多opcode elif opcode 0xFF: # RETURN return self.stack.pop() if self.stack else None return None # 模拟一个MD5函数 def md5_func(s): import hashlib return hashlib.md5(s.encode()).hexdigest() # 假设我们从JS中提取出了这些数据 constants [timestamp, , secret_key, ] functions {1: md5_func} # 假设字节码序列是加载常量0(timestamp)加载上下文某key相加加载常量1()... 最后调用函数1(md5) bytecode [0x01, 0x00, 0x02, 0x00, 0x03, 0x01, 0x01, ...] vm JSVMPInterpreter(bytecode, constants, functions) vm.context[0] 1234567890 # 假设这是从外部传入的时间戳 result vm.run() print(f模拟计算的结果: {result})策略二提取并重写核心算法推荐用于逻辑清晰后当我们通过调试完全弄懂了加密逻辑例如sign md5(参数A固定串参数B时间戳)并且确认这个逻辑每次请求都固定不变那么最简洁高效的方法就是直接用Python实现这个算法完全绕过虚拟机。import hashlib import time def generate_sign(param_a, param_b): timestamp str(int(time.time() * 1000)) # 模拟JS的Date.now() fixed_str 某个从常量池找到的固定字符串 raw_str f{param_a}{fixed_str}{param_b}{timestamp} sign hashlib.md5(raw_str.encode()).hexdigest() return sign, timestamp实操心得在实际项目中我通常会双线进行。先用策略一Python移植VM来验证我对虚拟机逻辑的理解是否正确因为它能最忠实地复现JS行为。一旦验证通过并且发现算法本身并不复杂我就会将其重构为策略二的纯Python函数这样性能更高代码也更清晰。切忌在一开始没完全理解的情况下就试图直接写最终算法很容易出错。5. 完整爬虫案例构建现在我们将逆向得到的解密逻辑整合到一个完整的、可运行的Python爬虫中。5.1 项目结构与依赖创建一个新的项目目录结构如下qqmusic_spider/ ├── core/ │ ├── __init__.py │ ├── vm_simulator.py # JSVMP解释器模拟如果采用策略一 │ └── crypto.py # 加密解密核心函数策略二的最终算法 ├── spiders/ │ ├── __init__.py │ └── song_spider.py # 主爬虫逻辑 ├── utils/ │ ├── __init__.py │ ├── network.py # 网络请求封装 │ └── logger.py # 日志配置 ├── requirements.txt └── main.pyrequirements.txt内容requests2.28.0 execjs1.3.0 # 如果使用PyExecJS # js2py0.74 # 如果使用Js2Py pycryptodome3.15.0 # 用于AES、RSA等加密算法5.2 核心解密模块实现假设我们通过逆向发现获取歌曲详情API的请求需要两个加密参数params和encSecKey它们是通过RSA公钥加密和AES加密组合生成的。我们在crypto.py中实现。# core/crypto.py import base64 import json import random import string from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad import hashlib class QQMusicCrypto: 模拟QQ音乐Web端核心加密逻辑 注以下密钥和模式均为示例真实值需通过逆向分析获取。 def __init__(self): # 示例RSA公钥 (通常从页面初始化接口获取) self.pub_key -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...此处为示例公钥 -----END PUBLIC KEY----- # AES密钥和IV (可能是固定的也可能是动态生成的) self.aes_key b0123456789abcdef # 16字节 self.aes_iv babcdefghijklmnop # 16字节 def _random_str(self, length16): 生成随机字符串用于填充 return .join(random.choices(string.ascii_letters string.digits, klength)) def aes_encrypt(self, text): AES加密模式可能是CBC填充可能是PKCS7 cipher AES.new(self.aes_key, AES.MODE_CBC, self.aes_iv) padded_text pad(text.encode(utf-8), AES.block_size) encrypted_bytes cipher.encrypt(padded_text) return base64.b64encode(encrypted_bytes).decode(utf-8) def rsa_encrypt(self, text): RSA加密使用公钥加密AES密钥 key RSA.import_key(self.pub_key) cipher PKCS1_v1_5.new(key) encrypted_bytes cipher.encrypt(text.encode(utf-8)) return base64.b64encode(encrypted_bytes).decode(utf-8) def generate_params_and_encseckey(self, request_data): 生成最终的 params 和 encSecKey request_data: 原始的请求参数字典 # 1. 将请求数据转为JSON字符串 text json.dumps(request_data) # 2. 生成一个随机字符串作为AES密钥真实情况可能更复杂 secret_key self._random_str(16) # 3. 用这个随机密钥对请求文本进行AES加密得到 params # 注意这里需要模拟JS端的AES加密细节包括模式、填充、输出格式 # 假设我们有一个模拟好的AES加密函数 params self.aes_encrypt_with_custom_setting(text, secret_key) # 4. 对随机密钥进行RSA加密得到 encSecKey enc_seckey self.rsa_encrypt(secret_key) return params, enc_seckey def aes_encrypt_with_custom_setting(self, text, key): 模拟JS端具体的AES加密实现。 这里是一个高度简化的示例真实情况需要逆向确定 - 密钥和IV的生成方式 - 加密模式 (CBC, ECB等) - 填充方式 (PKCS7, ZeroPadding等) - 输出格式 (Base64, Hex等) # 示例CBC模式PKCS7填充输出Base64 iv b0102030405060708 # 可能是一个固定值 cipher AES.new(key.encode(utf-8), AES.MODE_CBC, iv) padded_data pad(text.encode(utf-8), AES.block_size) encrypted cipher.encrypt(padded_data) return base64.b64encode(encrypted).decode(utf-8)5.3 网络请求与爬虫主体# spiders/song_spider.py import time import json from core.crypto import QQMusicCrypto from utils.network import make_request # 一个封装了requests、重试、代理的模块 class SongSpider: def __init__(self): self.crypto QQMusicCrypto() self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Referer: https://y.qq.com/, Content-Type: application/x-www-form-urlencoded, # 注意实际可能是这种格式 } self.base_url https://u.y.qq.com/cgi-bin/musics.fcg def get_song_detail(self, song_mid): 获取歌曲详情 # 1. 构造原始请求数据 (这个结构需要从浏览器请求中捕获) raw_data { req_0: { module: vkey.GetVkeyServer, method: CgiGetVkey, param: { guid: 1234567890, # 可能需要随机生成或固定 songmid: [song_mid], songtype: [0], uin: 0, loginflag: 1, platform: 20 } }, comm: { uin: 0, format: json, ct: 24, cv: 0 } } # 2. 生成加密参数 params, enc_seckey self.crypto.generate_params_and_encseckey(raw_data) # 3. 构造最终POST请求的form-data form_data { params: params, encSecKey: enc_seckey } # 4. 发送请求 response make_request(POST, self.base_url, dataform_data, headersself.headers) if response and response.status_code 200: resp_json response.json() # 5. 解密响应数据 (如果响应也是加密的) # 通常响应是明文的JSON但关键数据如播放URL可能还在另一个加密字段里 decrypted_data self._decrypt_response(resp_json) return decrypted_data return None def _decrypt_response(self, resp_json): 解密响应。 实际情况可能更复杂可能需要从resp_json的某个字段取出加密字符串 然后用特定的密钥和算法解密。 # 示例假设响应中data字段是一个加密的字符串 encrypted_data resp_json.get(data, ) if encrypted_data: # 调用crypto模块的解密方法 # decrypted_str self.crypto.aes_decrypt(encrypted_data) # return json.loads(decrypted_str) pass # 如果响应已经是明文直接返回 return resp_json # utils/network.py 示例 import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def make_request(method, url, retries3, backoff_factor0.5, **kwargs): session requests.Session() retry_strategy Retry( totalretries, backoff_factorbackoff_factor, status_forcelist[429, 500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry_strategy) session.mount(http://, adapter) session.mount(https://, adapter) try: resp session.request(method, url, **kwargs, timeout10) resp.raise_for_status() return resp except requests.exceptions.RequestException as e: print(f请求失败: {url}, 错误: {e}) return None5.4 主程序与执行# main.py from spiders.song_spider import SongSpider def main(): spider SongSpider() # 示例歌曲ID需要从网页或搜索接口获取 song_mid 0039MnYb0qxYhV detail spider.get_song_detail(song_mid) if detail: print(json.dumps(detail, indent2, ensure_asciiFalse)) # 这里可以解析detail提取歌曲名、歌手、专辑、播放链接等信息 # song_name detail[req_0][data][test][0][song_name] # play_url detail[req_0][data][test][0][play_url] else: print(获取歌曲详情失败) if __name__ __main__: main()6. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到下面这些问题。这里记录了我踩过的坑和解决方法。6.1 逆向分析阶段问题1断点打不上或者代码被动态加载/混淆得太厉害找不到入口点。技巧尝试在Network面板中对目标请求右键选择Copy - Copy as cURL然后在命令行中执行。如果请求成功说明问题不在请求本身而在浏览器环境检测。这时可以尝试在开发者工具Console中在请求发送前注入代码覆盖或Hook一些环境检测函数如window._$、Object.defineProperty等。也可以使用XHR/fetch Breakpoints功能直接在所有发送到特定URL的请求上断点。问题2虚拟机逻辑太复杂单步调试跟丢了。技巧不要一味跟到底。在关键位置比如操作栈发生变化、调用外部函数前后使用console.log打印状态。可以写一个简单的JS脚本在Sources面板的Snippet里运行自动在特定操作码执行时打印信息。例如// 假设解释器函数叫vm它的上下文对象叫ctx var originalSwitch vm.prototype.dispatch; // 找到dispatch函数 vm.prototype.dispatch function(opcode) { console.log(IP: ${this.ip}, Opcode: ${opcode}, Stack:, this.stack.slice()); return originalSwitch.call(this, opcode); }问题3提取的JS代码在Node.js或PyExecJS中运行报错提示某些浏览器对象未定义。解决方案需要补全环境。# 在PyExecJS执行前注入环境变量 import execjs # 创建一个模拟的window和document对象 env_code window this; window.document {}; window.navigator { userAgent: Mozilla/5.0 ... }; window.location { href: https://y.qq.com }; // 如果代码使用了CryptoJS可能需要引入 // var CryptoJS require(crypto-js); with open(extracted_vm.js, r, encodingutf-8) as f: vm_code f.read() full_code env_code vm_code ctx execjs.compile(full_code) result ctx.call(main_entry_function, args) # 调用入口函数6.2 Python模拟阶段问题4模拟生成的params和encSecKey与服务端验证不通过。排查步骤对比用你的脚本和浏览器同时发起一次请求捕获两者生成的加密参数进行逐字符对比。往往一个大小写、一个填充字节的差异都会导致失败。检查编码确保所有字符串在加密前的编码一致UTF-8Latin-1。JS的btoa和Python的base64.b64encode处理非ASCII字符时行为可能不同。检查加密细节AES的模式CBC/ECB、填充PKCS7/ZeroPadding、初始向量IV是固定的还是动态的是否参与加密RSA的填充方案PKCS1_v1_5还是OAEP必须与JS端完全一致。检查输入源确认你构造的原始请求数据字典与浏览器发送的完全一致。特别注意数字和字符串的类型JS中123和123有时有区别以及对象的键顺序JS对象键序不固定但某些库序列化时可能按插入顺序或字母顺序。日志调试在Python加密函数的每一步都打印出中间结果如拼接后的字符串、加密前的字节与你在浏览器JS调试中捕获的同一阶段的结果进行对比。问题5请求返回-1000、-1001等错误码或提示“非法请求”。原因这通常是签名或参数错误但也可能是缺少必要的Cookie、Token或请求头。解决检查你的请求头是否完整复制了浏览器的User-Agent、Referer、Cookie特别是uin、qqmusic_key等。检查Cookie是否过期可能需要先模拟登录或访问首页获取一次有效的Cookie。有些接口需要先调用一个getkey或token的接口获取临时的密钥再用这个密钥去加密后续请求。问题6脚本运行一段时间后突然失效。原因对方更新了加密算法或密钥。应对策略将密钥、算法模式等可变参数配置化放在配置文件或外部数据库中方便更新。实现一个简单的健康检查机制定期用脚本访问一个简单接口如果失败则触发报警或自动重新分析这比较难但可以通知维护者。核心加密逻辑部分代码要模块化、清晰便于在算法更新时快速定位和修改。6.3 性能与维护问题7Python模拟JS加密速度慢尤其是用PyExecJS调用大型JS文件时。优化终极方案是用纯Python重写加密逻辑策略二彻底摆脱JS解释器的开销。如果必须执行JS考虑使用Js2Py纯Python实现而非PyExecJS需调用外部JS引擎避免进程间通信开销。或者将需要频繁调用的JS函数提前编译。对于RSA/AES等标准算法务必使用pycryptodome这样的本地C扩展库速度极快。问题8代码杂乱逆向分析结果和爬虫逻辑混在一起。建议严格遵循分离关注点原则。crypto.py只负责加密解密它不应该知道任何网络请求或业务逻辑。spider.py只负责构造请求、解析响应。vm_simulator.py如果存在只负责模拟虚拟机。这样当加密算法变更时你只需要修改crypto.py甚至只需要更新里面的几个密钥或函数。这个项目从技术上看是Python爬虫、JavaScript逆向和密码学知识的结合体。它没有一成不变的解决方案每一个网站、甚至同一个网站的不同版本其JSVMP实现都可能千差万别。最重要的不是记住某个具体的算法而是掌握这套分析、定位、理解、模拟的方法论。当你成功跑通第一个JSVMP保护的接口时那种成就感是无与伦比的而且这套技能会让你在面对绝大多数前端加密时都充满信心。