爬虫逆向实战:3DES加密原理与Python模拟实现详解
1. 项目概述为什么爬虫工程师必须懂3DES如果你正在和网站的反爬机制斗智斗勇尤其是那些数据被一层“密文”包裹着的网站那么“对称加密算法”绝对是你绕不开的一道坎。而3DES作为对称加密家族中一个承上启下的经典算法在今天的互联网数据交互中依然有它的身影。我处理过不少爬虫项目发现很多看似复杂的加密参数其底层核心就是3DES。不理解它你连数据包都看不懂更别提逆向还原出可用的明文数据了。简单来说3DESTriple Data Encryption Standard就是DES算法的“威力加强版”。在爬虫逆向的语境下它的角色通常是这样的网站服务器为了保护关键数据比如登录令牌、查询参数、列表数据等会在前端JavaScript代码中使用3DES算法进行加密然后将密文传输给后端。后端用对应的密钥解密后处理。我们的目标就是逆向分析出这个加密过程用Python模拟出来从而构造出合法的请求参数。这不仅仅是“找到加密函数然后调用”那么简单。你需要理解它的密钥管理是单密钥、双密钥还是三密钥、加密模式ECB、CBC、填充方式PKCS5以及初始化向量IV的使用。这些细节稍有差错生成的密文就无法被服务器正确解密你的爬虫也就卡在了第一步。接下来我会结合具体场景带你从原理到实战彻底拆解3DES在爬虫逆向中的应用。2. 核心原理拆解3DES如何为数据“上锁”要逆向先得搞懂正向流程。3DES的原理是理解一切的基础。2.1 从DES到3DES一次不够就来三次DESData Encryption Standard是上世纪70年代的标准密钥长度56位在当今算力下已不再安全。3DES的诞生并非设计一个全新算法而是巧妙地通过多次DES操作来增加强度。它主要有三种密钥方案密钥选项1三个独立密钥使用三个完全不同的56位密钥K1, K2, K3。加密过程是加密(K1) - 解密(K2) - 加密(K3)。这是最安全的方式有效密钥长度达到168位。解密过程则相反解密(K3) - 加密(K2) - 解密(K1)。密钥选项2两个独立密钥K1和K3使用同一个密钥。即加密(K1) - 解密(K2) - 加密(K1)。有效密钥长度112位。这是目前较为常见的实现。密钥选项1三个相同密钥K1K2K3。这实际上退化成了标准的DES仅用于向后兼容现已不推荐。在爬虫逆向中你遇到的绝大多数是密钥选项2两个独立密钥。因为它在安全性和性能之间取得了较好的平衡也是许多标准库如Python的pycryptodome的默认模式。注意很多前端加密库如CryptoJS在调用3DES时看似只传入了一个24字节192位的密钥。实际上这个长密钥在内部被等分成了K1前8字节、K2中间8字节、K3后8字节。如果K1等于K3就是密钥选项2的模式。这是分析源码时要留意的关键点。2.2 工作模式与填充决定加密的“样式”光有算法和密钥还不够3DES在实际使用时还需要指定“工作模式”和“填充方案”。这是爬虫逆向中最容易出错的地方。常见工作模式ECB电子密码本最简单的模式将明文分成块每块独立加密。缺点非常致命相同的明文块会产生相同的密文块容易受到模式分析攻击。在爬虫中如果数据本身规律性强如序列化的JSON有大量重复结构ECB模式会留下明显的“指纹”。CBC密码分组链接这是目前最常用的模式也是爬虫逆向的重点关注对象。它在加密前先将当前明文块与前一个密文块进行异或操作。对于第一个块需要一个“初始化向量”IV来参与异或。IV不需要保密但必须不可预测通常随机生成。CBC模式能很好地隐藏明文模式。填充方案因为DES/3DES是分组加密算法一次处理8个字节64位。如果明文长度不是8的倍数就需要填充。常见的是PKCS5/PKCS7填充两者在8字节分组下等价。例如如果最后一个块缺3个字节就填充3个值为0x03的字节。一个完整的加密流程CBC模式 PKCS5填充可以表示为Ciphertext 3DES_Encrypt_CBC(Key, IV, PKCS5_Padding(Plaintext))逆向时你的任务就是找到前端代码中对应的Key、IV、Mode和Padding。3. 逆向实战定位与分析前端3DES加密逻辑理论说再多不如动手跟一遍。假设我们遇到一个网站其搜索接口的keyword参数是一串看不懂的密文。3.1 抓包与初步判断首先用浏览器开发者工具F12的Network面板抓取搜索请求。你会发现POST数据或Query参数中有一个长得像Base64编码的长字符串例如paramsU2FsdGVkX14m...。观察特征虽然不能仅凭外观断定但经过3DES CBC加密后再Base64编码的数据本身没有特别固定的前缀。但你可以尝试一个简单判断改变搜索词明文观察密文是否完全改变。如果只是局部变化可能是ECB模式如果全部变化则可能是CBC等带IV的模式。搜索关键函数在Sources面板全局搜索CtrlShiftF与加密相关的关键词。对于3DES可以搜索TripleDES3DESCryptoJS.DES或CryptoJS.TripleDES(如果用了CryptoJS库)mode:和padding:(用于查找模式配置)encrypt、decrypt有时密钥是硬编码的字符串或可追溯的变量搜索key、secret、iv也可能有收获。3.2 逆向分析CryptoJS示例假设我们幸运地找到了类似下面的代码片段这是CryptoJS的典型用法function encryptData(word) { var key CryptoJS.enc.Utf8.parse(123456781234567812345678); // 24字节密钥 var iv CryptoJS.enc.Utf8.parse(01234567); // 8字节IV var encrypted CryptoJS.TripleDES.encrypt(word, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); // 默认输出OpenSSL格式的Base64字符串 }逐行分析key24字节的UTF-8字符串被解析成WordArray对象。这印证了之前说的“24字节密钥”。iv8字节的初始化向量。CryptoJS.TripleDES.encrypt调用3DES加密函数。选项{iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7}明确指定了CBC模式和PKCS7填充。encrypted.toString()将加密结果转换为字符串。CryptoJS默认会输出一个特殊的Base64字符串它实际上包含了“Salted__”前缀和盐值salt用于派生密钥和IV。这是一个超级重要的坑实操心得CryptoJS的“Salted”格式当你不传递iv参数或者以字符串形式直接传递key和iv时CryptoJS默认会使用一个随机盐salt并通过EVP_BytesToKey函数派生实际的加密密钥和IV。其输出格式是Salted__ 8字节盐值 实际密文。服务器端如PHP、OpenSSL通常也兼容这种格式。但在Python中模拟时你必须重现这个密钥派生过程或者确保前端代码像上面示例一样显式地、以WordArray形式提供key和iv这样才能避免使用随机盐。大多数为爬虫设计的前端加密为了保持一致性会采用显式指定key和iv的方式。3.3 无源码的Hook与调试如果代码被混淆或打包找不到清晰的函数定义怎么办这时需要动用动态调试技术。Hook CryptoJS在Console中注入代码重写CryptoJS.TripleDES.encrypt方法将其赋值给一个临时变量并在调用时打印出参数和结果。var originalEncrypt CryptoJS.TripleDES.encrypt; CryptoJS.TripleDES.encrypt function(message, key, cfg) { console.log([Hook] 3DES Encrypt Called!); console.log(Message:, message); console.log(Key:, key); console.log(Key (hex):, CryptoJS.enc.Hex.stringify(key)); console.log(Cfg:, cfg); var result originalEncrypt.apply(this, arguments); console.log(Ciphertext (Base64):, result.toString()); console.log(Ciphertext (Hex):, CryptoJS.enc.Hex.stringify(result.ciphertext)); return result; };执行搜索操作加密函数的详细信息就会在控制台打印出来密钥和IV一览无余。下断点调试在Network面板中找到加密参数生成的请求右键选择“Replay XHR”或“Copy as fetch”。在Initiator调用栈中一步步向上追溯找到触发加密的JavaScript代码行打上断点然后重新触发请求单步跟踪变量的值。4. Python模拟实现从逆向结果到可运行代码拿到密钥、IV、模式、填充方式后下一步就是用Python复现加密过程。这里强烈推荐使用pycryptodome库它功能完整且文档清晰。4.1 环境准备与基础加密首先安装库pip install pycryptodome假设我们逆向出的参数如下密钥Keyb123456781234567812345678(24字节)初始化向量IVb01234567(8字节)模式CBC填充PKCS5/PKCS7基础加密代码如下from Crypto.Cipher import DES3 from Crypto.Util.Padding import pad import base64 def encrypt_3des_cbc(plain_text, key, iv): 使用3DES CBC模式加密文本 :param plain_text: 明文字符串 :param key: 字节串长度必须为16或24 :param iv: 字节串长度必须为8 :return: Base64编码的密文字符串 # 确保明文是字节串 plain_bytes plain_text.encode(utf-8) # 使用PKCS7填充与PKCS5在8字节块下相同 padded_bytes pad(plain_bytes, DES3.block_size) # 创建加密器使用CBC模式 cipher DES3.new(key, DES3.MODE_CBC, iv) # 执行加密 cipher_bytes cipher.encrypt(padded_bytes) # 将密文转换为Base64便于网络传输 cipher_b64 base64.b64encode(cipher_bytes).decode(utf-8) return cipher_b64 # 使用示例 key b123456781234567812345678 # 24字节密钥 iv b01234567 # 8字节IV plaintext {page:1,keyword:手机} encrypted_data encrypt_3des_cbc(plaintext, key, iv) print(f加密结果: {encrypted_data})4.2 处理CryptoJS的“Salted”格式如果前端使用的是CryptoJS的默认行为即没有显式传递IV输出了带Salted__前缀的字符串那么Python端也需要模拟其密钥派生过程。这稍微复杂一些。from Crypto.Cipher import DES3 from Crypto.Protocol.KDF import PBKDF1 from Crypto.Util.Padding import pad import base64 import hashlib def encrypt_3des_cbc_openssl_format(plain_text, passphrase, saltNone): 模拟CryptoJS/OpenSSL的3DES加密输出Salted__格式 :param plain_text: 明文字符串 :param passphrase: 密码字符串前端传入的key字符串 :param salt: 盐值8字节如果为None则随机生成 :return: OpenSSL兼容的Base64密文字符串 if salt is None: salt b\x00 * 8 # 简单示例实际应使用os.urandom(8)生成随机盐 # 1. 使用PBKDF1派生密钥和IV (与CryptoJS的EVP_BytesToKey兼容) # 注意CryptoJS使用MD5迭代次数为1 key_iv PBKDF1(passphrase.encode(utf-8), salt, 16, count1, hashAlgohashlib.md5) # 先取16字节 key_iv PBKDF1(passphrase.encode(utf-8) key_iv[:8], salt, 8, count1, hashAlgohashlib.md5) # 再取8字节 # 派生出的前24字节作为3DES密钥后8字节作为IV key key_iv[:24] iv key_iv[24:32] # 2. 加密明文 plain_bytes plain_text.encode(utf-8) padded_bytes pad(plain_bytes, DES3.block_size) cipher DES3.new(key, DES3.MODE_CBC, iv) cipher_bytes cipher.encrypt(padded_bytes) # 3. 组合成OpenSSL格式: Salted__ salt ciphertext openssl_bytes bSalted__ salt cipher_bytes # 4. Base64编码 cipher_b64 base64.b64encode(openssl_bytes).decode(utf-8) return cipher_b64 # 使用示例模拟前端只传了一个密码字符串的情况 passphrase mySecretPass plaintext search query encrypted encrypt_3des_cbc_openssl_format(plaintext, passphrase) print(fOpenSSL格式加密结果: {encrypted})注意事项密钥与IV的生成一致性这是爬虫模拟加密时失败率最高的环节。前端JavaScript尤其是CryptoJS和后端可能是Java、PHP、Python在将字符串密码转换为实际加密密钥和IV时使用的算法如PBKDF、EVP_BytesToKey、哈希函数MD5、SHA1、迭代次数、盐值处理方式必须完全一致。在逆向时一定要通过Hook或调试确认前端最终传入加密函数的key和iv的确切字节值而不仅仅是字符串。最稳妥的方法是在Python中直接使用这些字节值而不是尝试重现其派生过程。4.3 解密验证确保双向可逆为了百分百确认我们的Python加密代码是正确的一个最好的验证方法是用Python加密然后用同一段Python代码解密看是否能还原明文。更进一步可以尝试用前端JavaScript代码解密Python生成的密文或者用Python解密前端生成的密文。Python解密函数from Crypto.Cipher import DES3 from Crypto.Util.Padding import unpad import base64 def decrypt_3des_cbc(cipher_b64, key, iv): 使用3DES CBC模式解密 :param cipher_b64: Base64编码的密文字符串 :param key: 字节串长度必须为16或24 :param iv: 字节串长度必须为8 :return: 明文字符串 # Base64解码 cipher_bytes base64.b64decode(cipher_b64) # 创建解密器 cipher DES3.new(key, DES3.MODE_CBC, iv) # 执行解密 padded_bytes cipher.decrypt(cipher_bytes) # 去除填充 plain_bytes unpad(padded_bytes, DES3.block_size) return plain_bytes.decode(utf-8) # 验证测试 key b123456781234567812345678 iv b01234567 plaintext 这是一条测试数据 encrypted encrypt_3des_cbc(plaintext, key, iv) print(f加密后: {encrypted}) decrypted decrypt_3des_cbc(encrypted, key, iv) print(f解密后: {decrypted}) assert plaintext decrypted, 加解密验证失败 print(加解密验证成功)5. 爬虫集成与参数构造实战现在我们已经有了可靠的3DES加密函数接下来就是把它集成到爬虫中动态构造请求参数。5.1 场景模拟加密搜索参数假设目标网站/api/search接口接收一个JSON格式的请求体其中encryptedParams字段是经过3DES加密的搜索条件。未加密的原始参数可能是{ page: 1, size: 20, keyword: 笔记本电脑, sort: price_asc }爬虫构造流程import requests import json from your_3des_module import encrypt_3des_cbc # 导入之前写好的加密函数 def build_encrypted_payload(key, iv, search_keyword, page1): 构造加密的请求参数 # 1. 构造原始参数字典 raw_params { page: page, size: 20, keyword: search_keyword, sort: price_asc } # 2. 将字典转换为JSON字符串 # 注意必须确保JSON序列化的格式如空格、键顺序与前端完全一致。 # 有时前端会使用 JSON.stringify(params, null, 0) 来压缩格式。 params_json json.dumps(raw_params, separators(,, :), ensure_asciiFalse) # separators(,, :) 移除空格这是常见优化需根据实际情况调整 # 3. 使用3DES加密JSON字符串 encrypted_b64 encrypt_3des_cbc(params_json, key, iv) # 4. 构造最终请求体 payload { encryptedParams: encrypted_b64, # 可能还有其他固定参数如时间戳、版本号等 timestamp: int(time.time() * 1000), appVersion: 1.0.0 } return payload def search_products(keyword): key b从逆向中获取的24字节密钥 iv b从逆向中获取的8字节IV url https://target-site.com/api/search # 构造加密载荷 payload build_encrypted_payload(key, iv, keyword) # 添加必要的请求头User-Agent, Content-Type等 headers { User-Agent: Mozilla/5.0..., Content-Type: application/json;charsetUTF-8, # 可能还需要Cookie或特定的Auth Header } # 发送请求 response requests.post(url, jsonpayload, headersheaders) if response.status_code 200: # 响应数据可能也是加密的需要同样的方式解密 resp_data response.json() encrypted_result resp_data.get(data) if encrypted_result: # 假设响应也是3DES加密使用相同的key/iv解密注意响应可能使用不同的密钥 decrypted_result decrypt_3des_cbc(encrypted_result, key, iv) return json.loads(decrypted_result) else: print(f请求失败: {response.status_code}) return None5.2 处理动态密钥与IV更复杂的情况是密钥和IV不是硬编码的而是每次请求动态生成的例如从服务器接口获取一个临时token或由前端根据时间戳计算得出。这就需要你进一步逆向密钥的生成逻辑。从接口获取在页面加载或初始化时网站可能会通过一个公开的接口返回一个加密用的key或seed。你需要先请求这个接口提取出密钥材料。本地计算密钥可能由一些固定字符串和可变参数如用户ID、时间戳、随机数拼接后再经过MD5、SHA1等哈希函数计算得出。你需要通过Hook或静态分析找到这个计算函数并用Python复现。# 示例密钥由固定字符串加时间戳取整后MD5生成 import hashlib import time def generate_dynamic_key(): fixed_str static_salt_ timestamp int(time.time()) // 60 # 每分钟变化一次 raw_key fixed_str str(timestamp) # 取MD5的前24位字符作为密钥注意这里是字符不是字节。前端可能再做一次转换 md5_hash hashlib.md5(raw_key.encode()).hexdigest()[:24] # 前端可能将hex字符串转换为字节或者直接作为字符串使用 # 需要根据实际情况确认这里假设前端用 CryptoJS.enc.Utf8.parse(md5_hash) return md5_hash.encode(utf-8) # 或 bytes.fromhex(md5_hash)6. 常见问题排查与调试技巧实录即使按照步骤操作模拟加密也可能失败。下面是我踩过无数坑后总结的排查清单。6.1 密文比对从十六进制入手最直接的验证方法是比对密文的十六进制Hex值而不是Base64字符串。Base64编码可能因为换行符、填充字符导致视觉差异但Hex是纯数据表示。操作步骤在浏览器中通过Hook或控制台获取前端加密结果的密文字节数组的Hex字符串。在CryptoJS中可以通过CryptoJS.enc.Hex.stringify(ciphertext.ciphertext)获得。在你的Python代码中加密后不要立即Base64编码先输出密文字节数组的Hex表示cipher_bytes.hex()。对比两个Hex字符串。如果完全一致恭喜你加密过程完全正确。如果不一致问题一定出在密钥、IV、明文、模式或填充上。6.2 分步隔离定位法当Hex值不一致时采用“分步隔离”法定位问题检查明文输入是否一致问题前端加密的明文到底是什么是一个JSON字符串吗字符串末尾有换行符吗键的顺序是否固定排查Hook前端加密函数打印出message.toString(CryptoJS.enc.Utf8)和message的Hex值。在Python中同样打印plain_bytes.hex()进行比对。检查密钥和IV的字节值是否一致问题这是最常见的错误来源。你以为的密钥字符串在前端可能被CryptoJS.enc.Utf8.parse或CryptoJS.enc.Hex.parse处理过。排查Hook前端打印key和iv的Hex值CryptoJS.enc.Hex.stringify(key)。确保Python中使用的key和iv字节串与之一模一样。检查加密模式和填充方式问题默认模式是不是ECB填充是不是NoPadding排查仔细查看前端加密函数的配置对象。如果没指定CryptoJS的默认模式是CBC默认填充是PKCS7。但其他库可能不同。检查是否有额外的编码或处理问题加密后前端是否对结果进行了二次处理比如先转Hex再Base64或者进行了URL编码。排查追踪加密函数返回的结果直到它被放入请求参数前的每一步转换。6.3 典型错误案例表错误现象可能原因解决方案Python加密结果长度与前端不同填充方式不一致。前端可能是ZeroPadding或NoPadding。确认前端使用的padding方案。Python中pad函数可指定padmode如pad(..., stylepkcs7)或使用zero_pad。服务器返回“解密失败”或“参数错误”密钥或IV字节值错误。最常见的是字符串到字节的转换方式不对。使用Hex比对法确保密钥/IV字节级一致。检查前端是否有parse过程Utf8/Hex/Base64。只有第一次请求成功后续失败IV是固定的但服务器可能要求每次加密使用随机IV。或者CBC模式需要前一个密文块作为下一个的IV在流式加密中需要链式传递。确认IV的生成逻辑。如果是随机IV需要将IV本身明文和密文一起发送给服务器通常拼接在密文前。加密结果Hex值前16位相同后面不同明文、密钥、IV、模式都正确但明文的前8个字节相同。这恰好说明是CBC模式因为第一个块加密结果相同后续块因IV链式影响而不同。检查从第二个明文块开始的内容是否不同。这通常是正常的重点确认第一个块的加密结果是否完全匹配。完全无法找到加密函数代码被重度混淆加密可能被封装在WebAssembly中或者使用了不常见的库。尝试搜索特征字节或字符串。在Network面板观察请求发起前的最后一个栈帧。使用“XHR/fetch Breakpoints”功能在发送请求时断住反向追溯。6.4 高级技巧使用Node.js作为“桥梁”验证当Python模拟异常复杂尤其是遇到CryptoJS那种独特的密钥派生逻辑时一个取巧但极其有效的方法是用Node.js直接执行前端的加密函数。将你找到的包含完整加密逻辑的JavaScript代码片段保存为一个.js文件。在Node.js环境中使用node -e执行这段代码或者写一个简单的脚本导出加密函数。在Python爬虫中使用subprocess模块调用这个Node.js脚本传入明文参数获取加密结果。这样能100%保证加密结果与浏览器端一致。虽然增加了系统依赖但在验证阶段和解决疑难杂症时非常有用。稳定后可以再根据Node.js的执行过程慢慢推导出纯Python的实现。逆向3DES加密就像在解一个结构已知但参数未知的锁。核心在于细致入微的观察和精准的比对。每一次成功的逆向都是对前端代码逻辑的一次深刻理解。记住Hex比对是你的终极武器它能将模糊的问题转化为精确的数据差异帮你快速定位到那个出错的字节。