当前位置: 首页 > news >正文

得物小程序sign与data加密逆向分析:从抓包到Python算法还原实战

1. 项目概述:从“黑盒”到“白盒”的逆向工程之旅

最近在分析一些电商平台的数据接口时,得物小程序成为了一个典型的研究案例。它的接口安全机制,尤其是对请求参数signdata的加密,以及对响应体的解密,构成了一个完整的“黑盒”通信流程。对于数据分析师、爬虫工程师或是安全研究人员而言,理解并还原这套算法,意味着能够以程序化的方式与平台进行“合规”的数据交互,或者深入理解其安全设计逻辑。这不仅仅是破解几个参数那么简单,它涉及到对小程序运行机制、前端JavaScript代码保护、以及后端加密逻辑的完整逆向分析链条。整个过程就像在解一个设计精巧的谜题,每一步都需要耐心、细致的观察和逻辑推理。

本文将带你完整走一遍得物小程序(以5.19版本为例)的sign签名生成、data参数加密以及响应体解密的算法还原过程。我们会使用主流的逆向分析工具,如Charles/Fiddler抓包、Chrome开发者工具调试,以及Python进行算法复现。目标读者是具有一定网络协议基础和JavaScript知识的开发者或安全爱好者,通过本文,你将不仅能掌握针对得物小程序的特定方法,更能获得一套通用的、应对类似加密场景的逆向分析思路和实战技巧。

2. 逆向分析环境搭建与初步抓包

工欲善其事,必先利其器。在开始逆向算法之前,一个稳定、可控的分析环境至关重要。对于小程序的分析,我们通常需要在真机上进行,因为小程序的运行环境与浏览器有所不同,其网络请求可能受到更多限制。

2.1 核心工具链准备

首先,你需要准备以下工具:

  1. 抓包工具CharlesFiddler。我个人更倾向于Charles,它的界面更友好,对HTTPS流量的解密支持也更成熟。确保安装好CA证书,并配置好代理(通常为手机和电脑在同一局域网,手机Wi-Fi设置代理指向电脑IP和Charles监听端口,如8888)。
  2. 调试工具:PC端微信开发者工具或手机端调试。对于小程序,最直接的方式是在PC微信中打开小程序,然后利用微信开发者工具(需开启调试模式)进行动态调试。另一种更深入的方式是使用已Root的Android手机或越狱的iOS设备,配合xposedfrida等框架进行Hook,但这门槛较高。我们优先采用PC端调试方案。
  3. 逆向分析工具
    • 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-signsign
  • Query Parameters中可能也包含一个sign参数。
  • Request Body不再是常见的JSON或表单格式,而是一个经过加密的字符串,可能以data=xxx的形式提交,或者整个Body就是一个密文块。
  • Response Body同样不是直接的JSON,而是一串看似乱码的加密数据,需要客户端解密后才能得到真正的JSON结构。

我们的首要任务,就是定位生成这些加密参数(尤其是sign和加密的data)的JavaScript代码在哪里,以及它是如何工作的。

3. 定位加密入口与核心逻辑分析

小程序的前端代码虽然经过压缩和混淆,但其核心逻辑必然存在于发送网络请求的模块中。微信小程序的网络请求主要使用wx.request方法。

3.1 搜索与断点定位

在微信开发者工具中打开得物小程序的调试模式,切换到“源代码(Sources)”面板。由于代码被压缩,我们可以使用全局搜索(Ctrl+Shift+F)来寻找关键线索。

搜索关键词策略

  1. 直接搜索sign。这可能会找到很多处,包括变量名、字符串等。可以尝试搜索"sign"(带引号的字符串)或sign:(作为对象属性)。
  2. 搜索网络请求相关的关键词,如wx.request,uni.request(如果用了uni-app框架),或者JSON.stringify,encrypt,md5,sha256,hmac,AES,CBC等加密相关词汇。
  3. 搜索可能存在的自定义函数名,比如getSign,encryptData,encodeParams等。这需要结合抓包看到的请求参数名进行猜测。

通过搜索,你很可能找到一个被高度混淆的JavaScript文件,里面的变量名都是a,b,c,o,n等单字母。不要慌,这是常态。我们的目标不是读懂每一行,而是找到加密发生的“入口函数”。

