十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JS逆向实战:AES加密破解与Python爬虫模拟

JS逆向实战:AES加密破解与Python爬虫模拟 1. 项目概述当爬虫遇上“四库一平台”做数据采集的朋友尤其是关注建筑行业数据的对“全国建筑市场监管公共服务平台”业内俗称“四库一平台”这个名字一定不陌生。这个平台汇聚了全国建筑企业、人员、项目和诚信的海量数据对于市场分析、风险控制、商业调研来说价值巨大。然而和许多重要的政务数据平台一样为了保障数据安全和防止恶意爬取“四库一平台”在前端数据展示上普遍采用了动态渲染和接口加密技术。这意味着你直接打开网页能看到企业资质、人员证书等信息但当你试图通过传统的爬虫工具如requests直接调用接口去获取这些数据时会发现要么拿不到数据要么拿到一堆“乱码”。这就是典型的前端JS加密。平台的核心数据接口在响应时返回的并非直观的JSON明文而是经过JavaScript加密后的密文。这些密文需要在浏览器环境中由特定的JS代码解密后才能渲染成我们看到的网页内容。因此想要稳定、高效地获取这些数据JS逆向就成了必须攻克的技术关卡。简单说我们的目标就是弄明白网页上的JavaScript是如何加密请求、解密响应的然后用Python模拟这一整套逻辑让我们的程序能像浏览器一样“看懂”数据。最近一次深度分析该平台基于2023年末的版本其核心加密方案主要围绕AES对称加密展开并混合了随机密钥、数据填充等常见但易踩坑的细节。网上能找到的很多旧教程要么已经失效要么语焉不详只给结果不说过程让初学者无从下手。接下来我将以一个具体的查询接口为例完整拆解从浏览器开发者工具分析到本地Python代码还原的每一步重点不仅在于“怎么做”更在于“为什么这么做”以及“过程中会遇到哪些坑”。2. 逆向分析的核心思路与准备工作2.1 逆向分析的基本逻辑逆向不是盲目猜测而是有迹可循的推理过程。面对一个加密接口我们的分析路径通常是定位目标在浏览器中打开目标网页例如企业信息查询页使用开发者工具F12的“网络”Network面板筛选XHR/Fetch请求找到真正返回核心数据的那一个接口。其特征通常是响应体Response看起来像乱码或者是一串有规律的字符如Base64编码后的样子。寻找加密入口查看该请求的“发起者”Initiator调用栈或者直接搜索响应中的关键字段如接口URL的一部分、或已知的明文字段名在网站的JS文件中定位到处理加密/解密的函数。理解加密流程通常分为请求参数加密和响应体解密两部分。我们需要找到生成加密密钥、执行加密算法如AES、以及处理加密模式如CBC、填充方式如PKCS7的具体代码。关键参数追踪AES加密需要密钥Key、初始向量IV。这些值可能是固定的也可能是动态生成的例如从服务器另一个接口获取或由前端JS计算得出。必须找到它们的来源。代码还原与模拟将关键的JavaScript加密/解密函数用Python或其他语言等价实现。这涉及到算法库的选择如pycryptodome和参数的对齐。2.2 环境与工具准备工欲善其事必先利其器。以下是本次逆向分析及后续Python模拟会用到的核心工具浏览器Google Chrome 或 Microsoft Edge推荐。其开发者工具功能最强大。开发者工具插件/技巧“搜索”功能CtrlShiftF在所有加载的JS资源中全局搜索关键字是定位加密函数的神器。“重写”Overrides或本地替换可以将线上混淆的JS文件保存到本地格式化后打断点调试避免每次刷新页面代码被重新加载和混淆。“代码段”Snippets可以编写和运行自定义的JS代码片段用于测试提取到的函数。Python环境建议Python 3.8及以上版本。关键Python库requests用于发送HTTP请求。pycryptodome一个功能强大且流行的加密算法库。非常重要Python自带的crypto库已废弃且功能不全pycryptodome是其替代品完美支持AES等多种算法。pip install pycryptodomejson用于处理JSON数据。re,time,hashlib等辅助工具库。注意安装pycryptodome时如果之前安装过旧的pycrypto可能会有冲突。建议在虚拟环境中操作或者先卸载旧库pip uninstall pycrypto pycryptodome再重新安装pycryptodome。3. 实战“四库一平台”某数据接口逆向全流程我们以平台上一个查询企业详情的POST接口为例。假设其接口地址为https://example.com/api/company/detail此为示例实际地址请自行在Network面板中查找。3.1 第一步网络抓包与初步观察打开Chrome开发者工具F12切换到Network网络面板并勾选“Preserve log”保留日志防止页面跳转时请求记录被清空。在网页上进行一次企业查询操作。在网络请求列表中寻找类型为XHR或Fetch的POST请求其响应内容Preview或Response看起来不是规整的JSON而是一大串看似无规律的字符。这就是我们的目标接口。点击该请求查看其Headers标头和Payload负载。请求头Request Headers通常会有Content-Type: application/json但也可能因为加密而不同。请求负载Request Payload你可能会看到类似{data: U2FsdGVkX1/...很长一串Base64...}这样的结构。这说明data参数是加密后的密文。响应体Response Body同样可能直接是一串密文或者是一个JSON但其data字段的值是密文如{code: 200, data: U2FsdGVkX1/..., msg: success}。3.2 第二步定位加密与解密函数这是逆向的核心环节需要耐心和技巧。搜索关键字段在开发者工具的Sources源代码面板使用全局搜索CtrlShiftF。搜索关键词可以尝试接口URL的一部分如company/detail。请求负载中可能的明文字段名如果你在网页表单输入了“测试公司”可以搜索“测试公司”但中文可能被编码。更通用的关键词encrypt加密、decrypt解密、AES、CBC、PKCS7、CryptoJS一个常用的前端加密库。分析调用栈在Network面板点击目标请求查看Initiator发起者标签页。这里会显示是哪个JS文件、哪一行代码发起了这个网络请求。点击对应的行号可以直接跳转到Sources面板的相应位置这里往往就是加密函数被调用的地方。打断点调试在疑似加密或解密函数的位置打上断点重新触发请求。当程序暂停在断点时观察调用堆栈Call Stack、作用域Scope中的变量值。你可以看到传入函数的明文参数是什么加密后的结果是什么以及关键的key、iv、mode、padding等参数的值。以一次实际分析为例 我们可能搜索AES后找到一个名为security.js的文件里面有一个函数function encryptData(data) { var key CryptoJS.enc.Utf8.parse(一个16/24/32字节的字符串); var iv CryptoJS.enc.Utf8.parse(另一个16字节的字符串); var encrypted CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(data), key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); // 通常返回Base64字符串 }同时也能找到一个对应的decryptData函数用于解密响应。3.3 第三步关键参数提取与算法确认通过调试我们必须确认以下信息这些是后续Python模拟的基石加密算法确认是AES。这是目前最常见的。密钥Keykey的值。可能是固定的字符串硬编码在JS里也可能是通过某个算法动态计算出来的比如对某个固定字符串做MD5再取前16位。务必确认其长度AES-128对应16字节AES-192对应24字节AES-256对应32字节。初始向量IViv的值。同样需要确认其来源和长度通常为16字节与AES块大小一致。加密模式Mode最常见的是CBC模式。也可能是ECB、CFB等。CBC模式需要IV而ECB不需要。填充方式Padding最常见的是PKCS7填充在PKCS5中对于AES块大小是等价的。这是确保明文长度是块大小整数倍的方法。输出格式加密后的结果通常会被转换成Base64字符串进行传输。也可能直接是十六进制字符串。实操心得很多平台的key和iv并不是直接写在明处的字符串。它们可能是通过window.xxx、localStorage或另一个初始化接口获取的。你需要顺着代码逻辑往上找看这个key变量是从哪里赋的值。有时key是一个经过CryptoJS.enc.Utf8.parse处理后的WordArray对象在调试时看到的是内存对象你需要找到它原始的字符串形式。3.4 第四步Python代码还原与实现假设我们分析出以下参数算法AES模式CBC填充PKCS7密钥Key“this_is_a_16byte_key”(16字节UTF-8编码)初始向量IV“1234567890123456”(16字节UTF-8编码)数据编码UTF-8输出Base64下面是使用pycryptodome库进行加密和解密的Python实现import base64 import json from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad class AESCipher: def __init__(self, key, iv): 初始化AES加密器 :param key: 密钥字符串长度必须为16(AES-128), 24(AES-192), 或32(AES-256)字节 :param iv: 初始向量字符串长度必须为16字节 self.key key.encode(utf-8) self.iv iv.encode(utf-8) # 确保key和iv长度正确这里以AES-128为例 if len(self.key) not in [16, 24, 32]: raise ValueError(f密钥长度必须为16, 24或32字节当前为{len(self.key)}字节) if len(self.iv) ! 16: raise ValueError(f初始向量长度必须为16字节当前为{len(self.iv)}字节) def encrypt(self, plain_text): AES-CBC-PKCS7加密 :param plain_text: 待加密的明文字符串 :return: Base64编码的密文字符串 # 将明文转换为字节并使用PKCS7进行填充 plain_bytes plain_text.encode(utf-8) padded_bytes pad(plain_bytes, AES.block_size, stylepkcs7) # 创建AES加密器 cipher AES.new(self.key, AES.MODE_CBC, self.iv) # 执行加密 encrypted_bytes cipher.encrypt(padded_bytes) # 将加密后的字节转换为Base64字符串 encrypted_b64 base64.b64encode(encrypted_bytes).decode(utf-8) return encrypted_b64 def decrypt(self, encrypted_b64): AES-CBC-PKCS7解密 :param encrypted_b64: Base64编码的密文字符串 :return: 解密后的明文字符串 # 将Base64字符串解码为字节 encrypted_bytes base64.b64decode(encrypted_b64) # 创建AES解密器 cipher AES.new(self.key, AES.MODE_CBC, self.iv) # 执行解密 decrypted_padded_bytes cipher.decrypt(encrypted_bytes) # 去除PKCS7填充 decrypted_bytes unpad(decrypted_padded_bytes, AES.block_size, stylepkcs7) # 将字节转换回字符串 decrypted_text decrypted_bytes.decode(utf-8) return decrypted_text # 使用示例 if __name__ __main__: # 初始化参数替换为你逆向分析得到的真实key和iv key this_is_a_16byte_key # 16字节 iv 1234567890123456 # 16字节 aes AESCipher(key, iv) # 模拟请求加密查询参数 query_params { companyName: 某某建设集团有限公司, pageNum: 1, pageSize: 10 } plain_text json.dumps(query_params, ensure_asciiFalse, separators(,, :)) print(f待加密明文{plain_text}) encrypted_data aes.encrypt(plain_text) print(f加密后(Base64){encrypted_data}) # 模拟接收响应解密服务器返回的数据 # 假设服务器返回的密文是 server_encrypted_response_b64 # 这里用我们刚加密的结果模拟一下 server_encrypted_response_b64 encrypted_data decrypted_response aes.decrypt(server_encrypted_response_b64) print(f解密后响应{decrypted_response}) # 通常解密后是一个JSON字符串可以再解析 response_dict json.loads(decrypted_response) print(f解析为字典{response_dict})代码关键点解释pad和unpadpycryptodome的Crypto.Util.Padding模块专门处理填充。stylepkcs7对应前端的PKCS7填充。AES.new()创建加密/解密器。参数顺序为key, mode, iv。对于CBC模式必须提供iv。base64.b64encode/decode用于在字节和Base64字符串之间转换。注意编码解码都使用utf-8。json.dumps(..., ensure_asciiFalse)在序列化包含中文的字典时确保输出的是中文而非Unicode转义符这样加密前的字符串才和前端JS里JSON.stringify的结果一致。4. 逆向过程中的典型问题与排查技巧即使按照上述步骤操作在实际还原过程中也极易遇到各种问题。下面是一些常见坑点及解决方案。4.1 问题一Python解密后得到乱码或报错ValueError: Padding is incorrect.这是最常见的问题意味着你的解密过程某个环节和前端对不上。排查步骤核对Key和IV这是最可能出错的地方。确认Key和IV的字符串值、长度、编码完全一致。前端JS中CryptoJS.enc.Utf8.parse(key)是将UTF-8字符串转换成WordArray我们在Python里直接用key.encode(utf-8)得到字节即可本质是相同的。但要注意如果Key是“1234567812345678”长度就是16如果是“12345678”长度就是8不满足AES要求前端可能会自动处理如重复拼接或哈希你需要找到它最终用于加密的那个WordArray的实际字节内容。核对加密模式Mode和填充Padding100%确认是AES-CBC和PKCS7Padding。ECB模式不需要IV如果误用会出错。核对数据编码确保加密前的明文字符串编码一致。前端JS通常使用UTF-8。在Python中使用json.dumps(..., ensure_asciiFalse).encode(utf-8)。核对Base64处理有些前端库输出的Base64可能带有换行符或使用了URL安全的变种将和/替换为-和_。你需要观察原始密文如果发现-和_就需要使用base64.urlsafe_b64decode。另外确保没有遗漏任何字符。验证加密环节一个有效的调试方法是“自加密自解密”。先用你的Python代码加密一个简单字符串如“test”然后用同一套参数解密看是否能成功。如果失败说明加密逻辑有问题。如果成功再用浏览器执行相同的加密可以通过在Console里调用你找到的JS加密函数对比两者输出的Base64密文是否完全一致。如果不一致就能锁定是Key、IV、明文、还是算法细节的差异。4.2 问题二Key或IV是动态生成的如果Key/IV不是硬编码的你需要找到它们的生成逻辑。常见来源接口返回在页面加载时可能先请求一个/api/getConfig或/api/init之类的接口返回一个包含key、iv或nonce随机数的字段。JS计算通过对一个固定字符串进行MD5、SHA256等哈希运算然后截取特定长度作为Key。例如key CryptoJS.MD5(some_secret).toString().substr(0, 16)。时间戳或随机数可能与当前时间戳、一个随机数有关。应对策略在开发者工具中对存储Key/IV的变量设置“监控”Watch然后刷新页面或触发请求看它的值是如何变化的。顺着赋值语句向上回溯直到找到源头。然后在Python中完全复现这个生成逻辑。4.3 问题三遇到非标准AES或混合加密有些平台可能使用AES的变种或结合了其他加密。GCM模式除了CBCGCM模式也日渐流行它同时提供加密和认证。如果遇到需要处理authTag认证标签。pycryptodome也支持AES.MODE_GCM。自定义编码/解码加密后的字节可能不是直接转Base64而是先转成了十六进制Hex字符串或者进行了一次自定义的变换。RSAAES混合加密更复杂的情况是前端用RSA公钥加密一个随机的AES密钥然后将这个加密后的密钥和用该AES密钥加密的数据一起发给服务器。这就需要你先逆向RSA加密部分拿到AES密钥后再用AES解密数据。这种情况需要分步处理先解决RSA通常使用rsa或Crypto.PublicKey.RSA库再解决AES。4.4 问题四JS代码高度混淆难以阅读这是逆向最大的挑战。代码被压缩、变量名被替换成单字母、逻辑被拆分打乱。应对技巧使用本地替换Overrides将混淆的JS文件保存到本地用代码格式化工具如Prettier或浏览器的“Pretty Print”功能{}按钮进行美化虽然变量名改不回来但结构会清晰很多。搜索特征常量AES加密操作中通常会包含一些常量如CryptoJS.mode.CBC即使被混淆其属性访问路径如t.mode.xxx或相关的字符串如“CBC”可能还在。搜索这些特征。关注函数调用在加密点打上断点后不要只看当前函数多关注调用栈Call Stack和作用域Scope。关键参数往往是从上层函数传递下来的。使用AST解析工具进阶对于极其复杂的混淆可以借助ast抽象语法树解析库来分析和简化JS代码但这需要较高的技术水平。5. 构建健壮的爬虫程序成功逆向加解密后就可以将其整合到一个完整的爬虫程序中。这里给出一个更健壮、更工程化的示例框架。import requests import json import time import hashlib from typing import Dict, Any, Optional from .aes_cipher import AESCipher # 假设我们把上面的AESCipher类放在aes_cipher.py中 class ConstructionPlatformSpider: def __init__(self, base_url: str, key: str, iv: str): self.base_url base_url.rstrip(/) self.cipher AESCipher(key, iv) self.session requests.Session() # 设置通用请求头模拟浏览器 self.session.headers.update({ 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, Content-Type: application/json, # 根据实际情况调整 Referer: f{self.base_url}/, # 设置来源页有时是必须的 Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9, }) # 可能需要先获取一个有效的Cookie或Token self._init_session() def _init_session(self): 初始化会话例如访问首页获取Cookie或调用登录/初始化接口获取动态密钥 try: # 示例访问首页让服务器设置必要的Cookie如SESSIONID resp self.session.get(self.base_url, timeout10) resp.raise_for_status() print(会话初始化成功) # 如果密钥是动态的可以在这里调用另一个接口获取并更新self.cipher # dynamic_key_info self._get_dynamic_key() # self.cipher AESCipher(dynamic_key_info[key], dynamic_key_info[iv]) except Exception as e: print(f会话初始化失败: {e}) # 根据业务决定是否抛出异常 def _encrypt_request_data(self, data: Dict[str, Any]) - str: 加密请求数据 # 确保字典序列化后与前端一致 json_str json.dumps(data, ensure_asciiFalse, separators(,, :)) encrypted_str self.cipher.encrypt(json_str) return encrypted_str def _decrypt_response_data(self, encrypted_b64: str) - Dict[str, Any]: 解密响应数据 try: decrypted_str self.cipher.decrypt(encrypted_b64) return json.loads(decrypted_str) except json.JSONDecodeError as e: print(f响应解密后不是有效的JSON: {decrypted_str[:200]}...) raise e except Exception as e: print(f解密过程发生错误: {e}) raise e def query_company_detail(self, company_name: str, page_num: int 1) - Optional[Dict]: 查询企业详情示例接口 api_path /api/company/detail # 替换为真实路径 url f{self.base_url}{api_path} # 1. 构造请求明文数据 request_data { enterpriseName: company_name, # 字段名需根据实际接口调整 pageNum: page_num, pageSize: 20, # 可能还有其他固定参数或时间戳 timestamp: int(time.time() * 1000), # 常见的时间戳参数 } # 2. 加密请求数据 encrypted_payload self._encrypt_request_data(request_data) # 有些接口要求将密文放在特定的字段里如 data post_data { data: encrypted_payload } # 3. 发送请求 try: # 注意如果接口接收的是JSON需要将post_data字典传给json参数 resp self.session.post(url, jsonpost_data, timeout15) resp.raise_for_status() # 检查HTTP状态码 response_json resp.json() # 4. 检查响应码 if response_json.get(code) ! 200: # 根据实际接口调整成功码 print(f接口返回错误: {response_json.get(msg)}) return None # 5. 解密数据部分 encrypted_response_data response_json[data] # 密文通常在data字段 decrypted_data self._decrypt_response_data(encrypted_response_data) return decrypted_data except requests.exceptions.RequestException as e: print(f网络请求失败: {e}) return None except KeyError as e: print(f响应格式异常缺少关键字段: {e}, 原始响应: {resp.text[:500]}) return None def run(self, company_list): 批量查询入口 results [] for company in company_list: print(f正在查询: {company}) data self.query_company_detail(company) if data: results.append(data) # 礼貌性延迟避免请求过快 time.sleep(1.5) return results # 使用示例 if __name__ __main__: # 配置信息需从逆向分析中获得 BASE_URL https://jzsc.mohurd.gov.cn # 示例请替换为实际地址 AES_KEY 你逆向得到的16/24/32字节密钥 AES_IV 你逆向得到的16字节初始向量 spider ConstructionPlatformSpider(BASE_URL, AES_KEY, AES_IV) # 查询单个公司 # detail spider.query_company_detail(某某建设集团有限公司) # if detail: # print(json.dumps(detail, indent2, ensure_asciiFalse)) # 批量查询 companies [公司A, 公司B] all_results spider.run(companies) print(f共获取到 {len(all_results)} 家公司的数据)这个框架的健壮性体现在会话管理使用requests.Session()保持Cookie和连接模拟浏览器会话状态。错误处理对网络请求、JSON解析、加解密过程都进行了try...except包装避免程序因单次请求失败而崩溃。可配置性将Base URL、Key、IV等配置参数化便于管理和修改。延迟与礼貌在批量请求中加入了time.sleep()降低请求频率避免对目标服务器造成过大压力这也是网络爬虫应遵守的伦理。模块化将加解密逻辑独立成类AESCipher使主程序逻辑更清晰。最后必须强调技术本身无罪但使用技术必须遵守法律法规和网站的robots.txt协议。对于“四库一平台”这类政府公共服务网站的数据应仅限于个人学习、研究或符合其服务条款的正当用途严禁用于商业爬取、数据倒卖等非法活动。在实际操作中务必控制请求频率避免影响网站正常运行。逆向工程是一把双刃剑深刻理解其原理能极大提升我们解决前端数据获取难题的能力但这份能力需要配合同等的责任心来使用。
返回列表