ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Go 实现 PC 微信聊天记录导出:密钥提取与 SQLite 解密实战

Go 实现 PC 微信聊天记录导出:密钥提取与 SQLite 解密实战 简介这是一份面向PC端微信用户的聊天记录备份工具源码基于wails框架与React前端构建核心解决微信聊天记录难以长期留存、版本停更后无法回看的问题。工具界面尽量贴近微信原生风格降低上手成本理论上兼容Windows 32/64位各版本微信支持多开账号选择与数据切换。功能覆盖图片、视频、语音、文件、名片、定位、通话、转账、小程序、视频号、QQ音乐及第三方分享等十余种消息类型并提供按类型、日期、群成员检索增量导出、书签、会话分享与自动定位浏览位置等实用能力。资源包共156个文件以129个png界面素材、8个go后端源码为主辅以md说明、json配置、plist与nsi打包脚本等压缩后约9.82MB目录结构清晰。已有771人学习下载适合具备Go与前端基础、希望自建聊天记录归档方案的开发者参考可从中了解消息解析、数据库解密与桌面端打包的完整实现思路。1. 一键导出PC微信聊天记录工具Go 源码能解决什么不能解决什么PC 微信的聊天记录存在本地加密数据库里很多人第一次想导出时都会卡在同一个地方文件找到了打开是乱码。Msg目录下的.db文件用普通 SQLite 工具直接读要么报「file is not a database」要么读出来全是二进制块。这不是文件损坏是微信对数据库做了加密密钥跟当前登录账号绑定。用 Go 写一个一键导出工具核心工作就三件事拿到密钥、解密数据库、把消息按会话导出成可读格式。Go 的优势在于编译成单个二进制、跨平台、标准库自带 SQLite 驱动生态成熟适合做成「双击就能跑」的小工具。这篇面向的是有基本 Go 环境、想自己动手导出 PC 微信聊天记录的开发者也适合想拿这个当 Go 实战项目练手的人。需要先明确边界工具只能处理你自己账号在本机产生的数据密钥获取依赖进程内存读取不同微信版本偏移会变这是整个方案里最容易翻车的环节。2. 密钥从哪来PC 微信数据库加密机制与 Go 读取思路2.1 微信本地数据库的加密方式PC 微信把聊天记录存在WeChat Files\wxid\Msg\下的多个.db文件里常见的有MSG0.db到MSG4.db、MediaMSG.db、MicroMsg.db等。这些文件本质是 SQLite 数据库但页数据经过 AES 加密每个数据库文件有独立的密钥。加密结构大致是这样SQLite 文件头 16 字节被替换成自定义标识第一页保留部分明文用于校验从某个偏移开始按 4096 字节一页做 AES-CBC 或 AES-ECB 加密。密钥是 32 字节由微信登录后根据账号信息派生存在进程内存里。所以导出工具的第一步不是读文件是从运行中的微信进程内存里把密钥搜出来。常见做法是用系统 API 读取微信进程内存按特征码定位密钥所在区域。Windows 下用ReadProcessMemorymacOS 下用task_for_pid加mach_vm_read。这一步是整个工具里最依赖版本适配的部分微信每次大版本更新都可能改内存布局。2.2 用 Go 读取进程内存并定位密钥下面是一段 Windows 平台下读取微信进程内存、按特征搜索密钥的简化代码。实际项目中需要根据微信版本调整特征偏移这里给出可运行的最小骨架。package main import ( fmt syscall unsafe ) var ( kernel32 syscall.NewLazyDLL(kernel32.dll) procOpenProcess kernel32.NewProc(OpenProcess) procReadProcess kernel32.NewProc(ReadProcessMemory) procVirtualQuery kernel32.NewProc(VirtualQueryEx) ) const ( PROCESS_VM_READ 0x0010 PROCESS_QUERY 0x0400 MEM_COMMIT 0x1000 ) type memoryInfo struct { BaseAddress uintptr AllocationBase uintptr AllocationProtect uint32 RegionSize uintptr State uint32 Protect uint32 Type uint32 } // findWeChatPID 通过进程名匹配微信 PID实际可用 tasklist 或 WMI 替代 func findWeChatPID() (uint32, error) { // 简化真实实现遍历进程快照匹配 WeChat.exe return 0, fmt.Errorf(需要实现进程枚举) } // readRegion 读取指定进程一段内存 func readRegion(hProcess uintptr, addr uintptr, size uintptr) ([]byte, error) { buf : make([]byte, size) var read uintptr ret, _, err : procReadProcess.Call( hProcess, addr, uintptr(unsafe.Pointer(buf[0])), size, uintptr(unsafe.Pointer(read)), ) if ret 0 { return nil, err } return buf[:read], nil } // scanKey 在内存中按特征定位 32 字节密钥 // 真实场景密钥附近通常有 wxid 或固定字符串特征 func scanKey(hProcess uintptr, base uintptr, size uintptr) []byte { data, err : readRegion(hProcess, base, size) if err ! nil { return nil } // 示例特征密钥前通常有固定 4 字节标记需按版本调整 for i : 0; i36 len(data); i { if data[i] 0x00 data[i1] 0x00 { candidate : data[i4 : i36] if looksLikeKey(candidate) { return candidate } } } return nil } func looksLikeKey(b []byte) bool { // 简单校验非全零且熵足够 var zero int for _, v : range b { if v 0 { zero } } return zero 8 }逻辑说明OpenProcess拿到微信进程句柄VirtualQueryEx遍历所有可读内存区域ReadProcessMemory把每段内存读出来再按特征扫描。参数上PROCESS_VM_READ是必须的权限MEM_COMMIT过滤已提交区域避免读空洞。scanKey里的特征偏移是版本相关的微信 3.9.x 和 4.0.x 的偏移不一样需要自己用调试器确认。提示读取其他进程内存需要管理员权限且部分安全软件会拦截。这是正常现象不是代码写错了。2.3 密钥验证怎么确认搜到的是对的搜到候选密钥后不能直接用要先验证。验证方法是用这个密钥去解密数据库第一页看解密后的前 16 字节是不是标准 SQLite 文件头SQLite format 3\x00。如果是密钥正确如果不是继续搜下一个候选。func verifyKey(key []byte, dbPath string) bool { f, err : os.Open(dbPath) if err ! nil { return false } defer f.Close() header : make([]byte, 4096) f.Read(header) // 微信第一页前 16 字节是自定义头从偏移 16 开始解密 block, err : aesDecrypt(header[16:4096], key) if err ! nil { return false } return bytes.HasPrefix(block, []byte(SQLite format 3\x00)) }这个验证步骤是整个工具里最关键的后悔药。没有它后面所有导出都是白费功夫。实际调试时我一般会先把候选密钥打印出来手动跑一次验证确认逻辑通了再自动化。3. 解密与导出Go 操作 SQLite 的完整链路3.1 解密数据库文件到临时目录拿到正确密钥后把加密的.db文件整体解密成标准 SQLite 文件。微信的加密是按页做的每页 4096 字节第一页特殊处理后续页直接 AES 解密。const pageSize 4096 func decryptDB(srcPath, dstPath string, key []byte) error { src, err : os.Open(srcPath) if err ! nil { return err } defer src.Close() dst, err : os.Create(dstPath) if err ! nil { return err } defer dst.Close() buf : make([]byte, pageSize) first : true for { n, err : src.Read(buf) if n 0 { break } if first { // 第一页前 16 字节保留其余解密 plain, e : aesDecrypt(buf[16:pageSize], key) if e ! nil { return e } dst.Write([]byte(SQLite format 3\x00)) dst.Write(plain) first false } else { plain, e : aesDecrypt(buf[:n], key) if e ! nil { return e } dst.Write(plain) } if err ! nil { break } } return nil }参数说明pageSize固定 4096这是微信数据库的页大小不要改。aesDecrypt内部用 AES-CBCIV 通常是全零或固定值具体看版本。解密后的文件写到临时目录用os.CreateTemp更安全避免路径冲突。3.2 用 Go 查询解密后的消息表解密后的数据库就是标准 SQLite可以直接用database/sql加mattn/go-sqlite3或纯 Go 的modernc.org/sqlite查询。消息主要存在MSG表字段包括localId、TalkerId、Type、SubType、IsSender、CreateTime、StrContent、BytesExtra等。import ( database/sql _ modernc.org/sqlite ) type Message struct { LocalID int64 TalkerID int64 Type int IsSender int CreateTime int64 Content string } func queryMessages(dbPath string, limit int) ([]Message, error) { db, err : sql.Open(sqlite, dbPath) if err ! nil { return nil, err } defer db.Close() rows, err : db.Query( SELECT localId, TalkerId, Type, IsSender, CreateTime, StrContent FROM MSG WHERE Type 1 ORDER BY CreateTime DESC LIMIT ?, limit) if err ! nil { return nil, err } defer rows.Close() var msgs []Message for rows.Next() { var m Message if err : rows.Scan(m.LocalID, m.TalkerID, m.Type, m.IsSender, m.CreateTime, m.Content); err ! nil { return nil, err } msgs append(msgs, m) } return msgs, nil }逻辑说明Type 1是文本消息图片、语音、视频等类型需要额外处理BytesExtra字段。TalkerId是会话 ID需要跟Contact表关联才能拿到昵称。CreateTime是 Unix 时间戳导出时转成可读格式。3.3 关联联系人表拿到会话名光有TalkerId没法看得关联Contact表。Contact表里有UserName、NickName、Remark等字段。func getContactName(db *sql.DB, talkerID int64) string { var userName, nickName, remark string err : db.QueryRow( SELECT UserName, NickName, Remark FROM Contact WHERE ROWID ?, talkerID).Scan(userName, nickName, remark) if err ! nil { return fmt.Sprintf(unknown_%d, talkerID) } if remark ! { return remark } if nickName ! { return nickName } return userName }这里有个坑Contact表的ROWID和MSG表的TalkerId对应关系不是永远稳定微信重建数据库后可能变。稳妥做法是用UserName字符串匹配而不是依赖数字 ID。3.4 导出成 CSV 或 JSON最后把消息写成 CSV 或 JSON。CSV 方便用 Excel 看JSON 方便程序处理。func exportCSV(msgs []Message, db *sql.DB, outPath string) error { f, err : os.Create(outPath) if err ! nil { return err } defer f.Close() w : csv.NewWriter(f) w.Write([]string{时间, 会话, 发送方, 内容}) for _, m : range msgs { sender : 对方 if m.IsSender 1 { sender 我 } w.Write([]string{ time.Unix(m.CreateTime, 0).Format(2006-01-02 15:04:05), getContactName(db, m.TalkerID), sender, m.Content, }) } w.Flush() return w.Error() }参数说明IsSender 1表示自己发的0表示对方发的。时间格式化用 Go 的参考时间2006-01-02 15:04:05这是固定写法。导出路径建议用filepath.Join拼接避免 Windows 和 macOS 路径分隔符差异。4. 避坑与排查导出 PC 微信聊天记录最容易翻车的 5 个地方4.1 密钥搜不到或搜到错的现象scanKey返回 nil或者返回的密钥验证不通过。原因微信版本更新后内存布局变了特征偏移对不上或者微信进程刚启动密钥还没加载到内存。解决先用调试器手动确认当前版本的密钥偏移更新scanKey里的特征。确保微信已经登录并打开过至少一个聊天窗口密钥才会稳定存在。如果还是不行换用「先附加进程再触发一次消息收发」的方式让密钥进入活跃内存页。4.2 解密后 SQLite 打不开现象解密文件用 SQLite 工具打开报错或者表结构读出来是乱的。原因页大小不是 4096或者 AES 模式用错了CBC 当成 ECB。部分微信版本第一页的保留字节数不是 16。解决先用十六进制工具看加密文件第一页确认自定义头的长度。AES 模式可以通过解密后前几字节是否符合 SQLite 页结构来判断。如果 CBC 不对就换 ECBIV 也要跟着调。4.3 消息内容乱码或缺失现象StrContent读出来是乱码或者部分消息为空。原因微信 4.0 之后部分消息内容压缩存储StrContent只是占位真实内容在BytesExtra或CompressContent字段里。解决检查表结构里有没有CompressContent字段有的话需要先解压。压缩算法常见的是 zlib 或 lz4用 Go 的compress/zlib或github.com/pierrec/lz4处理。图片和语音消息的路径在BytesExtra里需要按 protobuf 格式解析。4.4 导出大量消息时内存爆掉现象导出几万条消息时程序卡死或 OOM。原因一次性SELECT *把全部消息读进内存。解决用分页查询每次读 1000 条处理完再读下一批。或者用rows.Next()流式处理不要先append到切片再统一写。写 CSV 时用bufio.Writer缓冲减少 IO 次数。4.5 跨平台路径和权限问题现象Windows 上能跑macOS 上读不到进程内存或者数据库文件被微信占用无法读取。原因macOS 需要task_for_pid权限且 SIP 会限制微信运行时数据库文件被锁定。解决macOS 下需要关闭 SIP 或者用codesign给工具签名。数据库文件被占用时先复制一份到临时目录再解密不要直接读原文件。Windows 下用CopyFileAPI 可以绕过部分锁定。5. 进阶把导出工具做成可维护的 Go 项目5.1 用接口隔离版本差异微信版本一变密钥偏移和表结构都可能变。把「找密钥」和「解析消息」抽象成接口每个版本一个实现主流程不变。type KeyFinder interface { Find(pid uint32) ([]byte, error) } type MessageParser interface { Parse(db *sql.DB, limit int) ([]Message, error) } type WeChatVersion struct { Name string Finder KeyFinder Parser MessageParser } var versions []WeChatVersion{ {Name: 3.9.x, Finder: Finder39{}, Parser: Parser39{}}, {Name: 4.0.x, Finder: Finder40{}, Parser: Parser40{}}, } func detectVersion(pid uint32) *WeChatVersion { // 读进程版本号或特征字符串匹配 return versions[0] }这样加新版本只需要加一个实现不用改主流程。我一般会在Finder里加日志把搜到的候选密钥和验证结果都打出来方便下次版本更新时快速定位。5.2 导出格式的可扩展设计不要写死 CSV。定义一个Exporter接口支持 CSV、JSON、HTML 多种输出。type Exporter interface { Export(msgs []Message, db *sql.DB, w io.Writer) error } type CSVExporter struct{} type JSONExporter struct{} func runExport(e Exporter, msgs []Message, db *sql.DB, path string) error { f, _ : os.Create(path) defer f.Close() return e.Export(msgs, db, f) }参数说明io.Writer让导出目标可以是文件、标准输出或网络流。JSON 导出时注意中文不要转义用json.Encoder加SetEscapeHTML(false)。5.3 验证导出结果的三个检查点导出完不要直接交付先做三个检查第一随机抽 10 条消息跟微信界面上手动核对内容和时间第二统计导出条数和数据库COUNT(*)是否一致第三检查时间范围是否覆盖你预期的区间。这三个检查能挡住 90% 的「看起来导出了但实际缺数据」的问题。5.4 一个具体技巧用 SQL 直接验证解密结果不想写代码验证时可以用命令行 SQLite 工具直接查解密后的文件。sqlite3 /tmp/decrypted.db SELECT COUNT(*) FROM MSG; sqlite3 /tmp/decrypted.db SELECT StrContent FROM MSG WHERE Type1 LIMIT 5;如果这两条命令能正常返回说明解密和表结构都没问题。如果报no such table说明解密后的文件不是完整 SQLite回去检查页大小和 AES 模式。这个技巧在调试阶段比写 Go 代码快得多我一般先用它确认解密逻辑通了再回去完善 Go 代码。最后说个血泪经验每次微信更新后先别急着改代码用调试器手动跑一遍密钥搜索流程确认偏移变了多少再动代码。盲目改参数只会让问题更乱。希望帮到你。本文还有配套的精品资源点击获取
返回列表