写作与公文
在 macOS 上提取、解密并可视化微信 Mac 4.x…
适用于 macOS 上已登录微信 Mac 4.x,想把收藏数据库解析成带图表、词云、筛选浏览和详情弹窗的 HTML 报告的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 macOS 上提取、解密并可视化微信特定
解决思路
- 安装依赖:pip3 install frida frida-tools pycryptodome;准备 Python 3.9+、Frida 17.x、macOS、已登录的 WeChat Mac 4.x。
- 按项目布局准备脚本:scripts/parse_favorites.py(SQLite/CSV/JSON → 统一 JSON)、scripts/generate_report.py(JSON → 单文件 HTML)、scripts/demo_data.py(合成测试数据)。
- Step 1 去除 WeChat 的 hardened runtime:先 killall WeChat 2>/dev/null; sleep 2;再 cp -R /Applications/WeChat.app ~/Desktop/WeChat.app;然后 codesign --force --deep --sign - ~/Desktop/WeChat.app。不要用 sudo,否则数据目录变成 /var/root/… 并破坏 DB 路径解析。
- Step 2 保存 hook_wechat.js:通过 Module.findExportByName('libcommonCrypto.dylib', 'CCKeyDerivationPBKDF') 定位导出;Interceptor.attach 该函数;onEnter 记录 args[3] password ptr、args[4] password len、args[5] salt ptr、args[6] salt len、args[9] iterations、args[10] derived key ptr、args[11] derived key len;onLeave 读取 salt 和 derived key 字节,转成十六进制,构造 {salt, key, ts} JSON 行并用 send(line) 输出。
- Step 3 运行 Frida hook:frida ~/Desktop/WeChat.app/Contents/MacOS/WeChat -l hook_wechat.js --runtime=v8 2>/dev/null | tee /tmp/wechat_frida_keys.log &;启动微信并登录,然后打开收藏(Favorites)tab;等待约 60 秒让所有 DB key 派生完成,再 Ctrl+C。关键:favorite.db 只在用户进入收藏 tab 时打开,若在打开收藏前 hook,key 不会出现。
- Step 4 匹配 key 到 favorite.db:编写 match_key.py。find_db 在 ~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files 下查找 */db_storage/favorite/favorite.db,若有 wxid 则选包含该 wxid 的路径。read_salt 打开 DB 跳过前 16 字节(SQLite header),再读 16 字节作为 salt。match 将 salt 转 hex,遍历 /tmp/wechat_frida_keys.log 每行 JSON,匹配 entry['salt'] 后返回 entry['key']。运行后输出 enc_key (hex) 和 db path。
- Step 5 解密数据库:编写 decrypt_db.py,使用 SQLCipher 4 参数 AES-256-CBC、HMAC-SHA512、kdf_iter=256000、page_size=4096、reserve=80(64 HMAC + 16 IV)、salt 位于文件前 16 字节。用 hashlib.pbkdf2_hmac('sha512', raw_key, salt, KDF_ITER, dklen=32) 得到 page_key,再用 hashlib.pbkdf2_hmac('sha512', page_key, salt, 1, dklen=32) 得到 hmac_key。分页处理:第 1 页取 data[16:PAGE_SIZE],后续每 PAGE_SIZE 取一页;每页 content 为前 PAGE_SIZE-RESERVE,reserved 为后 RESERVE,IV 为 reserved[HMAC_SIZE:HMAC_SIZE+IV_SIZE];用 AES.new(page_key, AES.MODE_CBC, iv).decrypt(content);第 1 页恢复 SQLite header;其他页直接输出 plaintext;每页输出后补 RESERVE 个零字节。运行:python3 decrypt_db.py ~/Library/Containers/.../favorite.db <32-byte-key-hex> /tmp/favorite_decrypted.db。
- Step 6 解析收藏:WeChat 4.x 使用单表 fav_db_item,内容为 XML(3.x 使用 FavItems/FavDataItem 拆分)。TYPE_MAP 映射 1:text、2:image、3:voice、4:video、5:playlist、6:location、7:attachment、8:article、43:video_channel、49:link。parse_xml_content 提取 title、desc、source、url:article(type=8) 用 <pagetitle> 或 <title>、<url>、<sourcename> 或 <fromnickname>、<desc>;link(type=49) 用 <title>、<url>、<sourcename>、<desc>;voice/video(3,4,43) 遍历 <dataitem> 找第一个非空 <datatitle> 作为 title,source 用 <fromnickname>;其他类型用 <title> 或 <pagetitle>、<desc> 或 <content>、<fromnickname>。parse 查询 SELECT localId, favLocalId, type, createTime, updateTime, xmlBuf, tagNames FROM fav_db_item ORDER BY createTime;构造 id、type、created_at/updated_at(datetime.utcfromtimestamp)、title、desc、source、url、tags(按逗号分割 tagNames 并过滤空值)。运行:python3 parse_favorites.py /tmp/favorite_decrypted.db /tmp/data.json。
- Step 7 生成 HTML 报告:运行 python3 generate_report.py --input /tmp/data.json --output /tmp/report.html;然后必须通过本地 HTTP 服务打开,因为 file:// 会破坏 ECharts 事件委托:cd /tmp && python3 -m http.server 8765;open http://localhost:8765/report.html。
- 配置参考:SQLCipher version=4;Cipher=AES-256-CBC;HMAC=HMAC-SHA512;KDF iterations=256000;Page size=4096 bytes;Reserve per page=80 bytes(64 HMAC + 16 IV);Salt location=文件字节 0-15;Table name(4.x)=fav_db_item;Article title field=<pagetitle>,不是 <title>。
- 修复 Key not found in log:确认在 Frida 附加期间打开过收藏 tab;用 wc -l /tmp/wechat_frida_keys.log 检查日志是否有条目;用 xxd ~/…/favorite.db | head -2 检查 ASCII header 后的第 16-31 字节 salt 是否匹配。
- 修复 database disk image is malformed:重新核对 KDF_ITER=256000 和 PAGE_SIZE=4096;若微信更新导致参数变化,尝试 kdf_iter=64000(SQLCipher 3 默认)作为 fallback。
- 修复 codesign: No identity found:使用 ad-hoc 签名 codesign --force --deep --sign - ~/Desktop/WeChat.app,不要传证书名。
- 修复报告图片 broken:缩略图 URL 是微信 CDN 链接,需要活跃网络会话;生成 img 标签时加 onerror 处理器,使用 " 避免内联 HTML 引号冲突。
- 修复 onclick 在 file:// 不触发:不要用内联 onclick,改为在父元素上做事件委托,例如 document.getElementById('card-list').addEventListener('click', function(e){var card=e.target.closest('.fav-card'); if(card) showDetail(card.dataset.id);});
- 修复 WeChat 更新后 hook 停止工作:重新复制并重签名 app bundle,然后重新运行完整 pipeline;PBKDF2 hook 目标是系统库 libcommonCrypto.dylib,对微信二进制变化有韧性,但重签名步骤必须重复。
- 生成报告应包含:Summary cards 静态 KPI 卡片;Monthly trend ECharts line + area;Type distribution ECharts doughnut;Top 15 sources ECharts horizontal bar;Activity heatmap ECharts heatmap(weekday × hour);Word cloud 使用 echarts-wordcloud;Tag cloud CSS flex tags;Favorites browser 卡片网格,带 type/tag 过滤、全文搜索、分页;Detail modal 显示完整内容、URL、source、tags。
- 已知限制也要注意:image/video/file 二进制 blob 存在微信加密 CDN,不能离线预览;关键提取需要 macOS + Frida,无 Windows/Linux 支持;每次微信更新后桌面副本必须重新签名;tagNames 列存逗号分隔标签字符串,空标签在客户端过滤。
适用边界 · 注意事项
- 不适用于非 macOS、未安装或未登录 WeChat Mac 4.x 的环境;无 Windows/Linux 支持。
- 不适用于无法使用 Frida 注入或不能重新签名微信 App Store 构建的场景;每次微信更新后需重新复制并重签名桌面 App。
- 不适用于需要离线预览图片/视频/文件原件的场景;这些二进制存于微信加密 CDN,缩略图也需要网络会话。