得物小程序sign与data加密逆向分析:从抓包到Python算法还原实战
1. 项目概述从“黑盒”到“白盒”的逆向工程之旅最近在分析一些电商平台的数据接口时得物小程序成为了一个典型的研究案例。它的接口安全机制尤其是对请求参数sign和data的加密以及对响应体的解密构成了一个完整的“黑盒”通信流程。对于数据分析师、爬虫工程师或是安全研究人员而言理解并还原这套算法意味着能够以程序化的方式与平台进行“合规”的数据交互或者深入理解其安全设计逻辑。这不仅仅是破解几个参数那么简单它涉及到对小程序运行机制、前端JavaScript代码保护、以及后端加密逻辑的完整逆向分析链条。整个过程就像在解一个设计精巧的谜题每一步都需要耐心、细致的观察和逻辑推理。本文将带你完整走一遍得物小程序以5.19版本为例的sign签名生成、data参数加密以及响应体解密的算法还原过程。我们会使用主流的逆向分析工具如Charles/Fiddler抓包、Chrome开发者工具调试以及Python进行算法复现。目标读者是具有一定网络协议基础和JavaScript知识的开发者或安全爱好者通过本文你将不仅能掌握针对得物小程序的特定方法更能获得一套通用的、应对类似加密场景的逆向分析思路和实战技巧。2. 逆向分析环境搭建与初步抓包工欲善其事必先利其器。在开始逆向算法之前一个稳定、可控的分析环境至关重要。对于小程序的分析我们通常需要在真机上进行因为小程序的运行环境与浏览器有所不同其网络请求可能受到更多限制。2.1 核心工具链准备首先你需要准备以下工具抓包工具Charles或Fiddler。我个人更倾向于Charles它的界面更友好对HTTPS流量的解密支持也更成熟。确保安装好CA证书并配置好代理通常为手机和电脑在同一局域网手机Wi-Fi设置代理指向电脑IP和Charles监听端口如8888。调试工具PC端微信开发者工具或手机端调试。对于小程序最直接的方式是在PC微信中打开小程序然后利用微信开发者工具需开启调试模式进行动态调试。另一种更深入的方式是使用已Root的Android手机或越狱的iOS设备配合xposed、frida等框架进行Hook但这门槛较高。我们优先采用PC端调试方案。逆向分析工具Chrome/Edge开发者工具用于静态分析美化后的JavaScript代码设置断点进行动态调试。Python环境安装requests,execjs(用于执行JavaScript代码),Crypto(或cryptography) 等库用于算法验证和复现。文本编辑器/IDE如VSCode用于查看和编辑代码。2.2 关键配置与避坑指南配置抓包环境时有几个坑点需要特别注意注意微信小程序和部分App默认会校验系统证书。仅安装Charles的CA证书到用户目录可能不够需要将证书安装到系统信任区。在Android 7.0以上这通常需要Root后手动移动证书文件。对于iOS描述文件安装后需要在“设置-通用-关于本机-证书信任设置”中完全信任Charles证书。如果遇到“网络错误”或“证书验证失败”大概率是证书信任问题。启动Charles确保Proxy - SSL Proxying Settings中已经添加了需要解密的域名如*.duapp.com,*.dewu.com。然后在PC微信中打开得物小程序进行一些常规操作比如浏览商品列表、查看商品详情。此时Charles的会话列表Sequence中应该会出现大量的HTTPS请求。2.3 首次抓包与请求特征观察过滤出目标域名例如api.dewu.com的请求仔细观察其中一个典型的接口请求比如获取商品详情的接口。你会发现其POST请求的载荷Payload并非明文而是呈现为加密后的形态。通常关键信息会体现在**请求头Headers和请求体Body**中。一个典型的得物加密请求可能具有以下特征Headers中可能包含一个自定义的签名头如x-sign或sign。Query Parameters中可能也包含一个sign参数。Request Body不再是常见的JSON或表单格式而是一个经过加密的字符串可能以dataxxx的形式提交或者整个Body就是一个密文块。Response Body同样不是直接的JSON而是一串看似乱码的加密数据需要客户端解密后才能得到真正的JSON结构。我们的首要任务就是定位生成这些加密参数尤其是sign和加密的data的JavaScript代码在哪里以及它是如何工作的。3. 定位加密入口与核心逻辑分析小程序的前端代码虽然经过压缩和混淆但其核心逻辑必然存在于发送网络请求的模块中。微信小程序的网络请求主要使用wx.request方法。3.1 搜索与断点定位在微信开发者工具中打开得物小程序的调试模式切换到“源代码Sources”面板。由于代码被压缩我们可以使用全局搜索CtrlShiftF来寻找关键线索。搜索关键词策略直接搜索sign。这可能会找到很多处包括变量名、字符串等。可以尝试搜索sign带引号的字符串或sign:作为对象属性。搜索网络请求相关的关键词如wx.request,uni.request如果用了uni-app框架或者JSON.stringify,encrypt,md5,sha256,hmac,AES,CBC等加密相关词汇。搜索可能存在的自定义函数名比如getSign,encryptData,encodeParams等。这需要结合抓包看到的请求参数名进行猜测。通过搜索你很可能找到一个被高度混淆的JavaScript文件里面的变量名都是a,b,c,o,n等单字母。不要慌这是常态。我们的目标不是读懂每一行而是找到加密发生的“入口函数”。一个有效的方法是在搜索到疑似加密函数的地方比如一个函数里调用了CryptoJS库的方法或者有很长的字符串运算在该函数入口处打上断点。然后在小程序前端触发一个网络请求比如点击刷新商品列表。如果断点被触发那么恭喜你找到了关键位置。3.2 调用栈分析与参数追踪当断点命中时查看右侧的“调用栈Call Stack”。调用栈会显示当前函数是被谁调用的一层层回溯你就能找到整个加密调用的链条。通常这个链条的顶端会与wx.request的调用相关。在调试器中你可以查看当前作用域Scope中所有变量的值。重点关注传入这个加密函数的参数是什么它很可能是一个包含原始请求参数如商品ID、页码、时间戳等的JavaScript对象。这个函数内部对参数做了什么处理一步步单步执行F10观察变量的变化。注意看是否有以下操作添加固定参数如timestamp,nonce,appVersion,platform等。参数排序按照字母顺序对对象的键进行排序这是生成签名的常见步骤。字符串拼接将排序后的键值对拼接成特定格式的字符串如key1value1key2value2。拼接密钥在字符串的首尾或中间拼接一个固定的密钥secret或salt。哈希计算将拼接后的字符串进行MD5、SHA256等哈希运算得到sign。加密操作对完整的参数对象或data字段进行AES、RSA等加密。实操心得在单步调试时善用控制台Console。你可以将任何感兴趣的变量拖到控制台查看其完整内容或者直接输入表达式进行计算。例如当你看到拼接后的字符串s时可以在控制台输入s来确认其内容甚至可以手动计算md5(s)来验证是否与生成的sign一致。3.3 得物sign算法还原实例分析根据对历史版本和当前抓包的分析得物的sign生成算法虽然可能随版本更新但核心思路有迹可循。一个常见的模式是收集参数收集所有需要参与签名的参数包括URL查询参数Query String和请求体Body参数。对于POST请求Body参数可能是加密前的原始JSON对象。规范化处理排除某些不参与签名的字段如sign本身。对所有参数名进行字典序排序。将每个参数和值转换为字符串并进行URL编码或特定编码。构造待签名字符串将规范化后的参数以keyvalue的形式用连接起来形成字符串str_to_sign。拼接密钥在str_to_sign的末尾或开头拼接一个从服务器下发的、或硬编码在客户端的secret。这个secret的获取是逆向的另一个关键点可能藏在代码的常量里也可能通过某个初始化接口获取。计算哈希对拼接后的字符串计算MD5或SHA256并将结果转换为小写十六进制字符串即为最终的sign值。在调试器中你需要一步步验证这个过程。找到排序、拼接、哈希计算的具体代码行。将关键逻辑的JavaScript代码片段提取出来。4. data参数加密与响应体解密算法解析sign保证了请求的完整性和不可篡改性而data的加密则保证了请求/响应内容的机密性。得物很可能采用对称加密算法如AES来加密核心的业务数据。4.1 定位加密/解密函数在找到sign生成函数附近通常也能找到加密函数。搜索encrypt,decrypt,AES,CBC,mode,padding等关键词。同样通过断点调试来定位。当你在发送请求前的代码里找到加密调用时观察加密密钥Key和初始化向量IV从哪里来可能是固定的字符串也可能是通过某个算法动态生成的例如用sign的一部分作为Key。加密模式是什么常见的是AES-CBC或AES-ECB。CBC模式需要IV。填充方式是什么常见的是PKCS7填充。输出格式是什么通常是Base64编码或十六进制字符串。响应体的解密是加密的逆过程。你需要找到处理网络响应的地方可能是wx.request的success回调函数里在那里会有对返回数据进行解密的函数调用。其密钥、IV、模式、填充方式应与加密时一致。4.2 算法还原与Python实现一旦在调试器中理清了逻辑就可以开始用Python还原算法了。这里以最常见的AES-CBC-PKCS7加密和MD5签名为例。首先安装必要的库pip install pycryptodome requests4.2.1 Sign签名还原示例假设我们从逆向分析中得知sign的生成规则是将所有参数除sign外按key字典序排序拼接成key1value1key2value2的格式末尾拼接固定字符串secret123然后取MD5小写。import hashlib import urllib.parse def generate_sign(params, secretsecret123): 生成得物风格签名 :param params: dict, 请求参数字典 :param secret: str, 密钥 :return: str, 签名值 # 1. 移除sign参数本身如果存在 params.pop(sign, None) # 2. 对参数键进行排序 sorted_keys sorted(params.keys()) # 3. 构造待签名字符串 str_to_sign .join([f{k}{params[k]} for k in sorted_keys]) # 4. 拼接密钥 str_to_sign secret # 5. 计算MD5并返回小写十六进制 m hashlib.md5() m.update(str_to_sign.encode(utf-8)) return m.hexdigest() # 测试 test_params { page: 1, size: 20, timestamp: 1684567890123, nonce: abc123 } signature generate_sign(test_params) print(f生成的sign: {signature})4.2.2 Data加密解密还原示例假设加密采用AES-128-CBC密钥为16字节IV为16字节使用PKCS7填充。from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import base64 class DewuCrypto: def __init__(self, key: bytes, iv: bytes): 初始化加密器 :param key: 16/24/32字节的密钥 :param iv: 16字节的初始化向量 self.key key self.iv iv def encrypt_data(self, plaintext: str) - str: 加密数据返回Base64字符串 cipher AES.new(self.key, AES.MODE_CBC, self.iv) # 明文需要编码为bytes并进行PKCS7填充 padded_data pad(plaintext.encode(utf-8), AES.block_size) ciphertext cipher.encrypt(padded_data) return base64.b64encode(ciphertext).decode(utf-8) def decrypt_data(self, ciphertext_b64: str) - str: 解密Base64编码的密文返回明文字符串 cipher AES.new(self.key, AES.MODE_CBC, self.iv) ciphertext base64.b64decode(ciphertext_b64) decrypted_padded cipher.decrypt(ciphertext) # 去除PKCS7填充 plaintext_bytes unpad(decrypted_padded, AES.block_size) return plaintext_bytes.decode(utf-8) # 测试 # 注意真实的key和iv需要从逆向的代码中获取这里是示例 key bthisis16bytekey! # 16字节 iv bthisis16byteiv!! # 16字节 crypto DewuCrypto(key, iv) original_data {productId: 123456, skuId: 789} encrypted crypto.encrypt_data(original_data) print(f加密后的data: {encrypted}) decrypted crypto.decrypt_data(encrypted) print(f解密后的数据: {decrypted}) assert decrypted original_data重要提示以上key、iv、secret以及具体的拼接规则都是示例。你必须通过逆向分析从得物小程序的JavaScript代码中提取出真实的值和算法逻辑。这些关键信息可能以字符串常量的形式存在也可能通过更复杂的逻辑计算得出。5. 完整请求构建与算法验证在还原了sign和data的算法后下一步就是构建一个完整的、能够成功与服务器通信的请求。5.1 请求参数组装流程一个完整的自动化请求流程如下准备原始参数构造业务需要的原始参数字典raw_params。例如{page: 1, size: 20, keyword: 球鞋}。添加系统参数根据逆向结果添加必要的系统参数如timestamp当前毫秒时间戳、nonce随机字符串、appVersion、platform等。这些参数通常也参与签名。生成签名将包含业务和系统参数的字典传入你的generate_sign函数计算出sign值。加密数据将需要加密的请求体可能是全部参数也可能是raw_params转换为JSON字符串然后用你的encrypt_data函数进行加密得到密文encrypted_data。构建最终请求Headers设置必要的请求头如Content-Type: application/x-www-form-urlencoded如果以表单形式提交可能还包括x-sign,x-timestamp等。Body/Params如果接口要求将加密数据放在data字段那么请求体可能就是dataencrypted_data。同时sign和timestamp等参数可能作为查询参数Query Params附在URL上。5.2 使用Python的requests库发送请求import requests import time import json def make_dewu_request(api_url, raw_body_params, common_params): 模拟得物小程序请求 :param api_url: 接口地址 :param raw_body_params: 需要放在请求体并加密的业务参数dict :param common_params: 公共参数dict如timestamp, nonce等通常参与签名和拼接到URL :return: 解密后的响应JSON # 1. 合并参数用于签名 all_params_for_sign {**common_params, **raw_body_params} # 2. 生成签名 (使用之前逆向得到的算法和secret) sign generate_sign(all_params_for_sign, secret你的真实SECRET) # 3. 加密请求体 plain_body json.dumps(raw_body_params, separators(,, :), ensure_asciiFalse) # 紧凑格式 encrypted_body_str crypto.encrypt_data(plain_body) # 使用之前定义的crypto对象 # 4. 构建最终请求参数 # 假设签名和公共参数放在URL查询字符串中加密数据放在POST表单的data字段 query_params common_params.copy() query_params[sign] sign # 5. 准备请求数据 post_data { data: encrypted_body_str } # 6. 发送请求 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., # 模拟小程序UA Content-Type: application/x-www-form-urlencoded, } response requests.post(api_url, paramsquery_params, datapost_data, headersheaders) # 7. 解密响应 if response.status_code 200: # 假设响应也是加密的并且结构可能是 {code:0, data: 加密字符串} resp_json response.json() encrypted_resp_data resp_json.get(data) if encrypted_resp_data: decrypted_resp crypto.decrypt_data(encrypted_resp_data) return json.loads(decrypted_resp) else: return resp_json # 可能某些接口不加密 else: raise Exception(f请求失败: {response.status_code}, {response.text}) # 使用示例 common { timestamp: int(time.time() * 1000), nonce: 随机字符串, appVersion: 5.19, platform: mini_program } body_params {productId: 12345} try: result make_dewu_request(https://api.dewu.com/product/detail, body_params, common) print(请求成功结果, result) except Exception as e: print(请求异常, e)6. 逆向过程中的常见问题与排查技巧即使按照步骤操作你也一定会遇到各种问题。下面是一些常见坑点和解决思路的实录。6.1 抓包无数据或证书错误现象Charles看不到小程序流量。排查检查手机代理设置是否正确IP和端口检查电脑防火墙是否关闭或允许Charles尝试在Charles中启用“透明代理Proxy - macOS Proxy/Windows Proxy”。现象小程序提示网络错误或证书错误。排查这是最典型的证书问题。确保Charles的根证书已正确安装并被系统完全信任。对于Android高版本可能需要使用JustTrustMeXposed模块或Frida脚本绕过证书锁定SSL Pinning但这需要Root环境。对于PC微信调试可以尝试在微信开发者工具中关闭“域名校验”等安全选项如果存在。6.2 无法定位加密代码或断点不触发现象搜索关键词找不到或者找到的函数断点从不触发。排查代码动态加载小程序的代码可能不是一次性加载的。尝试在Network面板查看JS文件的加载或者在Sources面板的Page标签下查找所有加载的脚本文件。代码高度混淆尝试使用js-beautify等工具对混淆的代码进行格式化虽然变量名无法恢复但代码结构会清晰很多便于搜索function定义和return语句。Hook通用方法如果直接定位困难可以尝试Hook网络请求的底层函数。在Chrome开发者工具的Console中可以重写XMLHttpRequest.prototype.send或fetch方法在其中打印参数并打上debugger语句。这能帮你捕获到所有请求并查看其调用栈。6.3 算法还原后签名/加密仍然无效现象用自己的Python代码生成的sign和服务端校验不通过或者加密后的data服务器无法解密。排查这是最考验耐心的环节。你需要像侦探一样对比每一个细节。参数比对在JavaScript加密函数入口处断点记录下传入函数的所有参数的精确值包括类型数字还是字符串。在你的Python代码中确保传入generate_sign的字典完全一致键的顺序不影响但值必须相同。字符串格式比对在JavaScript中将待签名的字符串拼接后的结果打印出来。在你的Python代码中也将拼接后的字符串打印出来。进行逐字符比对包括空格、换行、URL编码的差异encodeURIComponent和Python的urllib.parse.quote可能有细微差别。编码比对确保哈希计算MD5/SHA256前字符串的编码一致。JavaScript通常使用UTF-16或某种内部编码而Python默认是UTF-8。一个稳妥的方法是在JavaScript调试器里直接计算CryptoJS.MD5(str).toString()的结果与Python的hashlib.md5(str.encode(‘utf-8’)).hexdigest()对比。如果不一致尝试在Python中使用str.encode(‘utf-16le’)或其他编码。密钥/IV比对确认AES加密使用的Key和IV的字节序列完全一致。JavaScript中可能将字符串通过CryptoJS.enc.Utf8.parse转换成WordArray你需要确认这个转换过程。在Python中直接使用key.encode(‘utf-8’)得到的字节可能不同。有时密钥是Base64或Hex格式的需要先解码。加密模式与填充再次确认AES的模式CBC/ECB、填充PKCS7/ZeroPadding、输出格式Base64/Hex是否完全一致。CryptoJS库的默认模式可能与PyCryptodome有差异。6.4 版本更新导致算法失效现象之前好用的脚本突然全部失效返回签名错误。应对这是常态。平台会定期更新加密算法或密钥。你需要重新进行抓包和逆向分析看sign的生成规则、secret、加密密钥等是否有变化。有时变化很小只是增加了一个固定参数或改变了拼接顺序。个人经验建立一个完整的“现场快照”非常重要。在成功逆向一个版本后除了保存Python代码最好也保存以下信息关键JavaScript函数的美化后代码片段。一次成功请求的完整Charles抓包记录包含请求头、请求体、响应体。调试器中关键变量待签名字符串、密钥、IV等的截图或文本记录。 这样当算法更新时你可以快速对比新旧版本的差异极大提升排查效率。逆向工程是一个与平台防御机制不断博弈的过程。它没有一成不变的答案核心在于掌握通用的分析方法抓包定位、静态搜索、动态调试、逻辑比对、代码还原。通过得物这个案例希望你能将这套方法论内化从而有能力去应对其他具有类似加密机制的应用或小程序。记住耐心和细致是成功的关键每一个字节的差异都可能导致失败。