ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

wxappUnpacker小程序反编译:wxapkg解包与AST拆包排错

wxappUnpacker小程序反编译:wxapkg解包与AST拆包排错 简介wxappUnpacker 是一套面向微信小程序开发者与安全研究人员的反编译工具包用于将小程序包还原为可读的 WXML、WXSS、JavaScript 与 JSON 代码并支持主包与分包处理便于分析业务逻辑、优化加载性能及开展安全审计。压缩包共 780 个文件约 1.97MB以 js、json、md、ts、map 为主包含工具源码、依赖说明、类型声明与调试映射另有 license、cmd/ps1 脚本及配置文件目录结构较完整便于在本地搭建解析环境并按模块查阅。已有 269 人学习说明其在逆向分析与小程序机制研究场景中有一定参考价值。读者可据此了解反编译流程与关键文件构成对照主包、分包结构梳理页面配置、样式与交互实现为代码复用、问题排查和漏洞检测提供线索。使用时需注意仅限合法合规场景遵守微信开发者协议与相关授权要求。1. 第一次运行 wxappUnpacker 处理小程序反编译时我盯着控制台那行Wrong Header!看了半小时包是我从自己的测试机里导出来的路径没写错Node 也没崩依赖都在。问题出在包体本身首字节不是0xBE说明它在我动手之前就已经被处理过一遍。这件事之后我养成一个习惯拿到任何一个 wxapkg先花两分钟看头部和索引再决定要不要上 wxappUnpacker。小程序反编译这件事工具只占三成剩下七成在“你能不能判断这次还原算成功”。这篇按我实际的操作顺序走先讲容器里到底装了什么再走一遍从小程序包到可读工程的完整链路然后把我踩过的五类失败现象逐条对上原因和解法最后说还原完之后怎么读代码才有价值。新手可以照着命令跑通熟手可以看我在参数和校验点上的取舍。2. 容器与管线wxappUnpacker 处理的是哪几层文件2.1 一个 wxapkg 里装的不是压缩包是索引表加数据区很多人第一次解包会下意识去找解压密码其实 wxapkg 根本没有压缩这一层。它是一个自定义二进制容器结构大致是一字节起始标记0xBE接着三个四字节的长度字段一字节分隔标记0xED然后是文件数量跟着一张索引表每条记录包含“文件名长度 文件名 数据偏移 数据长度”索引表之后才是所有文件内容首尾相接的数据区。所有多字节整数都是大端序这点在你自己写脚本读的时候特别容易翻车按小端解析出来会得到一堆荒唐的数字。位置字段长度说明0起始标记1 字节固定0xBE被改过基本就是加密或二次封装1保留字段4 字节大端整数不同构建版本取值不一不用深究5索引区长度4 字节索引表总字节数9数据区长度4 字节所有文件内容加起来的总长13分隔标记1 字节固定0xED14文件数量4 字节索引条目数18 起索引条目变长文件名长度 / 文件名 / 偏移 / 长度真正的坑在数据区。解出来的东西离“源代码工程”差得很远所有页面的 JS 逻辑被define(pages/xxx/index.js, function(...){...})这样的包裹串成一整个app-service.jsWXML 被压成一行标签属性引号被吞掉{{}}里的表达式被做过转义WXSS 的import被内联展开rpx换算规则散落在各处。所以严格说这不是“反编译”是“结构还原”。我在评估一个包的时候从来不用“能不能还原出源码”当标准而是看“能不能还原到可读、可搜索、可定位逻辑”的程度。还有一个常被忽略的点包里不只有代码。图片、字体、音频这些二进制资源是原样存着的索引表里能拿到准确的偏移和长度所以资源还原的准确率接近 100%出问题的一定是代码那几层。把这条记住后面排查的时候能省一半时间——资源对得上、代码对不上问题一定出在解析器不在容器。2.2 四个脚本各管一层串起来才是一次完整还原wxappUnpacker 不是一个单体程序是入口加几个分工明确的脚本。入口wuWxapkg.js只干容器层的事校验头部、读索引、按偏移把每个文件切出来写盘然后根据文件类型决定要不要继续往下交给别的脚本。真正的还原逻辑分散在wuJs.js拆app-service.js、wuWxml.js还原 WXML、wuWxss.js还原样式、wuConfig.js处理app-config.json里。wuJs.js的做法值得单独说。它用esprima把整个app-service.js解析成 AST然后在语法树上遍历找形如define(模块名, 函数体)的调用节点把模块名当路径、把函数体重新生成代码写出去。这个思路的成立前提是构建产物还保留着可识别的define包裹。一旦构建链路换成别的打包器、或者基础库版本变了把模块表换了形式AST 里就找不到这些节点结果就是只得到一个几十万行的巨型 JS——这不是工具坏了是它的识别假设失效了。wuWxml.js更像一套规则集而不是编译器。它做的是逆向压缩动作把被吞掉的属性引号补回来、把自闭合标签补全、把被转义的表达式还原成{{}}、把被压成一行的结构按缩进展开。所以它对“标准写法”效果很好碰上自定义组件、新语法、或者被内联成字符串模板的片段还原质量会断崖式下降。wuWxss.js的逻辑类似主要处理import展开后的还原和rpx相关的换算。理解这条管线之后很多报错的性质就清楚了容器层报错头部、长度、索引是硬错误必须解决解析层报错AST 遍历失败、正则匹配不上是软错误经常是“部分还原”还能继续用。我一般会先把容器层跑通拿到全部原始文件再决定要不要花时间调解析层——因为光靠原始文件里的字符串搜索很多时候已经够定位问题了。2.3 三个开关决定你要不要返工入口脚本的参数不多但每一个都直接影响后面要不要重跑。-o表示只解容器、不做代码还原跑完得到的是原始文件树这个模式在我只想确认包结构和资源清单的时候特别省时间几十秒出结果不会卡在 AST 解析上。-d用于处理子包。-s主包目录指定主包解出来的路径子包里的公共资源引用要靠它来定位。# 老版本解析依赖在新 Node 上容易抛错建议固定到 12/14 nvm use 14 npm install # esprima / escodegen / js-beautify / css 等解析依赖 # 只解容器不还原代码先看目录结构和资源清单快 node wuWxapkg.js -o ./pkg/__APP__.wxapkg # 完整还原产物输出到与包同名的目录 node wuWxapkg.js ./pkg/__APP__.wxapkg # 子包必须带 -s且主包要先跑完顺序反了会找不到公共路径 node wuWxapkg.js -s./__APP__ ./pkg/sub1.wxapkg-o这个开关我建议所有人都先用一遍。原因很实际完整还原在大包上可能跑几分钟如果包体本身有问题你会在几分钟后拿到一个半成品然后再回头怀疑是解析器的问题。先用-o确认容器层干净把变量一个个排除掉比一次性跑完再猜要快得多。这个习惯类似调试时先打日志再下断点看着笨实际最省时间。3. 复现链路从小程序包到可读工程的四步实操3.1 先定位包体、校验头部别急着上工具包体的位置和命名是有规律的。主包通常叫__APP__.wxapkg子包按分包名命名同一目录下平铺。我一般先把整个目录拷出来按“主包 子包”分组然后对每个文件跑一遍头部检查。这一步的意义不在于“检查能不能用”而在于“提前知道哪些包需要特殊处理”避免跑到一半才发现某个子包是加密的。import struct, sys, os def peek(path): with open(path, rb) as f: buf f.read() # 首字节不是 0xBE说明包体被处理过加密或二次封装先别往下走 if not buf or buf[0] ! 0xBE: print(f{os.path.basename(path)}: 首字节异常跳过) return info1, idx_len, body_len struct.unpack(III, buf[1:13]) # 全程大端序 if buf[13] ! 0xED: print(f{os.path.basename(path)}: 分隔标记异常索引区可能被替换) return count struct.unpack(I, buf[14:18])[0] print(f{os.path.basename(path)} 文件数{count} 索引{idx_len} 数据{body_len}) p 18 for _ in range(min(count, 10)): # 只打印前 10 条够看结构了 n struct.unpack(I, buf[p:p4])[0]; p 4 name buf[p:pn].decode(utf-8, replace); p n off, size struct.unpack(II, buf[p:p8]); p 8 print(f {name} off{off} size{size}) if __name__ __main__: for f in sys.argv[1:]: peek(f)这段脚本的关键在三个地方III里的是大端序写成会解析出完全对不上的数字偏移off是相对于数据区起点的不是相对文件头实际读取时要加上数据区起始位置文件名用utf-8加replace容错解码因为个别包里的路径是转义过的。跑完这个你对包里有多少文件、有没有异常条目、资源大概占多少心里就有数了。3.2 主包先跑、子包后跑顺序错了后面全白干分包机制是这套流程里最容易出错的一环。主包里有公共组件、公共样式、公共工具函数子包在引用这些资源时用的是相对主包根的路径。wuWxapkg.js在处理子包时需要靠-s参数知道主包解到哪儿了才能把这些引用解析成真实文件。如果顺序反了或者忘了带-s你会得到一堆“找不到模块”的报错然后误以为是解析器不支持。我的操作顺序固定成三步先跑主包并确认产物目录结构正确再逐个跑子包每个都带上-s主包产物目录最后把子包的产物合并回主包目录冲突的文件按“主包优先、子包覆盖非公共路径”处理。第三步不是工具做的是手工或者脚本做的因为合并策略取决于你要干什么——如果只是想搜索关键字合不合并无所谓如果要还原完整工程结构就必须合。还有一个细节子包之间可能互相引用。这种情况下合并顺序会影响结果我一般先按app.json里的分包声明顺序建一个依赖图把叶子包先合公共包最后合。听着麻烦但比事后对着几十个“模块未定义”试错要快。3.3 五个检查点判断这次还原算不算成功跑完不知道成不成是最浪费时间的状态。我给自己定了五个检查点从粗到细依次过任何一个不通过就停下来先解决它不带病往下走。检查点怎么查不通过说明什么目录结构产物里是否有pages/、components/等目录且与配置文件里的路径一一对应容器层解析有问题索引表读错了JS 拆分app-service.js是否被拆成多个文件文件名是否就是模块路径define包裹没被识别AST 遍历失效WXML 结构标签是否成对闭合{{}}是否成对属性引号是否补全还原规则没有覆盖到这批写法WXSS 引用import是否已经展开成真实样式内容样式层还原被跳过或顺序错乱资源完整性抽几张图片对比字节数与原包索引记录的长度数据区偏移计算有偏差属于严重问题第五个检查点最容易被跳过但它最能说明问题。资源是原样存储的索引表里有准确长度如果产物里图片字节数和索引记录对不上那一定是“偏移计算”或者“数据区起点”错了这时候去调 JS 解析器纯属浪费时间。反过来如果资源全对但代码不对问题就精确定位在解析层可以直接去看wuJs.js的遍历逻辑有没有匹配上。4. 排查清单五类常见故障的现象、原因和解法4.1 首字节不是0xBE入口直接报Wrong Header!现象脚本一启动就退出提示头部校验失败日志里连文件数量都没打印出来。原因包体在你拿到之前已经被处理过了常见于从某些环境导出的包头部被替换或整体做了加密wuWxapkg.js的第一道校验就过不去。解决先用 3.1 的脚本确认首字节到底是什么如果是固定偏移的替换可以定位到原始标记的位置再看后面结构是否完整如果整个数据区都被加密那容器层工具无能为力得先解决解密再谈还原。我一般的判断是头部异常但数据区长度字段合理说明只是头被改连长度字段都是乱的说明整包被重写过。4.2 只得到一个几十万行的app-service.js没有拆成单文件现象容器层跑通了文件都出来了但 JS 目录里只有一个巨型文件其余页面文件一个没有。原因wuJs.js靠识别define(模块名, 函数体)这种调用形式来拆分模块构建产物一旦换成别的打包器或者模块表改成了数组映射AST 里就找不到这个节点。解决先在这个巨型文件里搜define(和webpackJsonp看它用的是哪种模块形式如果是define检查是不是被包在了一层eval或者字符串里这种要先剥一层如果是别家的模块表就自己写个脚本按模块表里的名称数组和函数数组配对还原思路一样只是入口节点不同。4.3 WXML 还原出来一堆残缺表达式{{}}对不上现象结构大致在但属性值变成半截表达式里的变量名被截断控制台还刷了一堆正则匹配失败的警告。原因这批模板用了自定义组件、新语法或者被压缩成了字符串拼接形式wuWxml.js的规则集覆盖不到。解决先别急着改规则降低期望——大多数排查场景里能在这堆文本里做关键字搜索就够了。如果确实需要结构就针对具体文件手工还原几个关键模板别想着一次性全救回来。我吃过这个亏花了两天调正则最后发现需要的三个关键字段在原始文本里直接搜就有。4.4 子包报“找不到模块”路径解析成一堆../现象子包单独跑能出文件但引用主包资源的语句全变成了失效路径报错集中在公共组件和工具函数上。原因没带-s参数或者-s指向的目录不是主包产物根目录或者跑的顺序反了——子包先跑主包目录还不存在。解决严格按“主包先、子包后、每个子包都带-s主包产物目录”的顺序重跑一遍如果还不行检查-s的值是不是写成了相对路径且当前工作目录不对改成绝对路径最稳。这个错误我犯过不止一次后来干脆在脚本里写死绝对路径。4.5 Node 版本太新装依赖时就崩现象npm install报错或者跑起来抛SyntaxError: Unexpected token堆栈指向解析库内部。原因这类工具大多写于 Node 10 到 12 的年代依赖的解析库版本很老在新版本 Node 上语法或 API 对不上。解决用版本管理器切到 12 或 14重新装依赖再跑。别试图升级依赖版本来“修好”它解析库的主版本一升esprima的 AST 节点结构就变了wuJs.js里的遍历逻辑会全部失效那才是真正的麻烦。我的做法是给这个工具单独建一个环境目录锁死版本不跟其他项目混用。5. 读懂还原产物把文件树变成可评估的攻击面5.1 先扫接口和密钥再谈逻辑还原出来的文件树有几百上千个文件从头读是不可能的。我的第一遍永远是关键字扫不看逻辑只看“面”。这一步的目标不是找漏洞是画出一张“这个包跟外界有多少条通路”的地图。搜索关键词想定位的东西后续动作request、http、api网络请求封装与接口前缀汇总出接口清单看有没有内网地址、测试环境地址token、sign、encrypt鉴权与签名算法判断签名是在客户端算还是服务端算key、secret、appid硬编码凭据单独列出来评估是否可被直接复用upload、download文件流转路径看有没有未校验的存储路径eval、Function(动态执行点这类点在客户端脚本里通常是风险放大器扫完你会得到两样东西一张接口表和一份“客户端自己算出来的安全参数”列表。后者是重点。签名算法放在客户端意味着算法本身对攻击者完全公开安全性只能靠密钥而密钥通常也在这个包里。这不是说一定有问题而是说这条链路的安全假设必须由服务端的二次校验来兜底还原产物恰好是验证“有没有兜底”的最直接材料。5.2 客户端校验只是体验层重点看服务端有没有兜底还原之后最容易产生的错觉是“这里能改所以有漏洞”。实际上客户端的所有判断都可以被绕过这句话在业内是常识但真正做评估的时候还是容易上头。我的做法是把所有客户端校验点列出来然后针对每一个去接口层找对应的服务端校验证据。找不到证据的才写成风险项。典型的几类客户端校验金额在提交前算好并做格式化服务端如果直接采信这个值就是问题权限判断靠本地缓存里的角色字段服务端如果不重新查就是问题次数限制在本地做计数服务端如果不记账就是问题。这三类在还原产物里都能找到痕迹——格式化函数、角色字段的读取点、计数变量的自增位置。找到痕迹之后判断依据不是代码写得好不好看而是“这个值最终有没有被送到服务端并被服务端复核”这需要结合接口表一起看。我在写这类评估记录时有个固定格式现象哪个文件的哪一行做了什么、影响路径这个值怎么流到接口、判定依据服务端有没有对应校验有就写“已兜底”没有就写“待确认”。这个格式看起来啰嗦但它能避免把“看起来不安全”写成“确认存在风险”后者是要负责的。5.3 把一次性动作变成可重复流程单次还原的价值有限真正省时间的是把它变成流程。我现在的做法是按日期建目录每次拿到新包就跑一遍固定脚本产物按版本归档然后用目录对比看两个版本之间改了哪些文件。这个动作在跟踪同一套业务的不同版本时特别有效——改动点往往就是新功能或者刚修过的地方重点看那几个文件比通读整棵树高效一个量级。#!/usr/bin/env bash set -e STAMP$(date %Y%m%d) OUT./dump/$STAMP mkdir -p $OUT # 主包先解产物目录作为后续子包的 -s 基准 node wuWxapkg.js -o $OUT/__APP__ ./pkg/__APP__.wxapkg /dev/null node wuWxapkg.js $OUT/__APP__ ./pkg/__APP__.wxapkg # 子包按声明顺序逐个处理-s 必须是绝对路径避免工作目录漂移 for sub in ./pkg/sub*.wxapkg; do node wuWxapkg.js -s$(realpath $OUT/__APP__) $sub || echo skip: $sub done # 只保留业务代码二进制资源另存避免归档体积膨胀 find $OUT -type f \( -name *.png -o -name *.jpg -o -name *.ttf \) -delete echo done - $OUT脚本里|| echo skip这一句是刻意的子包处理失败很常见但我更希望整批跑完再统一看哪些失败而不是第一个失败就中断。归档时删掉二进制资源是为了控制体积文本产物才是后续对比和搜索的对象。这套流程跑顺之后一个中等规模的包从拿到到可搜索大概十分钟以内。6. 一个具体技巧用目录对比锁定改动比通读整棵树高效得多如果只让我留一个技巧就是这个用diff -rq对比两个版本的还原产物只看差异文件列表。# 只列差异文件名不打印内容几千个文件也能一眼看完 diff -rq ./dump/20240110/__APP__ ./dump/20240118/__APP__ | grep -v ^Only in这个命令在跟踪同一套业务的迭代时几乎是作弊。业务方每次发版改动的文件通常只有几十个剩下几百个是原封不动的。你把差异列表拉出来按目录聚类一眼就能看出这次动的是哪个模块——如果集中在一个业务目录那就是功能迭代如果散落在网络层和工具目录那就值得多看一眼因为这类改动往往涉及鉴权、加密或者请求封装的调整是整个包里最需要复核的部分。我最深的一次教训跟这个技巧正好相反。早期我做评估拿到还原产物就一头扎进app-service.js几百个模块挨个读读了整整两天写出来的记录全是“某某函数实现了某某功能”一句有用的判断都没有。后来带我的前辈问了一句“你知道这次发版改了什么吗”我答不上来。他让我先做版本对比结果改动只集中在三个文件其中一个是签名函数——把一段本地拼接改成了走服务端下发。这一处改动直接推翻了前面两天的很多结论。从那以后我每次做这类分析都强制走一遍“先对比、再定点、最后才通读”的顺序不跳步。前面那些准备工作——头部校验、-o先跑一遍、五个检查点验收——看着繁琐本质上都是同一个道理先把变量控制住再谈结论。这份 wxappUnpacker 的整理版我放在资源里了入口脚本、依赖清单还有我这些年攒下来的报错对照备注都在里面几个典型失败的现场处理方式也一并写上去了拿去按第 3 章的顺序跑一遍就能出结果。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表