一个有效的方法是:在搜索到疑似加密函数的地方(比如一个函数里调用了CryptoJS库的方法,或者有很长的字符串运算),在该函数入口处打上断点。然后,在小程序前端触发一个网络请求(比如点击刷新商品列表)。如果断点被触发,那么恭喜你,找到了关键位置。

3.2 调用栈分析与参数追踪

当断点命中时,查看右侧的“调用栈(Call Stack)”。调用栈会显示当前函数是被谁调用的,一层层回溯,你就能找到整个加密调用的链条。通常,这个链条的顶端会与wx.request的调用相关。

在调试器中,你可以查看当前作用域(Scope)中所有变量的值。重点关注:

  • 传入这个加密函数的参数是什么?它很可能是一个包含原始请求参数(如商品ID、页码、时间戳等)的JavaScript对象。
  • 这个函数内部对参数做了什么处理?一步步单步执行(F10),观察变量的变化。注意看是否有以下操作:
    • 添加固定参数:如timestamp,nonce,appVersion,platform等。
    • 参数排序:按照字母顺序对对象的键进行排序,这是生成签名的常见步骤。
    • 字符串拼接:将排序后的键值对拼接成特定格式的字符串,如key1=value1&key2=value2
    • 拼接密钥:在字符串的首尾或中间拼接一个固定的密钥(secretsalt)。
    • 哈希计算:将拼接后的字符串进行MD5、SHA256等哈希运算,得到sign
    • 加密操作:对完整的参数对象或data字段进行AES、RSA等加密。

实操心得:在单步调试时,善用控制台(Console)。你可以将任何感兴趣的变量拖到控制台查看其完整内容,或者直接输入表达式进行计算。例如,当你看到拼接后的字符串s时,可以在控制台输入s来确认其内容,甚至可以手动计算md5(s)来验证是否与生成的sign一致。

3.3 得物sign算法还原实例分析

根据对历史版本和当前抓包的分析,得物的sign生成算法虽然可能随版本更新,但核心思路有迹可循。一个常见的模式是:

  1. 收集参数:收集所有需要参与签名的参数,包括URL查询参数(Query String)和请求体(Body)参数。对于POST请求,Body参数可能是加密前的原始JSON对象。
  2. 规范化处理
    • 排除某些不参与签名的字段(如sign本身)。
    • 对所有参数名进行字典序排序
    • 将每个参数和值转换为字符串,并进行URL编码(或特定编码)。
  3. 构造待签名字符串:将规范化后的参数以key=value的形式用&连接起来,形成字符串str_to_sign
  4. 拼接密钥:在str_to_sign的末尾(或开头)拼接一个从服务器下发的、或硬编码在客户端的secret。这个secret的获取是逆向的另一个关键点,可能藏在代码的常量里,也可能通过某个初始化接口获取。
  5. 计算哈希:对拼接后的字符串计算MD5SHA256,并将结果转换为小写十六进制字符串,即为最终的sign值。

在调试器中,你需要一步步验证这个过程。找到排序、拼接、哈希计算的具体代码行。将关键逻辑的JavaScript代码片段提取出来。

4. data参数加密与响应体解密算法解析

sign保证了请求的完整性和不可篡改性,而data的加密则保证了请求/响应内容的机密性。得物很可能采用对称加密算法(如AES)来加密核心的业务数据。

4.1 定位加密/解密函数

在找到sign生成函数附近,通常也能找到加密函数。搜索encrypt,decrypt,AES,CBC,mode,padding等关键词。同样通过断点调试来定位。

当你在发送请求前的代码里找到加密调用时,观察:

  • 加密密钥(Key)和初始化向量(IV)从哪里来?可能是固定的字符串,也可能是通过某个算法动态生成的(例如,用sign的一部分作为Key)。
  • 加密模式是什么?常见的是AES-CBCAES-ECB。CBC模式需要IV。
  • 填充方式是什么?常见的是PKCS7填充。
  • 输出格式是什么?通常是Base64编码或十六进制字符串。

响应体的解密是加密的逆过程。你需要找到处理网络响应的地方(可能是wx.requestsuccess回调函数里),在那里会有对返回数据进行解密的函数调用。其密钥、IV、模式、填充方式应与加密时一致。

4.2 算法还原与Python实现

