1. 项目概述与核心价值最近在整理爬虫实战案例库翻到了一个挺有代表性的项目某壁纸网站的数据加解密接口逆向。这个案例的典型性在于它不像那些简单的JSON接口直接返回明文数据而是对核心的图片列表、详情信息进行了AES加密。对于刚接触JS逆向或者数据解密的新手来说这是一个绝佳的“练手级”项目难度适中能完整走通“抓包 - 定位加密 - 分析参数 - Python复现”的全流程。说它难度一般是因为它的加密模式通常是CBC和填充方式PKCS7都是标准操作没有魔改关键点在于找到那个加解密的“钥匙”——密钥Key和初始化向量IV。这个项目能帮你解决什么问题呢最直接的就是当你发现一个网站的数据在Network里看是一堆乱码或者返回的是一大串看似无意义的字符时你不再束手无策。你会知道这背后很可能就是对称加密在作祟而AES是其中最常见的选手。通过这个案例你将掌握如何从纷繁复杂的JavaScript文件中定位到加解密函数提取出关键的加密参数并最终在Python端完美复现解密过程拿到结构清晰的原始数据。无论你是想批量下载壁纸构建自己的素材库还是单纯想研究一下前端数据安全策略这个实战都能给你提供一套清晰、可复用的方法论。2. 逆向分析的核心思路与抓包定位逆向的第一步永远是观察。打开目标壁纸网站按F12进入开发者工具切换到Network网络面板勾选上“Preserve log”保留日志。然后进行一个典型的操作比如点击“加载更多”或者切换壁纸分类。这时你会看到一系列的网络请求刷出来。我们的目标不是那些.jpg或.png的图片文件本身而是告诉浏览器这些图片在哪里、是什么的数据接口。通常这类接口会返回一个列表包含图片ID、标题、缩略图URL、高清图URL等信息。所以我们要在请求列表中寻找类型为XHR或Fetch的请求其响应Response内容看起来不是HTML也不是直观的JSON而是一大串像是Base64编码的字符串或者完全不可读的乱码。找到它这个请求的URL就是我们的主战场。点击这个请求查看其详细信息。在“Headers”选项卡里留意请求方法通常是POST、请求URL以及可能携带的请求参数Payload。在“Response”选项卡里你看到的可能就是加密后的密文。我们的核心任务就是找到浏览器是如何将这段密文变成我们能读懂的JSON数据的。注意有些网站可能会对请求参数也进行加密。所以在“Payload”里如果看到类似dataxxxxxx这样一大串的值也需要留意解密流程可能是双向的。思路很明确既然浏览器能解密说明解密的逻辑一定在前端JavaScript代码里。接下来我们要在浩如烟海的JS文件中找到它。这里有两个非常实用的技巧搜索关键词法在开发者工具的“Sources”源代码面板或“Network”面板已加载的JS文件中使用全局搜索CtrlShiftF。搜索的关键词可以包括decrypt、AES、CryptoJS一个常用的前端加密库、mode、padding、iv、key等。如果网站使用了CryptoJS那定位会非常快。XHR/Fetch断点法在“Sources”面板找到“XHR/Fetch Breakpoints”区域点击“”号添加一个包含你刚才找到的接口URL关键部分的断点。例如如果接口URL包含/api/wallpaper/list就添加这个字符串。然后重新触发请求代码执行会在发起这个网络请求前一刻暂停此时调用栈Call Stack会清晰地展示出是哪个JS函数发起的请求顺藤摸瓜就能找到数据处理的逻辑。通过以上方法我们大概率能定位到一个负责解密的函数。它可能长这样一个简化的示例function decryptData(encryptedData) { var key CryptoJS.enc.Utf8.parse(一个16/24/32位的字符串); var iv CryptoJS.enc.Utf8.parse(一个16位的字符串); var decrypted CryptoJS.AES.decrypt(encryptedData, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8)); }看到这个函数你就成功了一大半。接下来要做的就是把这行CryptoJS.AES.decrypt调用用Python的加密库如pycryptodome一模一样地还原出来。3. 关键加密参数解析与Python环境准备从定位到的JS代码中我们需要提取出几个最关键的参数这是Python复现的基石密钥Key一个字符串。在AES中它必须是16字节AES-128、24字节AES-192或32字节AES-256长。JS里通常用CryptoJS.enc.Utf8.parse将其从UTF-8字符串转换成WordArray一种内部表示。在Python中我们直接使用这个字符串的UTF-8编码字节即可。务必确认长度这决定了我们使用AES的哪种强度。初始化向量IV一个16字节的字符串。作用是为CBC密码分组链接模式提供初始的“随机性”确保同样的明文加密多次后密文不同。同样由Utf8.parse转换。加密模式Mode最常见的是CBC密码分组链接模式本例就是。其他还有ECB不推荐不安全、CFB等。填充方式Padding最常见的是Pkcs7。在AES块加密中明文长度必须是16字节的倍数不足的部分就需要填充。PKCS7是标准填充方式。密文格式网络传输的密文通常是Base64编码的字符串。JS的CryptoJS.AES.decrypt方法能自动处理Base64编码的密文。在Python中我们需要先对密文字符串进行Base64解码得到原始的加密字节串。在开始写Python代码前需要准备好环境。推荐使用pycryptodome库它是pycrypto的一个积极维护的分支功能强大且稳定。# 使用pip安装 pip install pycryptodome如果你遇到网络问题可以使用国内镜像源加速例如pip install pycryptodome -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后就可以在Python脚本中导入关键的模块了from Crypto.Cipher import AES from Crypto.Util.Padding import unpad # 用于解密后移除填充 import base64 import json4. Python复现AES解密全流程假设我们已经通过抓包拿到了加密的响应数据是一个Base64字符串encrypted_b64_data并且从JS中提取出了key_str和iv_str。下面是完整的Python解密代码实现和逐行解析。import base64 import json from Crypto.Cipher import AES from Crypto.Util.Padding import unpad def decrypt_wallpaper_data(encrypted_b64_data, key_str, iv_str): 解密壁纸网站接口返回的AES加密数据。 Args: encrypted_b64_data (str): Base64编码的加密字符串。 key_str (str): 密钥字符串。 iv_str (str): 初始化向量字符串。 Returns: dict: 解密并解析后的JSON数据。 # 1. 将密钥和IV从字符串转换为字节串 # 注意必须确保是UTF-8编码与JS中的CryptoJS.enc.Utf8.parse对应。 key_bytes key_str.encode(utf-8) iv_bytes iv_str.encode(utf-8) # 2. 对Base64编码的密文进行解码得到原始的加密字节串 # 这是解密操作前必不可少的一步。 encrypted_bytes base64.b64decode(encrypted_b64_data) # 3. 创建AES解密器实例 # 参数说明 # key_bytes: 密钥字节串 # AES.MODE_CBC: 指定使用CBC模式 # iv_bytes: 初始化向量字节串 cipher AES.new(key_bytes, AES.MODE_CBC, iv_bytes) # 4. 执行解密操作 # 此时得到的是带有PKCS7填充的原始明文字节串。 decrypted_padded_bytes cipher.decrypt(encrypted_bytes) # 5. 移除PKCS7填充得到干净的明文字节串 # unpad函数需要知道AES的块大小16字节。 decrypted_bytes unpad(decrypted_padded_bytes, AES.block_size) # 6. 将明文字节串解码为UTF-8字符串并解析为JSON对象 decrypted_str decrypted_bytes.decode(utf-8) data_dict json.loads(decrypted_str) return data_dict # 实战调用示例 # 以下为模拟数据实际数据需从网络抓包获取 encrypted_data_from_network U2FsdGVkX1...很长一串Base64... # 替换为实际密文 secret_key thisIsASecretKey16 # 长度必须为16/24/32字节此处为16 secret_iv thisIsASecretIv # 长度必须为16字节 try: result decrypt_wallpaper_data(encrypted_data_from_network, secret_key, secret_iv) print(解密成功数据样例) # 美观打印JSON数据 print(json.dumps(result, indent2, ensure_asciiFalse)) except Exception as e: print(f解密失败{e})代码逻辑深度解析编码一致性第1步的encode(utf-8)至关重要。它确保了Python端和JS端使用完全相同的字节序列作为密钥和IV。如果JS端密钥是1234567812345678那么Python端也必须用UTF-8编码将其转为16个字节任何编码差异都会导致解密失败。Base64解码先行网络传输的密文为了可读性和避免传输问题几乎总是Base64编码的。CryptoJS.AES.decrypt能自动识别并解码但pycryptodome的cipher.decrypt方法只接受原始的加密字节串。所以我们必须手动b64decode。CBC模式与IV创建AES.new对象时因为我们分析出是CBC模式所以必须传入iv_bytes。如果是ECB模式极少见则不需要IV参数。填充移除解密后得到decrypted_padded_bytes其末尾包含填充字节。unpad函数的作用就是根据PKCS7规则移除这些填充还原出原始的数据长度。忘记这一步最后解码的字符串末尾会包含一些不可见的乱码可能导致json.loads()失败。异常处理整个解密过程可能因多种原因失败密钥错误、IV错误、密文损坏、填充错误等。用try...except包裹起来是良好的实践便于定位问题。5. 进阶处理动态密钥与完整爬虫集成在实际的逆向项目中事情往往不会这么简单。很多网站为了增加逆向难度不会将密钥硬编码在JS里。密钥可能是动态生成的例如根据时间戳或随机数生成密钥md5(当前日期 固定盐值)。从另一个接口获取先请求一个/api/getKey的接口拿到本次会话的密钥。隐藏在网页的全局变量或某个标签属性中需要解析初始的HTML页面来提取。面对动态密钥我们的策略需要升级追踪密钥生成逻辑在找到解密函数后不要只看那几行。向上回溯看key和iv这两个变量是从哪里来的。是调用了一个getEncryptionKey()函数还是从window.globalConfig这个对象里读取的顺着调用链或赋值链找下去。模拟生成过程如果发现密钥是通过Date()、Math.random()或对某些固定字符串进行MD5运算得到的那么我们就在Python里完全复现这个生成算法。这可能需要你稍微分析一下前端的工具函数。二次请求获取如果密钥是从另一个接口获取的那么你的爬虫流程就需要分两步第一步请求密钥接口拿到密钥和IV第二步用这个密钥去请求真正的数据接口并解密。下面是一个模拟动态密钥生成基于当天日期并与爬虫请求集成的增强版示例import requests import hashlib import time from datetime import datetime # ... 导入之前的解密函数所需模块 ... def get_dynamic_key_iv(): 模拟前端动态生成密钥和IV的逻辑。 # 假设前端用当天日期YYYYMMDD拼接一个盐值然后取MD5的前16位作为Key后16位作为IV today_str datetime.now().strftime(%Y%m%d) salt myWallpaperSalt raw_str today_str salt # 计算MD5 m hashlib.md5() m.update(raw_str.encode(utf-8)) md5_hex m.hexdigest() # 32位十六进制字符串 # 前16个字符32位hex中的16位作为Key 需要转换成32字节的字符串不对。 # 注意MD5结果是128位16字节用32位十六进制字符串表示。 # 如果我们想要一个16字节的Key和一个16字节的IV可以直接用其字节形式分割。 md5_bytes m.digest() # 获取16字节的MD5摘要 dynamic_key md5_bytes[:16] # 前8字节不对前16字节就是全部了。所以这个逻辑需要看前端具体实现。 dynamic_iv md5_bytes # 如果前端用整个MD5当IV那长度是16字节符合。 # 更常见的前端逻辑可能是md5_hex是32字符取前16字符作为key字符串后16字符作为iv字符串。 # 假设前端是这样做的 key_str md5_hex[:16] # 例如 e10adc3949ba59ab iv_str md5_hex[16:] # 例如 be56e057f20f883e # 那么Python端就需要用这两个字符串而不是字节。 # 这里为了演示我们采用字符串方案并在解密函数中encode。 return key_str, iv_str def spider_wallpaper_list(category_id, page): 爬取指定分类和页面的壁纸列表。 # 1. 获取动态密钥模拟前端逻辑 secret_key, secret_iv get_dynamic_key_iv() print(f[*] 本次动态密钥: key{secret_key}, iv{secret_iv}) # 2. 构造请求参数这里假设接口需要明文参数实际可能参数也被加密 api_url https://目标网站.com/api/wallpaper/list payload { category: category_id, page: page, size: 20 } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/json } # 3. 发送请求获取加密响应 try: response requests.post(api_url, jsonpayload, headersheaders, timeout10) response.raise_for_status() # 检查HTTP错误 encrypted_data response.text # 假设接口直接返回Base64密文字符串 # 有时可能返回的是JSON如 {code:0, data: Base64String} # 需要根据实际情况解析encrypted_data response.json()[data] except requests.exceptions.RequestException as e: print(f[!] 网络请求失败: {e}) return None # 4. 解密数据 try: decrypted_result decrypt_wallpaper_data(encrypted_data, secret_key, secret_iv) return decrypted_result except Exception as e: print(f[!] 数据解密失败: {e}) # 打印部分密文辅助调试 print(f[!] 密文前100字符: {encrypted_data[:100]}) return None # 执行爬虫 if __name__ __main__: data spider_wallpaper_list(category_id1, page1) if data and data.get(code) 0: # 假设返回结构有code和list字段 wallpaper_list data.get(list, []) print(f[] 成功获取到 {len(wallpaper_list)} 张壁纸信息) for item in wallpaper_list[:3]: # 打印前3条 print(f - ID:{item[id]}, 标题:{item[title]}, 缩略图:{item[thumb]}) else: print([-] 数据获取失败)这个进阶示例展示了如何将解密模块嵌入到一个完整的爬虫流程中并处理了动态密钥的场景。关键在于get_dynamic_key_iv函数它完全复现了你在JS分析中找到的密钥生成算法。6. 实战调试技巧与常见问题排查即使代码看起来完全复现了JS逻辑第一次运行时也大概率会报错。别慌这是逆向的常态。下面是一个系统性的排查清单问题一ValueError: Invalid padding bytes.或PKCS#7 padding incorrect原因这是最常见的问题意味着解密后移除填充时失败。排查步骤确认密钥和IV99%的问题出在这里。请用print(len(key_bytes), len(iv_bytes))确认密钥是16/24/32字节IV是16字节。确保字符串完全正确包括大小写和任何不可见字符。一个技巧是在JS控制台打印出key和iv的原始字符串值而不是WordArray对象。确认密文确保encrypted_b64_data是完整的、没有多余换行或空格的Base64字符串。可以尝试用在线Base64解码工具验证一下是否能正常解码不关心解码后内容只关心格式。确认加密模式100%确定是CBC吗有没有可能是其他模式回头检查JS代码。手动验证解密在JS控制台用你提取的key_str、iv_str和抓包到的encrypted_b64_data直接调用那个decryptData函数看浏览器能否成功解密。如果不能说明你提取的参数或密文有问题。问题二UnicodeDecodeError: utf-8 codec cant decode byte...原因解密出的字节串无法用UTF-8解码。这通常发生在解密本身已经出错得到了乱码字节或者原始明文根本不是UTF-8编码的JSON可能是其他二进制数据但壁纸接口通常都是JSON。排查步骤在json.loads之前先打印decrypted_bytes的十六进制表示print(decrypted_bytes.hex())或前几十个字节。如果开头不是{或[对应的字节0x7b或0x5b那解密肯定错了。回到问题一的排查步骤重点检查密钥和IV。问题三解密出的JSON结构混乱或者有乱码原因解密成功了但填充没移除干净或者加密模式/填充方式判断有误。排查步骤尝试不用unpad直接decode(utf-8, errorsignore)看看字符串末尾有没有奇怪的字符如\x04\x04\x04\x04这是PKCS7填充的典型特征证实了填充存在。确认JS中使用的padding确实是CryptoJS.pad.Pkcs7。有些老系统可能用ZeroPadding。极少数情况下前端可能先对数据进行了压缩如gzip再加密。你需要先解密然后对解密后的字节进行gzip.decompress。观察Network中Response Headers有没有Content-Encoding: gzip或者查看解密后字节的前两个字节是否是0x1f 0x8bgzip魔数。通用调试技巧二分法定位在Python中将JS的每一步操作都打印出中间结果如Key的字节、IV的字节、Base64解码后的密文长度等与在JS控制台执行相同步骤打印的结果进行比对。哪里开始不一样问题就出在哪里。使用Node.js模拟如果Python环境调试太痛苦可以写一个简单的Node.js脚本用CryptoJS库直接执行解密验证你的参数和密文是否正确。这能帮你快速确定问题是出在参数提取阶段还是Python复现阶段。留意字符编码JS的字符串是UTF-16但在CryptoJS.enc.Utf8.parse时它会把字符串当作UTF-8来处理。确保你的Python字符串在调用encode(utf-8)前内容与JS字符串完全一致。对于包含非ASCII字符如中文的密钥虽然很少见要格外小心。实操心得遇到解密失败首先保持冷静。按上述清单一步步走从最基础的密钥IV长度和字符串内容查起。大多数“灵异事件”都是因为复制密钥时多了一个空格或少了一个字母。善用浏览器的控制台进行实时调试和比对是效率最高的方法。7. 安全、伦理与扩展思考完成了技术破解我们有必要停下来思考一下边界。爬虫和数据解密是强大的技术但必须用在合规合法的范围内。遵守robots.txt检查目标网站的robots.txt文件看它是否禁止了对你所爬取路径的访问。这是网络爬虫最基本的礼仪。尊重版权与使用条款壁纸通常是创作者的作品。批量下载后如果用于商业用途或大规模分发很可能侵犯版权。务必查看网站的用户协议明确其内容的使用权限。个人学习、研究通常是可接受的但红线不能越。控制请求频率在爬虫代码中务必在请求间添加随机延时如time.sleep(random.uniform(1, 3))避免对目标网站服务器造成瞬时高并发压力这既是道德要求也能防止你的IP被屏蔽。数据用途将获取的数据用于分析、学习或构建个人的离线素材库是没问题的。但切勿公开传播原始数据或用于牟利。从技术扩展的角度看这个AES解密案例是一个很好的起点更复杂的加密你可能会遇到AES的GCM模式带认证、或者RSAAES混合加密。其核心思路不变定位算法、分析参数、模拟流程。混淆与反调试高级网站会使用JS混淆、代码压缩、反调试技术如无限debugger、检测开发者工具来增加逆向难度。这就需要你掌握更多的调试技巧比如使用“Never pause here”忽略调试语句或者学习AST抽象语法树解混淆的基本概念。自动化工具对于大量类似的网站可以考虑将密钥提取、解密逻辑模块化甚至尝试用execjs这类库直接在Python中调用一小段关键的JS代码避免复杂算法的Python重写。这个“难度一般”的AES逆向项目就像一把钥匙为你打开了一扇门。门后是更广阔的数据安全与逆向工程世界。掌握了从观察到定位、从分析到复现的这一整套方法未来面对更复杂的加密方案时你也就有了拆解它的信心和工具。