
1. 项目概述为什么我们要关注小程序加密最近在做一个项目涉及到与某第三方小程序进行数据交互对方的数据传输是加密的。这让我想起了几年前刚开始接触小程序开发时对网络请求“裸奔”的担忧。现在越来越多的应用尤其是涉及用户隐私、交易支付的小程序都会在前端与后端之间做一层额外的数据加密。这不仅仅是简单的HTTPS而是在HTTPS的传输层加密之上对业务数据本身再进行一次应用层的加密。简单来说HTTPS保证了数据在传输过程中不被窃听和篡改但它解决的是“通道”的安全。而应用层加密解决的是“内容”的安全。即使有人通过某些手段比如在用户手机上安装了恶意证书进行中间人攻击解密了HTTPS流量看到的也只是一堆无法直接理解的密文。这对于防止数据泄露、接口重放攻击、业务逻辑绕过等安全风险至关重要。如果你是一个小程序开发者正在为如何保护你的API数据而发愁或者你是一个安全研究员、逆向爱好者想了解小程序加密的常见套路和破解思路那么这篇文章就是为你准备的。我会从一个实际的分析案例出发拆解常见的加密方案、核心原理并分享一些实用的分析工具和思路。2. 核心加密方案解析从通用到定制小程序的加密方案并非千篇一律但大体上可以归为几类。理解这些方案是进行分析的第一步。2.1 微信官方方案加密网络通道微信官方为小程序开发者提供了两种增强数据安全性的方案这在官方文档“小程序加密网络通道”中有明确说明。这是最正规、也是最值得优先排查的方案。第一种是API自实现方案。其核心是微信平台维护了一个“用户维度的可靠Key”。小程序前端可以通过wx.getUserCryptoManager().getLatestUserKey()这个API获取到一个包含encryptKey加密密钥、iv初始化向量、version密钥版本和expireTime过期时间的对象。后端则可以通过微信提供的服务端接口getUserEncryptKey获取同一用户最近三次的密钥。拿到密钥后加解密算法需要开发者自己实现通常采用AES对称加密。这种方案将密钥的管理和分发交给了微信开发者只需关注加解密逻辑安全性较高因为密钥与用户绑定且有时效性。第二种是微信网关方案。这是更“傻瓜式”的一键接入方案。开发者接入后小程序到服务端的通信链路会从公网自动切换到微信的专线链路并且全程由微信网关进行二层加密。对开发者而言几乎无需编写额外的加解密代码性能和安全性都由微信保障。在分析时如果发现请求的域名并非业务服务器域名而是指向微信的特定网关域名那么很可能就采用了这种方案。这种方案的分析重点就从算法逆向转移到了对微信网关协议的理解上。注意官方方案的分析难点在于encryptKey是前端动态获取的每次可能不同且与用户会话绑定。单纯抓包看到的数据包如果没有对应的密钥是无法解密的。这要求分析必须在一个完整的、可登录的用户会话上下文环境中进行。2.2 常见的自定义加密方案很多团队出于灵活性或历史原因会选择自定义加密方案。这类方案五花八门但核心组件无外乎以下几种的排列组合固定密钥对称加密这是最简单也最不安全的一种。前端和后端约定好一个固定的AES密钥和IV或者一个固定的DES密钥。所有用户的加密数据都用同一把钥匙。分析时一旦通过逆向找到硬编码在代码里的密钥所有通信即可破解。常见于早期或对安全要求不高的项目。动态密钥协商安全性比固定密钥高。通常在会话初始化时前端生成一个随机密钥或参数通过非对称加密如RSA加密后传给后端后端用自己的私钥解密后双方后续就用这个随机密钥进行对称加密通信。分析的关键在于找到密钥协商的接口和RSA公钥。参数签名严格来说这不是加密而是防篡改。常见的是将请求参数按一定规则排序后加上一个密钥secret进行MD5、SHA256或HMAC运算得到一个签名sign随请求一起发送。后端用同样规则计算一遍校验签名是否一致。这可以防止参数被修改但参数本身是明文的。分析重点是找出排序规则和用于签名的secret。混合加密整合方案结合了上述多种方式。例如对核心业务数据用动态协商的AES密钥加密同时对整个请求包或关键参数计算一个HMAC签名。这是目前中大型项目比较常见的做法安全性较高。在分析目标小程序时第一步就是抓包观察。如果请求体和响应体是肉眼不可读的、类似Base64编码的字符串可能还带有填充那么很大概率是对称加密。如果请求中有类似key、encryptedData、iv、signature、timestamp等字段那基本就是上述某种或多种方案的组合。3. 分析工具与前期准备工欲善其事必先利其器。分析小程序加密你需要一个合适的“作战环境”。3.1 抓包工具的选择与配置抓包是获取原始通信数据的第一步。对于小程序主要有两种抓包思路系统代理抓包针对Android或越狱iOS使用Charles、Fiddler或Burp Suite等工具。需要在电脑上开启代理并将手机Wi-Fi的代理设置为电脑IP和端口。关键一步必须在手机上安装并信任抓包工具的根证书否则无法解密HTTPS流量。对于Android通常可以直接下载证书文件安装。对于微信小程序还需要在微信中打开调试功能早期版本可通过debugx5.qq.com开启现在通常需要在开发者工具或特定条件下开启并确保小程序代码包未开启“HTTP/2”协议部分抓包工具对HTTP/2支持不佳有时需要关闭“域名校验”等。本地代理或Hook无需复杂网络设置这是更推荐的方法尤其是对于新手。使用像Reqable、HTTP Toolkit这样的工具或者直接使用微信开发者工具。微信开发者工具自带网络请求监控可以清晰看到每个请求和响应。对于已上线的小程序可以尝试在开发者工具中导入项目需有源码或通过某些方式获取或者使用“真机调试”功能在电脑上监控真机小程序的网络请求。Reqable这类工具的优势在于它们专门针对移动端App包括小程序做了优化有时能绕过一些证书绑定SSL Pinning机制。我的经验是优先尝试微信开发者工具的真机调试或Reqable。如果不行再考虑配置系统全局代理。很多时候小程序为了兼容开发环境在非正式环境下如IP访问、特定端口会关闭严格的证书校验这给我们抓包提供了机会。3.2 逆向分析环境搭建抓包得到密文后就需要看前端代码是如何生成这些密文的。这就需要逆向小程序的代码包。获取小程序包在Android手机上小程序的运行包通常存放在/data/data/com.tencent.mm/MicroMsg/{一串哈希}/appbrand/pkg/目录下文件后缀为.wxapkg。需要root权限才能访问。也可以使用一些第三方工具或模拟器来获取。在微信开发者工具中运行过的小程序其包文件也会缓存在电脑的特定目录。解包工具获取到.wxapkg文件后使用wxappUnpacker等开源工具进行解包。解包后会得到小程序的源代码结构包括.wxml、.wxss、.js和.json文件。我们关注的核心是.js文件。代码分析与调试解包出来的.js文件通常是经过压缩和混淆的变量名可能变成a、b、c可读性极差。你需要使用代码编辑器如VSCode和浏览器开发者工具。将关键的.js文件导入到微信开发者工具的一个空项目中或者在一个本地HTTP服务器中运行然后利用浏览器Sources面板进行调试、断点、变量查看。搜索关键词如encrypt、decrypt、CryptoJS、AES、RSA、sign、key、iv等能快速定位加密函数所在。实操心得逆向的第一步往往是“找入口”。不要一上来就扎进混淆的代码里。先抓包看看请求的URL路径是什么比如/api/login或/api/order/submit。然后在解包的JS代码中全局搜索这个URL路径通常能找到发起这个网络请求的JavaScript函数。从这个函数入手向上追溯参数是如何构造的往往就能顺藤摸瓜找到加密函数。4. 实战分析流程抽丝剥茧假设我们现在面对一个名为“某物”的小程序其提交订单的接口POST /api/order/create的请求体是加密的。我们来模拟一遍分析流程。4.1 第一步网络抓包与初步观察使用抓包工具成功捕获到订单创建请求。POST https://api.something.com/api/order/create HTTP/1.1 Content-Type: application/json User-Agent: MicroMessenger/... ... { encryptedData: U2FsdGVkX1oH5Q7K...很长一串Base64, iv: 5f4dcc3b5aa765d61d8327deb882cf99, timestamp: 1689134225000, sign: a1b2c3d4e5f678901234567890abcdef }响应也是类似的格式{ code: 0, encryptedData: U2FsdGVkX1qv4QcK..., iv: fedcba9876543210fedcba9876543210 }初步判断这很明显是一个自定义的加密方案。请求体中没有key字段说明密钥可能不是每次请求都传输的。存在iv初始化向量强烈暗示使用了AES加密CBC模式需要IV。存在sign签名用于验证请求完整性。timestamp很可能用于防重放。4.2 第二步逆向定位加密函数解包小程序后在JS文件中全局搜索/api/order/create。假设我们找到了这样一个函数function createOrder(t) { var e getApp().globalData.userInfo; var a Date.now(); var r { productId: t.productId, quantity: t.quantity, addressId: t.addressId }; var i JSON.stringify(r); // 关键点1调用加密函数 var s o.encryptData(i, a); return n.request({ url: /api/order/create, method: POST, data: { encryptedData: s.data, iv: s.iv, timestamp: a, sign: s.sign } }); }很好我们找到了入口。它先构造了业务参数对象r将其转为字符串i然后调用了一个名为o.encryptData的函数传入明文数据i和时间戳a。这个函数返回的对象s包含了data密文、iv和sign。接下来我们就要找到这个o对象或者encryptData函数的定义。继续搜索encryptData。可能会找到类似下面的代码var o require(./utils/crypto.js);或者直接找到crypto.js文件。打开这个文件里面很可能包含了核心的加密和签名逻辑。4.3 第三步剖析加密与签名逻辑假设crypto.js内容如下经过反混淆和简化const CryptoJS require(./crypto-js.min.js); // 引入了CryptoJS库 const SECRET_KEY 固定的密钥字符串; // 注意这里可能是硬编码的密钥 function encryptData(plainText, timestamp) { // 1. 生成一个随机的16字节IV const iv CryptoJS.lib.WordArray.random(16); const ivHex iv.toString(CryptoJS.enc.Hex); // 2. 使用固定的SECRET_KEY和随机IV进行AES-CBC加密 const encrypted CryptoJS.AES.encrypt(plainText, CryptoJS.enc.Utf8.parse(SECRET_KEY), { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); const encryptedBase64 encrypted.toString(); // 3. 计算签名参数排序 密钥 MD5 const signStr encryptedData${encryptedBase64}iv${ivHex}×tamp${timestamp}key${SECRET_KEY}; const sign CryptoJS.MD5(signStr).toString(); return { data: encryptedBase64, iv: ivHex, sign: sign }; } module.exports { encryptData: encryptData };分析结果加密算法AES-CBC密钥SECRET_KEY是固定的硬编码在代码里。这是安全漏洞加密过程每次加密使用一个随机生成的IVIV会以明文形式ivHex发送给服务器。服务器用同样的固定密钥和收到的IV即可解密。签名算法将encryptedData、iv、timestamp和固定key按、连接成字符串然后计算其MD5值作为签名。服务器收到后会用同样的规则重新计算签名进行比对防止数据在传输中被篡改。4.4 第四步模拟加密与验证现在我们已经掌握了全部秘密算法AES-CBC、密钥硬编码的SECRET_KEY、IV请求中的iv字段、签名算法。我们可以用任何编程语言或工具来模拟加密过程验证我们的分析是否正确。以Python为例使用pycryptodome库from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 import hashlib import json import time # 从逆向代码中获取的固定密钥假设是this_is_a_secret_key的MD5前16位 SECRET_KEY bthis_is_a_secret # 16字节 for AES-128 # 或者如果是32字节则是 AES-256 def encrypt_order_data(product_id, quantity, address_id): # 1. 构造明文 plain_obj { productId: product_id, quantity: quantity, addressId: address_id } plain_text json.dumps(plain_obj, separators(,, :)) # 紧凑格式 plain_bytes plain_text.encode(utf-8) # 2. 生成随机IV (16字节) iv b\x12\x34\x56\x78 * 4 # 这里用固定值模拟实际应用应用随机生成 # 真实场景应从请求中获取iv # 3. AES-CBC加密 cipher AES.new(SECRET_KEY, AES.MODE_CBC, iviv) padded_bytes pad(plain_bytes, AES.block_size) encrypted_bytes cipher.encrypt(padded_bytes) encrypted_base64 base64.b64encode(encrypted_bytes).decode(utf-8) # 4. 计算签名 (假设iv是hex字符串) timestamp str(int(time.time() * 1000)) iv_hex iv.hex() sign_str fencryptedData{encrypted_base64}iv{iv_hex}×tamp{timestamp}key{SECRET_KEY.hex()} sign_md5 hashlib.md5(sign_str.encode(utf-8)).hexdigest() return { encryptedData: encrypted_base64, iv: iv_hex, timestamp: timestamp, sign: sign_md5 } # 测试 if __name__ __main__: data encrypt_order_data(1001, 2, addr_001) print(json.dumps(data, indent2))运行这段代码生成的encryptedData、iv、timestamp、sign应该能和抓包到的真实请求结构一致并且如果我们用相同的密钥和IV去解密服务器返回的encryptedData也应该能得到正确的明文响应。至此对于这个使用“固定密钥AES-CBC MD5签名”的小程序其加密和解密流程我们已经完全掌握。可以编写脚本进行自动化接口测试甚至在不打开小程序的情况下模拟用户行为。5. 进阶挑战与应对策略上面的案例是一个简单的固定密钥场景。现实中会遇到更多复杂情况。5.1 密钥动态获取与协商如果逆向时找不到硬编码的密钥那密钥很可能是动态获取的。搜索getLatestUserKey、getUserKey、/api/getKey、/api/init等关键词。查看小程序启动后最早发起的几个请求密钥很可能在其中某个接口的响应里。这种密钥可能有有效期需要定期刷新。分析时需要模拟完整的登录和初始化流程将获取密钥的步骤也纳入脚本。5.2 非对称加密RSA的参与如果请求中有一个sessionKey或encryptedKey字段且长度很长例如几百个字符的Base64而真正的业务数据encryptedData相对较短这可能是一个“混合加密”过程前端用RSA公钥加密了一个随机生成的AES密钥将加密后的结果作为encryptedKey发送然后用这个随机AES密钥加密业务数据得到encryptedData。服务器用RSA私钥解密encryptedKey得到AES密钥再用它解密encryptedData。应对策略在JS代码中搜索RSA、publicKey、encrypt与KEY相关。找到RSA公钥通常是一个很长的Base64字符串或模数n和指数e后虽然我们无法破解私钥但我们可以复用这个公钥。在我们的模拟脚本中用同样的公钥去加密我们自己生成的随机AES密钥从而模拟整个流程。这要求我们能完全复现前端的密钥生成和加密逻辑。5.3 代码混淆与反调试高级一点的小程序会进行严重的代码混淆变量名替换、控制流平坦化并加入反调试代码。例如在代码中检测console、debugger关键字或者检测是否运行在开发者工具中一旦发现就触发死循环或跳转到错误页面。应对策略使用无头浏览器或修改版环境在Node.js中使用Puppeteer运行小程序页面并注入代码覆盖掉反调试函数。动态Hook使用Frida等工具在运行时Hook关键的加密函数如CryptoJS.AES.encrypt直接打印出输入参数和输出结果。这是对付复杂混淆的利器因为它不关心代码逻辑只关心函数调用。耐心与经验即使代码被混淆字符串常量如API地址、加密算法模式名CBC、Pkcs7通常还是明文的。通过搜索这些字符串结合调用栈分析依然可以定位关键函数。5.4 微信官方加密网络通道分析如果确认小程序使用了微信官方的加密网络通道API自实现方案分析思路需要调整。核心是获取到wx.getUserCryptoManager().getLatestUserKey()返回的encryptKey和iv。由于这个Key是微信服务器下发的、与当前用户登录态绑定的你无法通过静态分析得到。你必须在一个真实的、已登录的用户会话中进行分析。方法在微信开发者工具的真机调试模式下或者通过某些方式将小程序的代码包加载到一个可以执行JavaScript并拦截API调用的环境中例如基于V8引擎的自制调试器然后HookgetLatestUserKey的成功回调函数将获取到的encryptKey和iv打印出来。一旦拿到了这次会话的密钥就可以用标准的AES算法通常是CBC模式对抓包到的encryptedData进行解密。但请注意这个密钥有过期时间过期后需要重新获取。6. 法律、道德与实用边界在进行任何形式的逆向分析前必须明确法律和道德的边界。仅用于授权测试与学习所有的分析行为应仅限于自己拥有完全所有权或已获得明确书面授权的应用程序。对于他人的小程序未经授权进行逆向、解密、抓包属于侵权行为可能违反《计算机软件保护条例》和《反不正当竞争法》甚至涉及非法获取计算机信息系统数据罪。目的正当分析的目的应该是为了学习安全技术、进行安全评估如公司对自己的产品进行渗透测试、或与第三方进行合法的技术对接在对方提供有限文档的情况下进行调试。绝对禁止用于窃取用户数据、伪造请求、刷单、篡改业务逻辑等非法用途。数据脱敏在学习和研究过程中避免使用真实用户的敏感数据。应使用测试账号和测试数据。尊重知识产权分析过程中获得的任何代码、算法、密钥都不应被用于复制、抄袭或开发竞争产品。从实用角度讲掌握小程序加密分析技能对于开发者而言能帮助你设计出更安全的通信方案了解常见漏洞所在对于安全工程师而言是进行移动端应用安全评估的必备技能对于技术爱好者而言则是深入理解现代Web应用架构的一扇窗口。关键在于将这项技术用在正道上。