Python爬虫逆向实战:破解前端JS加密参数与响应数据

📅 2026/6/17 4:27:53
Python爬虫逆向实战:破解前端JS加密参数与响应数据
1. 项目概述当爬虫遇上“千山鸟飞绝”做爬虫的最怕遇到什么不是反爬虫的IP封禁也不是复杂的验证码而是那种你明明能看到数据包在眼前飞但里面的内容却像天书一样全是乱码或者加密字符。最近在分析一个目标站点时就遇到了这么个典型的“硬骨头”——请求参数被加密得面目全非服务器返回的响应数据也经过了层层加密整个流程堪称“千山鸟飞绝万径人踪灭”常规的请求-解析套路完全失效。这其实就是典型的JavaScript逆向工程场景也是衡量一个爬虫工程师技术水平的分水岭。今天我就以这个实战案例为蓝本拆解如何运用Python工具链一步步破解复杂的加密参数并解密响应数据最终拿到我们想要的明文信息。这个过程不仅适用于这个特定案例其思路和方法论对于处理市面上绝大多数基于前端JavaScript的加密方案都具有普适的参考价值。简单来说这个项目要解决的核心问题是如何在一个前端JavaScript负责核心加密逻辑的网站中通过逆向分析用Python复现其加密与解密过程从而实现自动化数据抓取。这涉及到对网络请求的抓包分析、对关键JavaScript代码的定位、理解其加密算法、以及最终在Python环境中实现等效逻辑。无论你是刚接触爬虫逆向的新手还是想深化理解的老手相信这个从实战出发的拆解都能给你带来启发。我们不会停留在理论而是会深入到每一个调试的细节和踩过的坑里。2. 逆向工程的核心思路与工具选型面对一个加密的网站盲目下手只会事倍功半。一个清晰的逆向思路和顺手的工具能让你事半功倍。我的整体思路可以概括为“由外而内顺藤摸瓜”。2.1 逆向分析的基本流程逆向爬虫的核心是理解前端浏览器与后端服务器之间的数据交换协议。当这个协议被JavaScript加密算法包裹时我们的任务就是揭开这层外衣。标准流程如下抓包定位使用浏览器开发者工具F12的Network面板捕获目标数据请求。重点关注XHR/Fetch请求观察其请求头Headers、请求参数Payload通常在Payload或Form Data标签页和响应内容Response。这一步的目标是确认加密的存在参数是否是一长串无规律的字符响应内容是否是乱码或明显的加密格式如Base64字符串、Hex字符串等搜索关键点在开发者工具的Sources面板或Network面板中对捕获到的加密参数值或响应中的特征字符串进行全局搜索CtrlShiftF。如果能直接搜到那是最幸运的情况。但更多时候这些值是动态生成的搜不到。堆栈调试这是逆向的精髓。在Network面板中找到目标请求右键选择“Copy - Copy as cURL”或“Copy - Copy as Node.js fetch”这能帮你保留完整的请求信息。更关键的是可以在这个请求上右键选择“Replay XHR”进行重放或者直接在该请求的Initiator标签页查看它的调用堆栈Call Stack。调用堆栈会告诉你是哪个JavaScript文件、哪一行代码发起了这个请求从而引导你找到加密函数所在的代码位置。代码分析定位到疑似加密/解密的JavaScript函数后需要仔细分析其逻辑。常见的加密方式包括AES、DES、RSA等标准加密算法的自定义实现或库调用如CryptoJS自定义的混淆算法如位运算、字符串拼接、数组变换以及将多种算法组合使用。你需要理清函数的输入明文参数、输出密文参数以及可能用到的密钥Key、初始向量IV等。Python复现在理解JavaScript加密逻辑后在Python中使用相应的库如pycryptodome、rsa、hashlib复现相同的算法。这一步的关键是确保每一步的中间结果都与JavaScript环境中的一致。2.2 工具链的选择与配置工欲善其事必先利其器。以下是我在Windows/macOS环境下长期使用并验证过的工具组合浏览器与开发者工具Google Chrome或Microsoft EdgeChromium内核。它们的开发者工具功能最强大、最标准。FireFox的开发者工具也很优秀但某些细节和扩展生态略有不同建议以Chrome为主。抓包与调试工具Fiddler Classic / Charles老牌且强大的HTTP/HTTPS抓包代理。对于需要查看手机端请求或进行更复杂请求篡改的场景非常有用。但在纯浏览器逆向中Chrome自带的Network面板通常已足够。浏览器开发者工具这是主战场。务必熟练掌握Network网络、Sources源代码、Console控制台这三个面板。JavaScript调试与格式化Pretty PrintSources面板中对于压缩成一行的JS文件点击左下角的{}图标可以格式化代码使其可读。OverridesChrome开发者工具的Overrides功能允许你将线上JS文件映射到本地修改后的版本实现断点调试和代码热更新是动态调试的神器。Console在Console中可以直接执行JavaScript代码片段用于测试某个函数的功能或者查看变量的值。Python环境与库Python 3.8建议使用较新的版本以获得更好的库支持。关键库requests/httpx/aiohttp用于发送HTTP请求。requests简单易用httpx支持HTTP/2和异步aiohttp为异步而生。pyexecjs/js2py谨慎使用。这两个库可以直接在Python中执行JavaScript代码。听起来很美好可以“免去复现算法的麻烦”但实际上坑非常多。复杂的、依赖浏览器环境如window、document对象或特定JS引擎的代码很难直接运行成功。它们通常只适用于非常简单的、纯算法的JS片段。我的建议是尽量用Python原生库复现这是最稳健的方式。pycryptodome一个功能强大的加密算法库几乎实现了所有常见的对称加密AES、DES、非对称加密RSA和哈希算法。这是复现加密算法的首选库替代老旧的pycrypto。rsa/cryptography如果需要处理RSArsa库更轻量cryptography是另一个全面的加密库。hashlibPython标准库用于MD5、SHA1、SHA256等哈希计算。json/re用于处理JSON数据和正则表达式匹配。注意关于pyexecjs很多新手会把它当作“银弹”。但在实战中90%以上的复杂网站加密都深度依赖浏览器环境直接执行其JS代码会报各种undefined错误。把时间花在理解算法并用Python实现长远来看效率更高代码也更可控。3. 实战拆解定位并分析加密逻辑现在我们进入实战环节。假设我们的目标是抓取某网站example.com的列表页数据发现其请求参数data和响应内容都是加密的。3.1 第一步网络抓包与初步观察打开Chrome开发者工具F12切换到Network面板勾选Preserve log保留日志然后访问或操作触发数据加载的页面。很快我们找到了一个关键的XHR请求其URL可能是https://api.example.com/getList方法为POST。查看它的Headers可能有一个自定义的签名头如X-Sign: abcdef123456...。查看它的Payload在Request Payload或Form Data里发现关键的请求参数是一个名为params的字段其值是一长串像U2FsdGVkX1/...的字符串这非常像Base64编码的加密数据。而点击Response看到的也不是JSON同样是乱码或类似7b227...22d7d的Hex字符串或者另一段Base64。这证实了我们的猜想请求和响应都被加密了。常规的requests.get()加上参数解析完全行不通。3.2 第二步逆向追踪加密入口这是最关键也最需要耐心的一步。我们的目标是找到生成那个加密params参数的JavaScript代码。搜索加密参数值在Sources面板全局搜索CtrlShiftF那一长串params的值。大概率是搜不到的因为这个值是每次请求动态生成的。搜索参数名或URL退而求其次搜索params这个字段名或者搜索请求URL的一部分/getList。这有可能找到构造请求的代码位置。使用调用堆栈Call Stack这是最有效的方法。在Network面板中找到那个加密请求点击它然后在右侧详情面板中找到Initiator标签页。这里会显示这个请求是由哪个JS文件、哪一行代码发起的。点击堆栈中的不同行可以直接跳转到对应的源代码位置。下断点动态调试通过Initiator或搜索我们定位到了一个可能是发起请求的函数比如叫submitRequest()或getData()。在Sources面板中找到这个函数在它的开头或者发送网络请求如fetch或XMLHttpRequest.send的那一行代码左侧点击设置一个断点。触发断点回到网页再次执行触发请求的操作比如点击“加载更多”。此时浏览器会暂停在断点处。观察变量在断点暂停的状态下右侧的Scope面板会显示当前作用域的所有变量。你可以把鼠标悬停在代码中的变量上查看其值或者在Console面板中输入变量名来查看。我们的目标就是找到params参数在传入send()方法之前它的值是如何被计算出来的。通常你会看到类似encryptedData encryptFunction(rawData, key)这样的代码。在我的这个案例中通过堆栈追踪我最终定位到了一个名为_encryptPayload的函数。它接收一个普通的JSON对象作为输入经过一系列处理输出那个Base64字符串。接下来就需要深入这个函数内部。3.3 第三步深入分析加密函数点击进入_encryptPayload函数格式化代码后我们看到了类似下面的逻辑已简化并脱敏function _encryptPayload(data) { // 1. 将对象转为JSON字符串 var jsonStr JSON.stringify(data); // 2. 使用一个固定的密钥进行AES加密CBC模式PKCS7填充 var key CryptoJS.enc.Utf8.parse(这是一个16/24/32字节的密钥); var iv CryptoJS.enc.Utf8.parse(初始向量); var encrypted CryptoJS.AES.encrypt(jsonStr, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // 3. 将加密结果转换为Base64字符串 var resultBase64 encrypted.toString(); // 4. 对Base64字符串进行自定义混淆例如字符替换或反转 var finalResult customObfuscate(resultBase64); return finalResult; }同时我还发现了一个_generateSign函数用于生成请求头X-Sign它可能是对URL、时间戳和某个Token进行MD5或SHA256哈希得到的。分析要点识别库CryptoJS是一个常用的前端加密库。看到它基本可以确定使用了标准加密算法。确定算法和模式代码中明确指出了是AES、CBC模式、Pkcs7填充。这是非常标准且友好的情况。找到密钥和IV密钥key和初始向量iv是硬编码在代码里的字符串。这是关键信息但要注意它们可能是经过某种编码如Utf8解析后的。注意额外处理加密后的Base64可能还经过了一个customObfuscate函数处理。这个函数可能是简单的字符替换如A换成1也可能是更复杂的变换。必须把这个逻辑也分析清楚。实操心得很多网站的加密并非单一算法而是“标准算法 自定义混淆”的组合拳。只复现AES解密而忽略了最后的自定义混淆步骤同样无法成功。务必跟踪数据处理的完整链条直到它被放入请求体发送出去的那一刻。4. Python复现从算法理解到代码实现理解了JavaScript的逻辑后就可以在Python中复现了。我们使用pycryptodome库。4.1 环境准备与库安装首先确保安装了必要的库pip install requests pycryptodome4.2 复现请求参数加密假设我们分析出的逻辑是原始数据JSON - AES-CBC加密 - Base64编码 - 自定义混淆。以下是Python复现代码import json import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad from Crypto.Util.Padding import unpad # 解密时会用到 def custom_obfuscate(b64_str): 复现JavaScript中的customObfuscate函数。 这里假设它是一个简单的字符替换例如A-1, B-2, -0 实际函数需要根据逆向分析结果来编写。 # 这是一个示例你需要替换成实际的分析结果 translation_table str.maketrans(AB, 120) return b64_str.translate(translation_table) def encrypt_payload(raw_data_dict): 模拟JavaScript的 _encryptPayload 函数 # 1. 转为JSON字符串 json_str json.dumps(raw_data_dict, separators(,, :), ensure_asciiFalse) # 注意JavaScript的JSON.stringify默认不包含空格所以这里用separators优化。 # 如果目标网站对JSON格式有严格要求如键顺序可能需要进一步处理。 # 2. AES-CBC加密 key 这是一个16/24/32字节的密钥.encode(utf-8) # 确保是字节串 iv 初始向量.encode(utf-8) # 确保是字节串 # 检查密钥长度AES支持16, 24, 32字节 if len(key) not in [16, 24, 32]: # 有时密钥不是直接用的可能需要用MD5/SHA256哈希一下得到固定长度 from hashlib import md5 key md5(key).digest() # 生成16字节密钥 # 或者用 sha256: key sha256(key).digest() # 生成32字节密钥 cipher AES.new(key, AES.MODE_CBC, iv) # 加密前需要填充使用PKCS7 padded_data pad(json_str.encode(utf-8), AES.block_size) encrypted_bytes cipher.encrypt(padded_data) # 3. 转为Base64 b64_str base64.b64encode(encrypted_bytes).decode(utf-8) # 4. 自定义混淆 final_result custom_obfuscate(b64_str) return final_result def generate_sign(url, timestamp, token): 模拟生成X-Sign请求头 假设签名算法是MD5(url timestamp token) import hashlib sign_string f{url}{timestamp}{token} m hashlib.md5() m.update(sign_string.encode(utf-8)) return m.hexdigest() # 使用示例 if __name__ __main__: raw_data { page: 1, size: 20, keyword: 搜索词 } encrypted_params encrypt_payload(raw_data) print(加密后的参数:, encrypted_params) # 构造请求 import requests url https://api.example.com/getList timestamp int(time.time() * 1000) # 模拟JS的Date.now() token 从Cookie或本地存储中获取的token # 这个需要从网站登录后的Cookie或LocalStorage中提取 sign generate_sign(url, timestamp, token) headers { User-Agent: 你的User-Agent, X-Sign: sign, Content-Type: application/x-www-form-urlencoded # 根据实际情况调整 } data { params: encrypted_params, t: timestamp # 可能还有其他固定参数 } # 注意如果请求是JSON格式则使用 jsondata如果是表单则使用 datadata # response requests.post(url, headersheaders, datadata) # print(response.text)关键点解析密钥处理JavaScript的CryptoJS.enc.Utf8.parse是将UTF-8字符串转换成WordArray一种内部表示在Python中我们直接用.encode(utf-8)得到字节串。如果密钥长度不符合AES要求可能需要哈希处理这在逆向时需观察JS代码是否有类似CryptoJS.MD5(key).toString()的操作。填充模式PyCryptodome的pad函数默认使用PKCS7与CryptoJS的Pkcs7对应。JSON序列化Python的json.dumps默认会输出空格而JS的JSON.stringify不会。使用separators(,, :)可以移除空格确保生成的字符串完全一致。某些网站甚至对键的顺序有要求这时可以使用collections.OrderedDict。自定义混淆函数custom_obfuscate函数必须严格按照你逆向分析出来的JavaScript逻辑来实现。可能需要写一个反向的映射关系。4.3 解密响应数据服务器返回的加密响应其解密过程往往是加密的逆过程。假设响应体是经过混淆的Base64字符串那么解密流程如下def custom_deobfuscate(obfuscated_str): 自定义混淆的反向操作。 示例将混淆的字符替换回来 1-A, 2-B, 0- translation_table str.maketrans(120, AB) return obfuscated_str.translate(translation_table) def decrypt_response(encrypted_response_str): 解密服务器返回的数据 # 1. 反向混淆 b64_str custom_deobfuscate(encrypted_response_str) # 2. Base64解码 encrypted_bytes base64.b64decode(b64_str) # 3. AES-CBC解密 key 这是一个16/24/32字节的密钥.encode(utf-8) iv 初始向量.encode(utf-8) # 同样处理密钥长度 if len(key) not in [16, 24, 32]: from hashlib import md5 key md5(key).digest() cipher AES.new(key, AES.MODE_CBC, iv) decrypted_padded_bytes cipher.decrypt(encrypted_bytes) # 4. 去除PKCS7填充 decrypted_bytes unpad(decrypted_padded_bytes, AES.block_size) # 5. 解码为JSON decrypted_str decrypted_bytes.decode(utf-8) result_dict json.loads(decrypted_str) return result_dict # 使用示例 # 假设 resp_text 是requests请求返回的文本即加密字符串 # decrypted_data decrypt_response(resp_text) # print(decrypted_data)5. 常见问题、调试技巧与避坑指南在实际操作中几乎不可能一帆风顺。下面是我总结的一些常见问题和解决技巧。5.1 问题排查清单问题现象可能原因排查思路与解决方案Python加密结果与JS不一致1. 密钥/IV处理错误编码、哈希。2. 加密模式或填充模式不匹配。3. 待加密数据JSON字符串不一致空格、键序。4. 自定义混淆函数逻辑有误。1.分段对比在JS代码中在加密的每一步如JSON.stringify后、加密后、Base64后、混淆后用console.log()打印结果。在Python中同样打印每一步的中间结果字节的Hex表示或字符串进行逐段比对。2.确认算法参数反复检查AES的key、iv、mode、padding是否完全一致。3.检查数据源确保传入加密函数的原始字典/对象完全一致。对于JSON比较字符串化后的精确内容。能成功加密请求但服务器返回错误1. 签名如X-Sign计算错误。2. 请求头缺失或错误如Cookie、Referer、User-Agent。3. 请求参数格式错误如应该是form-data却用了json。4. 加密逻辑有细微差别如时间戳精度、随机数。1.对比请求用工具如Fiddler抓取浏览器成功的请求用Python代码发送请求对比两者的所有细节URL、Method、Headers、Body。一丝一毫都不能差。2.检查签名确认签名算法的所有输入参数URL、时间戳、Token、请求体等及其拼接顺序、是否大小写敏感。3.模拟浏览器环境使用完整的浏览器Headers特别是User-Agent、Cookie维持会话、Referer等。解密响应失败Padding Error等1. 解密密钥/IV错误。2. 混淆/去混淆逻辑错误导致Base64字符串损坏。3. 服务器返回的数据可能不是单纯的加密数据可能包含其他前缀或后缀。4. 加密模式不是CBC可能是ECB、CFB等。1.验证Base64先去混淆后的字符串是否是一个合法的Base64字符串长度是4的倍数字符集正确。2.检查数据完整性打印出服务器返回的原始响应内容与浏览器接收到的进行比对看是否完全一致。3.尝试其他模式如果CBC报错检查JS代码是否使用了其他模式。无法在JS代码中找到加密函数1. 代码被高度混淆或压缩。2. 加密逻辑在WebAssembly或异步加载的模块中。3. 使用了特殊的Hook或代理。1.搜索特征搜索encrypt、encode、CryptoJS、AES、sign等关键词。2.Hook关键函数在Console中注入代码HookJSON.stringify、XMLHttpRequest.prototype.send、fetch等函数打印其参数从而定位加密发生的位置。3.跟栈要耐心仔细查看Initiator调用栈即使代码被混淆函数名可能无意义但通过断点跟栈观察变量变化也能理清逻辑。5.2 高级调试技巧Console Hook大法在页面加载前在开发者工具Console中执行以下代码可以拦截所有网络请求的发送参数和响应结果是定位加密位置的利器。// Hook XMLHttpRequest (function() { var originalSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send function(body) { console.log(XHR Request Body:, body); console.trace(); // 打印调用栈 return originalSend.apply(this, arguments); }; // Hook fetch var originalFetch window.fetch; window.fetch function(...args) { console.log(Fetch Request Info:, args); return originalFetch.apply(this, args).then(response { response.clone().text().then(text { console.log(Fetch Response Text:, text); }); return response; }); }; })();本地代码替换Overrides对于混淆的代码你可以在本地创建一个格式化的、加了注释的版本然后通过Chrome的Overrides功能映射到线上文件方便你下断点和调试。“油猴”脚本辅助编写Tampermonkey脚本在页面中注入辅助函数用于在控制台快速测试加密解密避免每次刷新页面都要重新操作。5.3 核心避坑经验不要迷信pyexecjs对于简单加密或许可行对于复杂环境依赖的投入产出比极低。理解算法并用Python实现是王道。细节决定成败一个空格、一个时间戳的毫秒与秒的差异、JSON键的顺序、Base64编码的字符集标准vs URL安全都可能导致整个加解密失败。务必进行逐字节/逐字符的比对。密钥可能动态生成不是所有密钥都硬编码在JS里。有的网站密钥是通过某个接口获取的或者由登录后的Token推导而来。逆向时需要跟踪密钥的来源。留意WebSocket和SSE对于一些实时数据网站可能使用WebSocket或Server-Sent Events它们的通信协议也可能被加密需要单独分析。遵守robots.txt与法律法规这是老生常谈但必须强调的底线。你的爬虫行为不应给目标网站服务器造成过大压力这也是为什么很多网站加强反爬的原因且必须遵守相关数据使用的法律法规尊重版权和个人隐私。逆向爬虫是一个需要耐心、细心和逻辑分析能力的技术活。它没有一成不变的公式每个网站都可能是一座独特的“千山”。但只要你掌握了“抓包-定位-分析-复现”这套基本方法论并辅以严谨的调试和比对绝大多数加密屏障都是可以被攻克的。这个过程本身也是对网络协议、加密学和编程能力的极佳锻炼。最后记住技术是用来解决问题的务必用在正当的、授权的场景之下。