一旦在调试器中理清了逻辑,就可以开始用Python还原算法了。这里以最常见的AES-CBC-PKCS7加密和MD5签名为例。

首先,安装必要的库:

pip install pycryptodome requests

4.2.1 Sign签名还原示例

假设我们从逆向分析中得知,sign的生成规则是:将所有参数(除sign外)按key字典序排序,拼接成key1=value1&key2=value2的格式,末尾拼接固定字符串secret123,然后取MD5(小写)。

import hashlib import urllib.parse def generate_sign(params, secret='secret123'): """ 生成得物风格签名 :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 = b'thisis16bytekey!' # 16字节 iv = b'thisis16byteiv!!' # 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

重要提示:以上keyivsecret以及具体的拼接规则都是示例。你必须通过逆向分析,从得物小程序的JavaScript代码中提取出真实的值和算法逻辑。这些关键信息可能以字符串常量的形式存在,也可能通过更复杂的逻辑计算得出。

5. 完整请求构建与算法验证

在还原了signdata的算法后,下一步就是构建一个完整的、能够成功与服务器通信的请求。

5.1 请求参数组装流程

一个完整的自动化请求流程如下:

  1. 准备原始参数:构造业务需要的原始参数字典raw_params。例如,{'page': 1, 'size': 20, 'keyword': '球鞋'}
  2. 添加系统参数:根据逆向结果,添加必要的系统参数,如timestamp(当前毫秒时间戳)、nonce(随机字符串)、appVersionplatform等。这些参数通常也参与签名。
  3. 生成签名:将包含业务和系统参数的字典,传入你的generate_sign函数,计算出sign值。
  4. 加密数据:将需要加密的请求体(可能是全部参数,也可能是raw_params)转换为JSON字符串,然后用你的encrypt_data函数进行加密,得到密文encrypted_data
  5. 构建最终请求
    • Headers:设置必要的请求头,如Content-Type: application/x-www-form-urlencoded(如果以表单形式提交),可能还包括x-sign,x-timestamp等。
    • Body/Params:如果接口要求将加密数据放在data字段,那么请求体可能就是data=encrypted_data。同时,signtimestamp等参数可能作为查询参数(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_ascii=False) # 紧凑格式 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, params=query_params, data=post_data, headers=headers) # 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高版本,可能需要使用JustTrustMe(Xposed模块)或Frida脚本绕过证书锁定(SSL Pinning),但这需要Root环境。对于PC微信调试,可以尝试在微信开发者工具中关闭“域名校验”等安全选项(如果存在)。

6.2 无法定位加密代码或断点不触发

  • 现象:搜索关键词找不到,或者找到的函数断点从不触发。
    • 排查
      1. 代码动态加载:小程序的代码可能不是一次性加载的。尝试在Network面板查看JS文件的加载,或者在Sources面板的Page标签下查找所有加载的脚本文件。
      2. 代码高度混淆:尝试使用js-beautify等工具对混淆的代码进行格式化,虽然变量名无法恢复,但代码结构会清晰很多,便于搜索function定义和return语句。
      3. Hook通用方法:如果直接定位困难,可以尝试Hook网络请求的底层函数。在Chrome开发者工具的Console中,可以重写XMLHttpRequest.prototype.sendfetch方法,在其中打印参数并打上debugger语句。这能帮你捕获到所有请求,并查看其调用栈。

6.3 算法还原后签名/加密仍然无效

  • 现象:用自己的Python代码生成的sign和服务端校验不通过,或者加密后的data服务器无法解密。
    • 排查:这是最考验耐心的环节。你需要像侦探一样对比每一个细节。
      1. 参数比对:在JavaScript加密函数入口处断点,记录下传入函数的所有参数的精确值(包括类型,数字还是字符串)。在你的Python代码中,确保传入generate_sign的字典完全一致(键的顺序不影响,但值必须相同)。
      2. 字符串格式比对:在JavaScript中,将待签名的字符串(拼接后的结果)打印出来。在你的Python代码中,也将拼接后的字符串打印出来。进行逐字符比对,包括空格、换行、URL编码的差异(encodeURIComponent和Python的urllib.parse.quote可能有细微差别)。
      3. 编码比对:确保哈希计算(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’)或其他编码。
      4. 密钥/IV比对:确认AES加密使用的Key和IV的字节序列完全一致。JavaScript中可能将字符串通过CryptoJS.enc.Utf8.parse转换成WordArray,你需要确认这个转换过程。在Python中,直接使用key.encode(‘utf-8’)得到的字节可能不同。有时密钥是Base64或Hex格式的,需要先解码。
      5. 加密模式与填充:再次确认AES的模式(CBC/ECB)、填充(PKCS7/ZeroPadding)、输出格式(Base64/Hex)是否完全一致。CryptoJS库的默认模式可能与PyCryptodome有差异。

6.4 版本更新导致算法失效

  • 现象:之前好用的脚本突然全部失效,返回签名错误。
    • 应对:这是常态。平台会定期更新加密算法或密钥。你需要重新进行抓包和逆向分析,看sign的生成规则、secret、加密密钥等是否有变化。有时变化很小,只是增加了一个固定参数或改变了拼接顺序。

个人经验:建立一个完整的“现场快照”非常重要。在成功逆向一个版本后,除了保存Python代码,最好也保存以下信息:

  1. 关键JavaScript函数的美化后代码片段。
  2. 一次成功请求的完整Charles抓包记录(包含请求头、请求体、响应体)。
  3. 调试器中关键变量(待签名字符串、密钥、IV等)的截图或文本记录。 这样,当算法更新时,你可以快速对比新旧版本的差异,极大提升排查效率。

逆向工程是一个与平台防御机制不断博弈的过程。它没有一成不变的答案,核心在于掌握通用的分析方法:抓包定位、静态搜索、动态调试、逻辑比对、代码还原。通过得物这个案例,希望你能将这套方法论内化,从而有能力去应对其他具有类似加密机制的应用或小程序。记住,耐心和细致是成功的关键,每一个字节的差异都可能导致失败。

http://www.jsqmd.com/news/1126759/

相关文章:

  • OpenCV 4.8 相机标定实战:5步完成棋盘格标定与畸变矫正(附Python代码)
  • uos-tc-exporter进阶指南:并发收集器原理与性能优化技巧
  • 高效电机驱动系统设计:TC78H660FTG与TM4C1299NCZAD方案
  • STM32与WSEN-ISDS三轴加速度计运动追踪系统开发指南
  • ASM330LHH与STM32F101ZG在运动跟踪系统中的应用与优化
  • TranslucentTB:5种透明魔法让你的Windows桌面瞬间呼吸起来
  • 收放板机如何应对特殊板件——从超薄板到厚铜板的取放策略
  • MAX9744与STM32F732IE的高效音频放大方案解析
  • ASM330LHH与STM32F334R8运动跟踪系统开发指南
  • AI时代产品经理转型指南:从执行者到人机协同策略指挥官
  • WindowsCleaner:免费开源的Windows系统清理终极方案,彻底解决C盘爆红问题
  • Java NIO 实战
  • STM32与XTR116的4-20mA电流环设计实战
  • 嵌入式矩阵键盘设计:硬件去抖动与中断触发方案
  • 第七周学习记录
  • OBS多平台推流终极指南:一键实现多平台同时直播的完整教程
  • 高效电机驱动系统设计与STM32控制优化
  • 【OpenHarmony/HarmonyOs 】从数学学习 App 出发:近场快传、实况窗与全场景智慧学习设计
  • 879644
  • WechatDecrypt终极指南:三步解锁你的微信记忆宝库
  • 5步掌握RimSort:告别RimWorld模组冲突烦恼的终极指南
  • LTC6904与MK20微控制器构建高精度方波发生器
  • Windows任务栏透明化技术实现|TranslucentTB架构解析与应用场景分析
  • ICM-42688-P与PIC18F97J94在工业自动化中的高效协同方案
  • STM32F042K6与13DOF传感器实现低成本高精度定位
  • 基于LP5812与PIC32MZ的智能灯光控制系统设计
  • 呼和浩特老房改造市场乱象 | 增项漏项成投诉重灾区,如何必坑成难点
  • LTC6904与PIC24FJ1024GB610实现高精度方波脉冲生成
  • 终极RimWorld模组管理器:RimSort如何让你告别模组冲突烦恼
  • dirsearch:Web 路径发现工具,安全测试绕不开