微信小程序逆向实战:从抓包到Python复现加密签名算法

发布时间:2026/7/27 21:07:43
微信小程序逆向实战:从抓包到Python复现加密签名算法 1. 项目概述与核心价值最近在做一个自动化签到工具时遇到了一个棘手的加密参数。这个参数来自一个微信小程序每次签到请求都必须携带否则服务器直接返回“签名错误”。抓包一看参数名是sign一串长长的、看起来毫无规律的字符串典型的加密结果。直接硬猜或者爆破基本不可能唯一的出路就是搞清楚这个sign是怎么算出来的。这就是典型的“逆向”场景——我们不知道加密逻辑但我们需要复现它。微信小程序的代码虽然经过编译和压缩但并非固若金汤。通过一系列工具我们可以将其反编译还原出可读性尚可的 JavaScript 源代码。我们的目标就是从这一堆代码中定位到生成sign的函数理解其算法逻辑然后用 Python 重新实现一遍。这个过程不仅是为了解决眼前的签到问题更是一次完整的逆向工程实战演练涵盖了小程序抓包、反编译、代码分析、算法还原和跨语言复现等多个环节。无论你是对安全感兴趣还是需要处理类似的数据接口对接问题这套流程都具有很高的参考价值。2. 逆向工程的前期准备与环境搭建2.1 核心工具链选型与原理工欲善其事必先利其器。逆向微信小程序我们需要一套从数据捕获到代码还原的完整工具链。首先我们需要捕获小程序与服务器之间的网络通信。这里我选择Charles作为抓包工具。它是一个跨平台的 HTTP 代理/监控工具功能强大对 HTTPS 流量的解密支持也很好。选择它的理由很简单稳定、图形化界面友好、支持重发和断点调试对于分析网络请求格式和参数结构来说非常直观。当然Fiddler、mitmproxy 也是不错的选择但 Charles 在 macOS 和 Windows 下的体验相对更统一。抓包只是第一步我们最终需要的是小程序的源代码。微信小程序的包体格式是.wxapkg。在安卓手机上运行过的小程序包会被缓存在特定目录。我们需要一台已经 Root 的安卓手机或者使用安卓模拟器如夜神、MuMu并获取 Root 权限。然后通过文件管理器如 RE 管理器找到路径/data/data/com.tencent.mm/MicroMsg/{一串长哈希}/appbrand/pkg/这里就存放着所有已访问过的小程序包。将其导出到电脑。拿到.wxapkg文件后我们需要反编译它。这里我使用wxappUnpacker这个开源工具。它是一个基于 Node.js 的项目专门用于解包和反编译微信小程序。其原理是逆向了小程序的编译和打包流程将压缩、混淆后的代码重新整理成接近原始的目录结构和代码文件。虽然代码变量名可能被压缩成单字母但整体逻辑和关键函数名如果开发者没有刻意混淆是可以被还原出来的。最后我们需要一个代码分析环境。反编译出来的主要是 JavaScript 文件我选择Visual Studio Code作为代码编辑器。它轻量、启动快对 JavaScript 的语法高亮、代码跳转支持得很好安装一些插件如 JavaScript (ES6) code snippets后阅读和分析代码的效率会更高。2.2 具体环境配置步骤Charles 安装与配置从官网下载并安装 Charles。打开 Charles在Proxy - SSL Proxying Settings中添加一个*:*的代理规则即代理所有主机和端口。在Proxy - Proxy Settings中记住默认的代理端口通常是 8888。最关键的一步在电脑和手机上安装 Charles 的 SSL 证书。电脑上直接在 Charles 的Help - SSL Proxying - Install Charles Root Certificate即可。手机上需要确保手机和电脑在同一个 WiFi并在手机 WiFi 设置中手动配置代理服务器为电脑的 IP 地址端口为 8888。然后用手机浏览器访问chls.pro/ssl下载并安装证书安卓手机可能还需要在系统设置中“信任用户安装的证书”。获取小程序包在已 Root 的安卓设备上打开目标微信小程序确保其主包加载完成。使用文件管理器导航到上述缓存路径。你会看到一些.wxapkg文件通常最大的那个或者文件名不带__前缀的是主包。将其复制到电脑。wxappUnpacker 环境搭建确保电脑已安装 Node.js。在命令行中使用git clone命令拉取 wxappUnpacker 的仓库代码或者直接下载 ZIP 包解压。进入项目目录执行npm install安装依赖。如果遇到某些 npm 包网络问题可以尝试配置国内镜像源。反编译操作在命令行中进入 wxappUnpacker 目录运行反编译命令。基本命令格式是node wuWxapkg.js [.wxapkg文件路径]。例如node wuWxapkg.js D:\download\_-1234567890.wxapkg。执行成功后会在.wxapkg文件同目录下生成一个同名的文件夹里面就是反编译出的所有源代码、配置和资源文件。注意微信会不定期更新小程序的打包格式或加密方式可能导致旧版 wxappUnpacker 失效。如果反编译失败或结果异常可以尝试寻找更新版本的反编译工具或脚本。另外此方法仅用于学习交流请勿用于非法用途。3. 核心逆向流程从抓包到定位加密点3.1 网络请求抓包与关键参数识别环境准备好后我们开始实战。打开 Charles确保代理开启然后在手机上打开目标签到小程序进行一次签到操作。回到 Charles 界面你应该能看到抓取到的一系列 HTTP/HTTPS 请求。我们需要从中找到签到相关的请求。通常可以通过 URL 路径可能包含signin、checkin、user等关键词或请求的时序在点击签到按钮后立即出现来判断。找到目标请求后查看其请求体如果是 POST或 URL 参数如果是 GET。以我遇到的案例为例这是一个 POST 请求请求体是 JSON 格式{ userId: 123456, timestamp: 1646389472000, nonce: a1b2c3d4, sign: e10adc3949ba59abbe56e057f20f883e }很明显userId、timestamp时间戳、nonce随机数都是明文参数而sign看起来像是一个 MD5 哈希值32位十六进制字符串。我们的目标就是找出这个sign是如何由其他参数可能还包括一个未知的密钥计算出来的。3.2 反编译代码分析与加密函数追踪拿到反编译的代码后整个小程序的逻辑都在里面了。目录结构通常包含app.js小程序入口、pages各个页面、utils工具函数等。我们的搜索策略要有层次全局搜索关键词在 VSCode 中打开反编译项目的根目录使用全局搜索功能CtrlShiftF。首先搜索sign。你可能会找到很多处包括网络请求的配置代码、参数组装的地方。重点关注那些将sign赋值给请求参数的代码行。定位网络请求库小程序通常使用wx.request或封装后的网络库如flyio、request等。找到发起签到请求的那个函数。在这个函数内部会有一个对象包含了所有要发送的参数sign就在其中它的值通常是一个函数调用的返回值比如getSign(params)或md5(str)。追踪函数调用点击跳转到这个计算sign的函数定义处。这个函数可能在本文件内也可能在utils目录下的某个工具文件里比如sign.js、encrypt.js或md5.js。分析加密逻辑现在你终于看到了“圣杯”——生成sign的原始代码。代码可能是这样的function getSign(data) { var key aSecretKeyFromServerOrFixed; // 1. 参数排序 var sortedKeys Object.keys(data).sort(); var str ; for (var i 0; i sortedKeys.length; i) { var k sortedKeys[i]; if (k ! sign data[k] ! undefined data[k] ! null) { str k data[k] ; } } // 2. 拼接密钥 str str key key; // 3. 计算MD5并转为大写 var md5Str md5(str); return md5Str.toUpperCase(); }这段代码是典型的“参数排序后拼接密钥再进行 MD5”的签名算法常见于许多 API 接口。你需要仔细分析哪些参数参与了签名通常是除了sign本身之外的所有请求参数。参数的顺序如何通常是按照参数名的 ASCII 码升序排列这是为了保证服务端和客户端排序一致。拼接的格式是什么常见的是key1value1key2value2keysecretKey。使用了什么哈希算法MD5、SHA1、SHA256 等。从输出长度和特征可以初步判断。有没有额外的转换比如哈希结果转为大写或小写。实操心得反编译出的 JS 代码变量名常被压缩比如var a, b, c;。这会给阅读带来困难。此时要结合上下文和函数调用来推断其含义。例如如果一个函数接收一个对象o然后对o的键进行排序那o很可能就是参数字典。多使用编辑器的“查找所有引用”功能可以帮助你理清数据流向。4. 加密算法解析与Python复现4.1 算法逻辑深度拆解假设我们定位到的加密函数如上节所示。我们来彻底拆解它输入一个 JavaScript 对象data包含了userId、timestamp、nonce等键值对。过滤排除sign字段本身也排除值为undefined或null的字段。这是为了防止这些无效字段影响签名结果。排序使用Object.keys(data).sort()获取键名数组并排序。JavaScript 的默认排序是基于字符串的 Unicode 码点对于字母和数字的组合其 ASCII 码升序结果与常见编程语言如 Python的字符串排序结果是一致的。这一点在跨语言复现时至关重要。拼接遍历排序后的键数组按照keyvalue的格式拼接成一个字符串。注意最后一个参数后面也会带然后再拼接固定的keysecretKey。最终形成一个如noncea1b2c3d4timestamp1646389472000userId123456keyaSecretKeyFromServerOrFixed的字符串。哈希对这个拼接后的字符串计算 MD5 哈希。MD5 算法是标准的输入是字符串的字节表示输出是 128 位的哈希值通常表示为 32 位十六进制字符串。格式化将得到的 MD5 字符串全部转换为大写.toUpperCase()作为最终的sign值。为什么是 MD5在这个场景下签名Signature的目的不是为了加密数据内容数据本身是明文的而是为了验证请求的完整性和来源合法性。服务器用同样的规则计算一遍签名如果一致就证明参数在传输过程中未被篡改并且客户端拥有正确的密钥secretKey。MD5 计算速度快输出长度固定且碰撞风险在非安全敏感的场景下可以接受因此被广泛用于此类接口签名。4.2 Python代码逐行复现与验证理解了算法用 Python 复现就水到渠成了。我们需要 Python 的hashlib库来计算 MD5。import hashlib import time def generate_sign(params, secret_key): 复现微信小程序的签名算法 :param params: dict, 请求参数字典不应包含sign :param secret_key: str, 签名密钥 :return: str, 计算得到的签名大写 # 1. 参数排序 # 注意排序的是键key且需按ASCII升序。Python的sorted默认行为一致。 sorted_keys sorted(params.keys()) # 2. 拼接键值对 sign_str for key in sorted_keys: value params[key] # 过滤掉值为None的参数与JS中的 undefined/null 过滤对应 if value is not None: # 将值转换为字符串确保类型一致。JS中拼接会自动转换。 sign_str f{key}{value} # 3. 拼接密钥 sign_str fkey{secret_key} # 4. 计算MD5 # 重要需要将字符串编码为字节bytes再进行哈希。 # 通常使用UTF-8编码这与JS中字符串的内部表示在计算MD5时通常是兼容的。 md5 hashlib.md5() md5.update(sign_str.encode(utf-8)) digest md5.hexdigest() # 5. 转换为大写并返回 return digest.upper() # 模拟请求参数 request_params { userId: 123456, timestamp: str(int(time.time() * 1000)), # 生成13位时间戳 nonce: a1b2c3d4 } secret aSecretKeyFromServerOrFixed # 计算签名 calculated_sign generate_sign(request_params, secret) print(f计算得到的签名: {calculated_sign}) # 输出示例计算得到的签名: E10ADC3949BA59ABBE56E057F20F883E关键点解析与避坑指南排序一致性sorted(params.keys())在 Python 3 中默认按字典序lexicographical order排序与 JavaScript 的Array.sort()对字符串数组的排序结果在绝大多数情况下一致。这是算法正确的基础。参数过滤Python 中我们用if value is not None:来过滤。注意空字符串和数字0在 JS 中是有值的在 Python 里也要保留除非算法明确排除。务必与反编译代码中的过滤逻辑严格保持一致。字符串编码hashlib.md5()需要字节输入。sign_str.encode(utf-8)将字符串转为 UTF-8 字节序列。这是最常见的编码通常与 JS 环境下的处理方式匹配。这是最容易出错的一步如果编码不一致得到的 MD5 会完全不同。大小写转换JS 代码中使用了.toUpperCase()所以 Python 中我们用.upper()。有些接口可能是小写务必根据实际情况决定。密钥来源secret_key是核心机密。它可能是硬编码在小程序代码里的安全性较低也可能是小程序启动时从服务器动态获取的安全性较高。你需要从反编译的代码中找到它被赋值的地方。5. 完整自动化签到脚本构建5.1 脚本架构设计与模块化复现了签名算法我们就可以构建一个完整的自动化签到脚本了。一个好的脚本应该模块清晰、易于维护和扩展。脚本主要分为以下几个模块配置模块存放用户ID、密钥等固定或需配置的信息。签名模块封装上一节实现的generate_sign函数。网络请求模块负责构造 HTTP 请求发送签到数据。任务调度模块实现定时执行签到的逻辑。日志与通知模块记录执行结果并在成功或失败时通过某种方式通知用户。这里我们先实现一个基础的单次签到脚本。# config.py USER_ID 你的用户ID SECRET_KEY 从反编译代码中找到的密钥 SIGN_API_URL https://api.xxx.com/user/signin # 签到接口URL从抓包获取 # sign.py import hashlib def generate_sign(params, secret_key): # ... 复用上面的函数实现 ... pass # request_sender.py import requests import json from config import USER_ID, SECRET_KEY, SIGN_API_URL from sign import generate_sign import time def perform_sign(): 执行一次签到操作 :return: tuple (success, message) # 1. 构造请求参数 params { userId: USER_ID, timestamp: str(int(time.time() * 1000)), nonce: generate_nonce(), # 生成一个随机字符串 } # 2. 计算签名 sign generate_sign(params, SECRET_KEY) params[sign] sign # 将签名加入请求参数 # 3. 发送请求 headers { Content-Type: application/json, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 # 模拟浏览器 } try: response requests.post(SIGN_API_URL, datajson.dumps(params), headersheaders, timeout10) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 resp_data response.json() # 4. 解析响应 if resp_data.get(code) 0 or resp_data.get(success): return True, 签到成功 resp_data.get(message, ) else: return False, f签到失败{resp_data.get(message, 未知错误)} except requests.exceptions.RequestException as e: return False, f网络请求异常{e} except json.JSONDecodeError: return False, 服务器响应不是有效的JSON格式 def generate_nonce(length8): 生成随机字符串作为nonce import random import string chars string.ascii_letters string.digits return .join(random.choice(chars) for _ in range(length)) # main.py from request_sender import perform_sign import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) if __name__ __main__: success, msg perform_sign() if success: logging.info(msg) # 可以在这里添加成功通知如发送邮件、Server酱消息等 else: logging.error(msg) # 可以在这里添加失败告警5.2 异常处理、日志与部署建议一个健壮的脚本必须考虑各种异常情况。网络异常使用try...except捕获requests库可能抛出的超时、连接错误等异常并给出友好提示。响应格式异常服务器可能返回非 JSON 数据或者 JSON 结构不符合预期。使用json.JSONDecodeError捕获解析错误并对resp_data.get()使用默认值避免键错误。签名验证失败如果服务器返回“签名错误”首先检查密钥是否正确确认SECRET_KEY没有错误且没有多余的空格。参数是否完整确认参与签名的参数列表与服务器端一致。有时服务器会添加一些隐含参数如固定字符串。排序和拼接规则是否严格按照键的 ASCII 码升序拼接时键和值之间的等号、参数之间的号是否一致末尾是否有多余的编码问题确认 Python 中拼接字符串的编码与 JS 端完全一致。一个调试技巧是在 JS 代码中可以通过浏览器开发者工具或 Node.js 环境用相同的参数和密钥计算一次签名并打印出拼接后的原始字符串。然后在 Python 中在计算 MD5 前也打印出拼接后的字符串进行逐字符比对包括不可见字符。日志记录使用 Python 的logging模块记录脚本运行情况包括时间、成功/失败状态、错误信息等便于后期排查问题。部署运行本地测试先在本地运行确保能成功签到一次。服务器部署如果需要 24 小时定时运行可以购买一台云服务器如腾讯云、阿里云的轻量应用服务器。定时任务在 Linux 服务器上使用crontab设置定时任务。例如每天上午9点执行0 9 * * * cd /path/to/your/script /usr/bin/python3 main.py /tmp/sign.log 21。无服务器方案也可以使用一些云函数服务如腾讯云 SCF、阿里云 FC设置定时触发器省去维护服务器的麻烦。注意事项自动化脚本涉及网络请求和用户账户操作务必遵守目标网站的服务条款。过度频繁的请求可能被视为攻击导致 IP 被封。建议在脚本中加入合理的延时并只在必要时运行。本教程仅供技术学习交流。6. 进阶技巧与疑难问题排查6.1 应对更复杂的加密与混淆策略不是所有的小程序签名都像上面例子那么简单。开发者可能会采用更复杂的策略来增加逆向难度。代码混淆与压缩这是最常见的。变量名、函数名被替换成无意义的短字符代码结构被压扁。应对方法耐心这是体力和眼力的考验。多使用编辑器的“查找”和“跳转到定义”功能。关注常量加密密钥、固定的盐值salt、初始向量IV等通常会以字符串或数字常量的形式留在代码中搜索引号内的字符串或明显的数字。动态调试如果条件允许可以尝试在微信开发者工具中导入反编译后的项目可能需要修复一些路径错误然后打断点动态调试观察变量的值这比静态分析高效得多。非标准哈希或自定义算法算法可能不是简单的 MD5可能是 SHA系列甚至是经过多次哈希、加盐的变种。也可能不是标准哈希而是自定义的位运算。识别算法特征观察sign值的长度和字符集。32位十六进制是 MD540位是 SHA164位是 SHA256。如果长度不规则可能是 Base64 编码后的结果。追踪加密函数在代码中搜索CryptoJS、md5、sha1、hash、encrypt等关键词找到使用的加密库。常见的库如CryptoJS其调用方式很有特点如CryptoJS.MD5(str).toString()。模拟执行对于非常复杂的、难以直接翻译的算法可以考虑使用execjs或PyExecJS库直接在 Python 中调用一个 JavaScript 运行时如 Node.js来执行那一段原始的 JS 加密函数。这是“终极武器”可以 100% 还原算法逻辑。密钥动态获取密钥不是硬编码而是从服务器接口获取有时还带有有效期。追踪网络请求在小程序启动或登录时抓包观察是否有返回token、secret、key等字段的请求。分析获取逻辑在反编译代码中找到获取并存储这个密钥的逻辑。你的 Python 脚本也需要先模拟登录或初始化请求拿到最新的密钥再用于签名计算。6.2 常见问题排查速查表在复现过程中你几乎一定会遇到签名不一致的问题。下表列出常见原因和排查步骤问题现象可能原因排查步骤服务器返回“签名错误”1. 密钥错误2. 参与签名的参数不全3. 参数排序规则不一致4. 字符串拼接格式错误5. 编码不一致1. 核对SECRET_KEY注意首尾空格。2. 检查 JS 代码确认哪些参数被filter掉了如sign本身、null/undefined。3. 在 JS 和 Python 中分别打印排序后的键名数组进行比对。4. 在 JS 和 Python 中分别打印拼接后的原始字符串进行逐字符比对可用十六进制查看。5. 确保 Python 中使用encode(utf-8)并尝试encode(ascii)或encode(latin-1)进行对比。签名结果长度不对使用的哈希算法判断错误检查sign长度。32位十六进制是 MD540位是 SHA164位是 SHA256。确认 JS 代码中调用的具体哈希函数。抓包看不到小程序请求1. Charles 证书未安装或未信任2. 小程序使用了 HTTP/2 或 QUIC3. 请求走了其他通道如 WebSocket1. 确保手机和电脑证书安装、信任步骤正确。2. 在 Charles 的Proxy - SSL Proxying Settings中确保*:*已启用。3. 尝试关闭手机的 IPV6。某些小程序或网络下Charles 可能无法完美代理所有流量。反编译出来的代码无法运行微信小程序引擎版本更新打包格式变化寻找更新版本的wxappUnpacker或相关解密工具。关注相关开源项目的更新。算法中包含了环境变量签名可能依赖navigator.userAgent、屏幕分辨率等在 JS 代码中查找除了请求参数外是否还拼接了其他全局变量。在 Python 中需要模拟这些值。一个关键的调试技巧在定位到 JS 的签名函数后不要急于翻译成 Python。先尝试在 Node.js 环境或浏览器控制台中隔离运行这个函数。用一组固定的参数和密钥得到标准的签名结果。这个结果将成为你 Python 代码的“标准答案”。在 Python 复现过程中每一步排序后的键列表、拼接后的字符串、MD5 前的字节都打印出来与 JS 环境下的中间结果进行比对这样可以快速定位差异所在。逆向工程就像解谜需要耐心、细心和逻辑推理能力。从微信小程序反编译到 Python 复现加密算法这条路径打通后你将不仅获得一个可用的自动化工具更能深刻理解前端与后端交互中的安全机制设计以及如何从黑盒中寻找突破口。这种能力在安全研究、数据对接、甚至是日常的问题排查中都极具价值。