实战剖析:从微信小程序反编译到AES加解密爬虫的完整链路
1. 微信小程序反编译基础准备
第一次接触微信小程序反编译时,我像大多数技术爱好者一样既兴奋又忐忑。微信小程序的.wxapkg文件本质上是个经过加密的压缩包,里面藏着小程序的前端源码和资源文件。要拿到这些资源,我们需要一套完整的工具链。
在Windows环境下,我推荐使用以下工具组合:
- Node.js环境:这是运行反编译脚本的基础,建议安装LTS版本(如16.x)
- 微信开发者工具:官方提供的IDE,可以用来验证反编译结果
- 解密工具pc_wxapkg_decrypt:专门用于解密微信缓存中的.wxapkg文件
- 反编译脚本wxappUnpacker:将解密后的包还原为可读的源代码
安装Node.js时有个小技巧:一定要勾选"Add to PATH"选项,否则后续命令行操作会遇到麻烦。验证安装是否成功可以运行:
node -v npm -v获取小程序包的过程很有意思。先在PC微信里打开目标小程序(比如某个外卖小程序),这时微信会在本地缓存目录生成加密包。具体路径通常是:
C:\Users\[用户名]\Documents\WeChat Files\Applet\[小程序ID]这里有个容易踩坑的地方:不同微信版本可能缓存路径略有不同。如果找不到,可以尝试在Everything等搜索工具中直接查找"APP.wxapkg"文件。
2. 解密与反编译实战操作
拿到加密的.wxapkg文件后,真正的挑战才开始。我遇到过不少解密失败的情况,大多数是因为路径中包含中文或空格。这里分享一个已验证可用的解密命令:
pc_wxapkg_decrypt.exe -wxid wxd418ee346d79d382 -in "C:\path\to\__APP__.wxapkg"成功解密后会生成dec.wxapkg文件。接下来用wxappUnpacker进行反编译:
node wuWxapkg.js ../decrypt/dec.wxapkg反编译过程中可能会遇到各种报错,最常见的是:
- 模块缺失错误:需要npm install安装依赖
- 文件损坏错误:可能解密不完整,需要重新操作
- 内存溢出错误:大程序包需要增加Node内存限制
反编译成功后,你会看到完整的项目结构:
- pages/ :页面组件目录
- utils/ :工具函数
- app.js :小程序入口文件
- app.json :全局配置
用微信开发者工具导入项目时,记得选择"导入项目"而不是"新建项目",并确保AppID填写正确(可以随便填测试号)。
3. 逆向分析网络请求
有了源码后,我开始寻找关键API接口。推荐使用Fiddler或Charles抓包,配合微信开发者工具的"网络"面板。这里有个实用技巧:在源码中搜索关键词如"request"、"wx.request"可以快速定位网络请求代码。
分析加密参数时,我通常采用"三板斧":
- 全局搜索:找encrypt、decrypt、AES、CBC等关键词
- 调用追踪:从wx.request入手回溯参数生成过程
- 断点调试:在开发者工具中设置断点观察实时数据
在某次分析中,我发现加密逻辑藏在utils/crypto.js里。关键代码段如下:
function encryptData(data) { const key = CryptoJS.enc.Utf8.parse("f13df6c54e8efdfe"); const iv = CryptoJS.enc.Utf8.parse("a3648c7c1ef3e9fe"); return CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString(); }特别注意padding方式(这里是Pkcs7),这个细节直接影响后续Python复现的成功率。headers中的常见加密字段包括:
- sign:参数签名
- timestamp:时间戳
- nonce:随机字符串
- encryptedData:加密的业务数据
4. Python复现AES加密逻辑
将JavaScript的加密逻辑移植到Python需要特别注意数据格式处理。这是我的实现方案:
from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 import json class WXEncryptor: def __init__(self): self.key = 'f13df6c54e8efdfe'.encode('utf-8') self.iv = 'a3648c7c1ef3e9fe'.encode('utf-8') def encrypt(self, data): if isinstance(data, dict): data = json.dumps(data, ensure_ascii=False) cipher = AES.new(self.key, AES.MODE_CBC, self.iv) padded_data = pad(data.encode('utf-8'), AES.block_size, style='pkcs7') encrypted = cipher.encrypt(padded_data) return base64.b64encode(encrypted).decode('utf-8') def decrypt(self, encrypted_data): cipher = AES.new(self.key, AES.MODE_CBC, self.iv) decrypted = cipher.decrypt(base64.b64decode(encrypted_data)) return decrypted[:-decrypted[-1]].decode('utf-8') # PKCS7 unpadding实际使用时可能会遇到以下问题:
- 编码问题:确保所有字符串统一用UTF-8编码
- 填充问题:JavaScript和Python的PKCS7实现可能有细微差异
- 字节对齐:AES要求数据长度是16字节的倍数
测试加密结果是否与小程序一致:
encryptor = WXEncryptor() test_data = {"app_type":"Wechat","version":"3.0"} encrypted = encryptor.encrypt(test_data) print(f"加密结果: {encrypted}")5. 构建完整爬虫链路
有了加密算法,就可以构建完整的爬虫了。我的爬虫架构通常包含以下模块:
import requests class WXSpider: def __init__(self): self.session = requests.Session() self.encryptor = WXEncryptor() self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'Content-Type': 'application/json' } def build_payload(self, params): """构造加密请求体""" timestamp = int(time.time() * 1000) payload = { **params, "timestamp": timestamp, "nonce": str(random.randint(1, 999999)) } return { "data": self.encryptor.encrypt(payload), "sign": self._generate_sign(payload) } def _generate_sign(self, params): """生成签名""" param_str = '&'.join([f'{k}={v}' for k,v in sorted(params.items())]) return hashlib.md5((param_str + 'your_salt').encode()).hexdigest() def fetch_data(self, api_url, params): """发送加密请求""" encrypted = self.build_payload(params) resp = self.session.post(api_url, json=encrypted, headers=self.headers) if resp.status_code == 200: return self.encryptor.decrypt(resp.json()['encryptedData']) return None实际使用时会遇到的反爬机制及应对策略:
- IP限制:使用代理池轮换IP
- 行为验证:控制请求频率,模拟人工操作间隔
- 参数校验:确保timestamp、nonce等参数符合目标系统的预期
6. 常见问题与调试技巧
在这个项目中我踩过不少坑,这里分享几个典型案例:
案例一:加密结果不一致现象:Python和JavaScript加密结果不同 排查步骤:
- 确认key和iv完全一致(包括编码方式)
- 检查padding方式(PKCS7在不同语言库中实现可能有差异)
- 验证输入数据是否完全相同(特别是JSON字段顺序)
案例二:解密后乱码解决方法:
# 正确的PKCS7 unpadding实现 def pkcs7_unpad(data): pad_len = data[-1] return data[:-pad_len]案例三:请求返回403可能原因:
- headers缺少必要字段(如Referer)
- 签名算法有细微差别
- 请求频率过高
调试时建议使用对比工具(如Beyond Compare)逐字节比对加密结果。对于复杂问题,可以构造最小测试用例:
# 最小测试用例 plaintext = "test1234" js_encrypted = "已知的JavaScript加密结果" py_encrypted = encryptor.encrypt(plaintext) assert js_encrypted == py_encrypted7. 进阶技巧与优化建议
经过多个项目实践,我总结出一些提升效率的方法:
代码混淆应对遇到混淆代码时,可以使用:
- AST解析工具分析代码结构
- 变量名重命名(如将_0x12ab3c改为更有意义的名称)
- 控制流平坦化还原
性能优化当需要处理大量数据时:
# 使用连接池 adapter = requests.adapters.HTTPAdapter( pool_connections=100, pool_maxsize=100 ) session.mount('http://', adapter) # 异步处理(aiohttp示例) async def async_fetch(url, params): async with aiohttp.ClientSession() as session: async with session.post(url, json=params) as resp: return await resp.json()自动化监控对于长期运行的项目,建议添加:
- 心跳检测(定期验证加密是否仍然有效)
- 异常报警(如响应结构变化)
- 自动降级机制(当主算法失效时切换备用方案)
法律与道德提醒需要特别注意:
- 仅对自有或授权的小程序进行分析
- 控制请求频率避免对目标服务器造成压力
- 不获取、不存储用户隐私数据
- 遵守robots.txt协议
