"xxx 啊,听说你是咱们部门最会 ‘搞事情’?老板让咱们研究一下小程序,看看人家是怎么实现那个酷炫功能的...,然后看看怎么防护?"
周一早上,我正对着电脑屏幕上的猫咪视频傻笑,突然收到了产品经理发来的消息。作为一个资深摸鱼专家,我的第一反应是:这活能不能推给别人?但看到消息末尾的"老板说"三个字,我瞬间像被浇了一盆冷水——摸鱼计划泡汤了。
"可是老板,咱们没有人家的源代码啊..."我试图垂死挣扎。
"不会逆向工程吗?年轻人要多学习嘛!"产品经理的回复让我彻底放弃了抵抗。
于是,我被迫踏上了微信小程序逆向工程的不归路。经过一番摸爬滚打,我终于从一个逆向小白成长为"半吊子专家"。今天,我就把这段"血泪史"整理成一份完整指南,希望能帮助那些和我一样"被迫营业"的摸鱼人们少走弯路...
微信小程序凭借其无需安装、即开即用的特性,已经成为移动互联网生态中不可或缺的一部分。作为开发者,了解微信小程序的内部结构和实现原理,不仅可以帮助我们更好地开发自己的小程序,还能从优秀的小程序中学习到先进的设计理念和技术实现。
本文将为大家提供一份完整的微信小程序逆向工程指南,涵盖从获取小程序包(.wxapkg)、解密到最终反编译的全过程,结合 PC 端和移动端两种获取方式,帮助大家轻松掌握微信小程序逆向。
如果您的安卓手机已经获取了 Root 权限,可以直接从手机中提取小程序包:
/data/data/com.tencent.mm/MicroMsg//appbrand/pkg/
.wxapkg 为后缀的文件,这些就是微信小程序的安装包如果您不想 Root 手机,可以使用安卓模拟器来获取小程序包:
/data/data/com.tencent.mm/MicroMsg//appbrand/pkg/
.wxapkg 文件,长按压缩后通过 QQ 发送到电脑除了移动端,我们也可以从 Windows PC 端的微信中获取小程序包。不过需要注意的是,PC 端的 wxapkg 包是被加密存储的:
C:\Users\[用户名]\Documents\WeChat Files\Applet\[小程序ID]\_APP_.wxapkg
wx2xxx84w9w7a3xxxx)PC 端微信对 wxapkg 包进行了加密处理,加密后的文件以 V1MMWX 开头。加密过程分为两步:
AES 加密:
saltiest
the iv: 16 bytes)对原始 wxapkg 包的前 1023 个字节进行 AES 加密XOR 加密:
0x66
文件组装:
V1MMWX 标识使用专门的解密工具 pc_wxapkg_decrypt.exe(360 报毒,我也就不提供下载了,有需要的可以自己搜索下载) 可以轻松解密 PC 端的 wxapkg 包:
pc_wxapkg_decrypt.exe -wxid 微信小程序id -in 要解密的wxapkg路径 -out 解密后的路径
详细参数说明:
-in:需要解密的 wxapkg 文件路径(默认:__APP__.wxapkg)-out:解密后的 wxapkg 文件路径(默认:dec.wxapkg)-wxid:小程序的 ID(必需参数)-iv:AES 加密的 IV,默认不需要设置(默认:the iv: 16 bytes)-salt:PBKDF2 用到的 salt,默认不需要设置(默认:saltiest)pc_wxapkg_decrypt.exe -wxid wx2xxx84w9w7a3xxxx -in "C:\Users\xxx\Documents\WeChat Files\Applet\wx2xxx84w9w7a3xxxx\_APP_.wxapkg" -out "D:\decrypted.wxapkg"
获取并解密 wxapkg 包后,我们需要使用反编译工具将其转换为可读的源代码。这里推荐使用 CrackMinApp 工具。(360 报毒,我也就不提供下载了,有需要的可以自己搜索下载)
CrackMinApp 是一款图形化的微信小程序反编译工具,使用 C# 和 Node.js 开发,主要特点包括:
准备工作:
wxapkg 目录下运行反编译:
CrackMinApp.exe
查看结果:
本文为大家提供了一份完整的微信小程序逆向工程指南,涵盖了从获取小程序包、解密到反编译的全过程。无论是移动端还是 PC 端,我们都可以通过相应的方法获取到小程序包,并使用专门的工具进行解密和反编译。
pc_wxapkg_decrypt.exe(PC 端)CrackMinApp
参考资料:
COCOS 小程序目录主要结构:
- assets/:存放自定的资源文件(图片、音频、视频等)
- jsb-default/:存放默认的 JSB 文件(包含小程序的主要逻辑)
- project.json:项目配置文件,包含小程序的元数据和设置
- src/:存放源代码文件(主要是 JS 文件)
- wxss/:存放 WXSS 样式文件(与 WXML 文件对应)
- wxml/:存放 WXML 视图文件(与 JS 文件对应)
src 的分析就不累述了,主要是 JS 文件,懂的都懂,如果是图片资源, 主要是在 assets 目录下。我这主要做了一个脚本,识别看看有没特别大的图,如果超过阀值,就记录下来,方便后续分析。
在微信小程序逆向工程中,我们经常会遇到使用 Brotli 压缩算法压缩的 .br 文件。这些文件通常包含了小程序的关键资源,如代码、图片和数据。默认情况下,我们可能需要手动一个个处理这些文件,这在文件数量较多时会变得非常繁琐且低效。
我在处理一个微信小程序逆向项目时,发现目录下有多个 .br 文件需要解密。原始的 decrypt_brotli.py 脚本只能处理单个文件,这显然无法满足批量处理的需求。因此,我决定对脚本进行改造,使其能够自动搜索并批量处理所有 .br 文件。
原始的 decrypt_brotli.py 脚本主要包含以下功能:
os 和 brotli)decrypt_brotli_file 函数用于解密单个 .br 文件main 函数中指定单个文件路径并解密为了实现批量处理,我需要:
.br 文件main 函数以支持批量处理.br 文件首先,我添加了一个 find_br_files 函数,用于在指定目录中递归查找所有 .br 文件:
def find_br_files(directory):
"""在指定目录中递归查找所有.br文件"""
br_files = []
print(f"正在搜索 {directory} 目录中的.br文件...")
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith('.br'):
br_files.append(os.path.join(root, file))
print(f"找到 {len(br_files)} 个.br文件")
return br_files
这个函数使用 os.walk 遍历目录树,检查每个文件是否以 .br 结尾,并将符合条件的文件路径添加到列表中。
接下来,我修改了 main 函数,使其能够:
find_br_files 函数查找所有 .br 文件def main():
"""主函数"""
# 定义搜索目录
search_dir = r'目录'
# 查找所有.br文件
br_files = find_br_files(search_dir)
if not br_files:
print("没有找到.br文件,程序结束")
return
# 统计结果
total_files = len(br_files)
success_count = 0
fail_count = 0
print(f"\n开始解密 {total_files} 个.br文件...")
print("=" * 60)
# 逐个解密文件
for i, br_file in enumerate(br_files, 1):
print(f"\n[{i}/{total_files}] 处理文件:")
if decrypt_brotli_file(br_file):
success_count += 1
else:
fail_count += 1
# 输出结果统计
print("\n" + "=" * 60)
print("解密结果统计:")
print(f"总文件数:{total_files}")
print(f"成功:{success_count}")
print(f"失败:{fail_count}")
print(f"成功率:{success_count/total_files*100:.2f}%")
print("\n所有操作完成!")
为了确保脚本的稳定性,我还增强了 decrypt_brotli_file 函数的错误处理能力:
def decrypt_brotli_file(br_file):
"""解密单个.br文件"""
try:
# 构建输出文件路径
output_file = br_file[:-3] # 移除.br扩展名
print(f" 输入文件: {br_file}")
print(f" 输出文件: {output_file}")
# 读取压缩文件
with open(br_file, 'rb') as f:
compressed_data = f.read()
# 获取压缩前的文件大小
compressed_size = len(compressed_data)
print(f" 压缩大小: {compressed_size / 1024 / 1024:.2f} MB")
# 解压缩
decompressed_data = brotli.decompress(compressed_data)
# 获取解压缩后的文件大小
decompressed_size = len(decompressed_data)
print(f" 解压大小: {decompressed_size / 1024 / 1024:.2f} MB")
print(f" 压缩率: {compressed_size / decompressed_size * 100:.2f}%")
# 写入解压缩文件
with open(output_file, 'wb') as f:
f.write(decompressed_data)
print(" 解密成功!")
return True
except Exception as e:
print(f" 解密失败: {str(e)}")
return False
修改后的脚本运行结果如下:
正在搜索 G:\localgit\wechatMiniAppReverse-main\out 目录中的.br文件...
找到 3 个.br文件
开始解密 3 个.br文件...
============================================================
[1/3] 处理文件:
输入文件: 小程序目录\80ce864cae15e82f.webgl.data.unityweb.bin.br
输出文件: 小程序目录\80ce864cae15e82f.webgl.data.unityweb.bin
压缩大小: 3.90 MB
解压大小: 12.50 MB
压缩率: 31.20%
解密成功!
[2/3] 处理文件:
输入文件: 小程序目录\35cd412081ec83c5.webgl.wasm.code.unityweb.wasm.br
输出文件: 小程序目录\35cd412081ec83c5.webgl.wasm.code.unityweb.wasm
压缩大小: 5.60 MB
解压大小: 40.50 MB
压缩率: 13.83%
解密成功!
[3/3] 处理文件:
输入文件: 小程序目录\35cd412081ec83c5.webgl.wasm.code.unityweb.wasm.br
输出文件: 小程序目录\35cd412081ec83c5.webgl.wasm.code.unityweb.wasm
压缩大小: 3.90 MB
解压大小: 18.60 MB
压缩率: 20.97%
解密成功!
============================================================
解密结果统计:
总文件数:3
成功:3
失败:0
成功率:100.00%
所有操作完成!
最后,附上完整的 decrypt_brotli.py 脚本代码:
import os
import brotli
def decrypt_brotli_file(br_file):
"""解密单个.br文件"""
try:
# 构建输出文件路径
output_file = br_file[:-3] # 移除.br扩展名
print(f" 输入文件: {br_file}")
print(f" 输出文件: {output_file}")
# 读取压缩文件
with open(br_file, 'rb') as f:
compressed_data = f.read()
# 获取压缩前的文件大小
compressed_size = len(compressed_data)
print(f" 压缩大小: {compressed_size / 1024 / 1024:.2f} MB")
# 解压缩
decompressed_data = brotli.decompress(compressed_data)
# 获取解压缩后的文件大小
decompressed_size = len(decompressed_data)
print(f" 解压大小: {decompressed_size / 1024 / 1024:.2f} MB")
print(f" 压缩率: {compressed_size / decompressed_size * 100:.2f}%")
# 写入解压缩文件
with open(output_file, 'wb') as f:
f.write(decompressed_data)
print(" 解密成功!")
return True
except Exception as e:
print(f" 解密失败: {str(e)}")
return False
def find_br_files(directory):
"""在指定目录中递归查找所有.br文件"""
br_files = []
print(f"正在搜索 {directory} 目录中的.br文件...")
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith('.br'):
br_files.append(os.path.join(root, file))
print(f"找到 {len(br_files)} 个.br文件")
return br_files
def main():
"""主函数"""
# 定义搜索目录
search_dir = r'小程序目录'
# 查找所有.br文件
br_files = find_br_files(search_dir)
if not br_files:
print("没有找到.br文件,程序结束")
return
# 统计结果
total_files = len(br_files)
success_count = 0
fail_count = 0
print(f"\n开始解密 {total_files} 个.br文件...")
print("=" * 60)
# 逐个解密文件
for i, br_file in enumerate(br_files, 1):
print(f"\n[{i}/{total_files}] 处理文件:")
if decrypt_brotli_file(br_file):
success_count += 1
else:
fail_count += 1
# 输出结果统计
print("\n" + "=" * 60)
print("解密结果统计:")
print(f"总文件数:{total_files}")
print(f"成功:{success_count}")
print(f"失败:{fail_count}")
print(f"成功率:{success_count/total_files*100:.2f}%")
print("\n所有操作完成!")
if __name__ == "__main__":
main()
src 源码部分就不展示了,目前提出的需求主要是分析图片资源,src 暂时还没提出需求,留到另一篇 blog 吧。
这里主要先说说图片资源
我们已经完成了前期处理,得到了 bin 文件, AssetStudio 这款工具下载地址:AssetStudio
AssetStudioGUI.exe
File → Load File
小程序目录
xxxx.webgl.data.unityweb.bin 文件,点击 打开
AssetStudio 会自动解析资源包,并在左侧面板显示资源类型:
Assets:所有资源Animations:动画资源AudioClips:音频资源Fonts:字体资源Materials:材质资源Meshes:模型资源Shader:着色器资源TextAssets:文本资源Textures:图片资源(我们主要关注这个)Textures 选项卡Export selected assets
确定,等待提取完成更新作者:zasdsd
更新日期:2026年1月6日