├── go.mod ├── go.sum ├── README.md ├── main.go └── util ├── file_util.go └── wechat_util.go /go.mod: -------------------------------------------------------------------------------- 1 | module WeChatMsgDump 2 | 3 | go 1.22 4 | 5 | require ( 6 | golang.org/x/crypto v0.32.0 7 | golang.org/x/sys v0.29.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 2 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 3 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 4 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## WeChatMsgDump 2 | 3 | WeChatMsgDump 是一个用 Go 编写的动态获取微信数据库密钥并解密数据库文件的小工具。 4 | 5 | ### 基本功能 6 | 7 | flag 均为可选。 8 | 9 | ``` 10 | Usage of WeChatMsgDump.exe: 11 | -dbfile 12 | Put WeChat Files db file to tmp dir 13 | -dumpKey 14 | only dump key 15 | -key string 16 | wx sqlite key 17 | -wxdir string 18 | WeChat Files dir, like: C:\WeChat Files\wxid_xxxxxxxxxxxx 19 | ``` 20 | 21 | ### 使用例子 22 | 23 | ![WindowsTerminal_3mcr3LN0r5](https://chevereto.1i6w31fen9.top/images/2025/01/15/WindowsTerminal_3mcr3LN0r5.png) 24 | 25 | ![7zFM_KvJsT6SzQa](https://chevereto.1i6w31fen9.top/images/2025/01/15/7zFM_KvJsT6SzQa.png) 26 | 27 | 可将 db.zip 解压,并使用相关数据库管理工具进行打开分析。例如 Firfox 中的 SQLite Manager 插件。 28 | 29 | ![firefox_JYt4zDMZX9](https://chevereto.1i6w31fen9.top/images/2025/01/15/firefox_JYt4zDMZX9.png) 30 | 31 | ### 数据库相关表信息 32 | 33 | MicroMsg.db 保存了一些联系人信息。 34 | 35 | 例如执行如下 SQL 语句即可获得联系人的微信号,备注,微信名的信息。 36 | 37 | ```SQL 38 | select UserName,Alias,Remark,NickName from Contact 39 | ``` 40 | 41 | MSG*.db 主要保存了聊天记录信息。其中 `IsSender=1` 字段代表为本人发送消息。例如查询某个联系人的聊天记录: 42 | 43 | ```SQL 44 | SELECT * from MSG where StrTalker = f'{MicroMsg.Contact.UserName}' ORDER BY CreateTime DESC limit 10 45 | ``` 46 | 47 | 其它表介绍:https://github.com/LC044/WeChatMsg/blob/master/doc/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BB%8B%E7%BB%8D.md 48 | 49 | ### 参考 50 | 51 | https://github.com/LC044/WeChatMsg/ 52 | 53 | https://github.com/SpenserCai/GoWxDump/ 54 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "WeChatMsgDump/util" 5 | "flag" 6 | "golang.org/x/sys/windows" 7 | "io" 8 | "log" 9 | "os" 10 | "unsafe" 11 | ) 12 | 13 | func init() { 14 | file := "./info.txt" 15 | logFile, _ := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 16 | multiWriter := io.MultiWriter(os.Stdout, logFile) 17 | log.SetOutput(multiWriter) 18 | return 19 | } 20 | 21 | func main() { 22 | dumpKey := flag.Bool("dumpKey", false, "only dump key") 23 | sqliteKey := flag.String("key", "", "wx sqlite key") 24 | wxdir := flag.String("wxdir", "", "WeChat Files dir, like: C:\\WeChat Files\\wxid_xxxxxxxxxxxx") 25 | dbFile := flag.Bool("dbfile", false, "Put WeChat Files db file to tmp dir") 26 | flag.Parse() 27 | 28 | // 离线 29 | if *dbFile && *sqliteKey != "" { 30 | err := util.DecryptDb(*sqliteKey) 31 | if err != nil { 32 | log.Fatalf("DecryptDb error: %v", err) 33 | } 34 | return 35 | } 36 | 37 | var wechatProcessHandle windows.Handle 38 | 39 | if *sqliteKey == "" { 40 | process, err := util.GetWeChatProcess() 41 | if err != nil { 42 | log.Fatalf("GetWeChatProcess error: %v", err) 43 | } 44 | 45 | wechatProcessHandle, err = windows.OpenProcess(windows.PROCESS_ALL_ACCESS, false, process.ProcessID) 46 | if err != nil { 47 | log.Fatalf("OpenProcess error: %v", err) 48 | } 49 | 50 | module, err := util.GetWeChatWinModule(process) 51 | if err != nil { 52 | log.Fatalf("GetWeChatWinModule error: %v", err) 53 | } 54 | 55 | version, err := util.GetVersion(module) 56 | if err != nil { 57 | log.Printf("GetVersion error: %v", err) 58 | } else { 59 | log.Printf("WeChat Version: %v", version) 60 | } 61 | 62 | var moduleInfo windows.ModuleInfo 63 | err = windows.GetModuleInformation(wechatProcessHandle, module.ModuleHandle, &moduleInfo, uint32(unsafe.Sizeof(moduleInfo))) 64 | if err != nil { 65 | log.Fatalf("GetModuleInformation error: %v", err) 66 | } 67 | 68 | bits, err := util.GetPEBits(wechatProcessHandle, moduleInfo.BaseOfDll) 69 | if err != nil { 70 | log.Fatalf("GetPEBits error: %v", err) 71 | } 72 | 73 | keyBytes := []byte("-----BEGIN PUBLIC KEY-----\n") 74 | publicKeyList, err := util.PatternScanAll(wechatProcessHandle, keyBytes) 75 | if err != nil { 76 | log.Fatalf("PatternScanAll error: %v", err) 77 | } 78 | if len(publicKeyList) == 0 { 79 | log.Fatalf("Failed to find PUBLIC KEY") 80 | } 81 | 82 | keyAddr, err := util.GetKeyAddr(wechatProcessHandle, moduleInfo.BaseOfDll, moduleInfo.SizeOfImage, bits, publicKeyList) 83 | if keyAddr == nil || err != nil { 84 | log.Fatalf("GetKeyAddr error: %v", err) 85 | } 86 | 87 | *sqliteKey = util.ReadKey(wechatProcessHandle, keyAddr, bits) 88 | if *sqliteKey == "" { 89 | log.Fatalf("Failed to find key") 90 | } else { 91 | log.Printf("sqlite key:%s\n", *sqliteKey) 92 | } 93 | } 94 | 95 | if *dumpKey { 96 | return 97 | } 98 | 99 | if *wxdir == "" && wechatProcessHandle != 0 { 100 | wxid := util.GetInfoWxid(wechatProcessHandle) 101 | log.Printf("WeChat User: %s", wxid) 102 | *wxdir = util.GetInfoFilePath(wxid) 103 | log.Printf("WeChat Info Dir: %s", *wxdir) 104 | } 105 | 106 | if *wxdir != "" && *sqliteKey != "" { 107 | util.DecryptCmd(*wxdir, *sqliteKey) 108 | } else { 109 | flag.PrintDefaults() 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /util/file_util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "crypto/aes" 7 | "crypto/cipher" 8 | "crypto/hmac" 9 | "crypto/sha1" 10 | "encoding/hex" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "log" 15 | "os" 16 | "path/filepath" 17 | "strings" 18 | 19 | "golang.org/x/crypto/pbkdf2" 20 | ) 21 | 22 | const ( 23 | SQLITE_FILE_HEADER = "SQLite format 3\x00" 24 | KEY_SIZE = 32 25 | DEFAULT_PAGESIZE = 4096 26 | DEFAULT_ITER = 64000 27 | ) 28 | 29 | var CurrentPath = func() string { 30 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 31 | if err != nil { 32 | return "" 33 | } 34 | return dir 35 | }() 36 | 37 | func DecryptCmd(wxid, sqliteKey string) { 38 | var err error 39 | err = CopyMsgDb(filepath.Join(wxid, "Msg", "Multi")) 40 | if err != nil { 41 | log.Fatalf("CopyMsgDb error: %v", err) 42 | } 43 | err = CopyMsgDb(filepath.Join(wxid, "Msg")) 44 | if err != nil { 45 | log.Fatalf("CopyMicroMsgDb error: %v", err) 46 | } 47 | // 解密tmp目录下的所有.db文件,解密后的文件放在 decrypted 目录下 48 | err = DecryptDb(sqliteKey) 49 | if err != nil { 50 | log.Fatalf("DecryptDb error: %v", err) 51 | } 52 | 53 | // 清理缓存目录 54 | err = os.RemoveAll(CurrentPath + "\\tmp") 55 | if err != nil { 56 | log.Printf("RemoveAll error: %v\n", err) 57 | } 58 | err = CompressFolder(CurrentPath+"\\decrypted", "db.zip") 59 | if err != nil { 60 | log.Printf("CompressFolder error: %v\n", err) 61 | } else { 62 | log.Println("Decrypt success: Decrypted files are in db.zip") 63 | } 64 | err = os.RemoveAll(CurrentPath + "\\decrypted") 65 | if err != nil { 66 | log.Printf("RemoveAll error: %v\n", err) 67 | } 68 | } 69 | 70 | func CopyMsgDb(dataDir string) error { 71 | // 判断目录是否存在 72 | _, err := os.Stat(dataDir) 73 | if err != nil { 74 | return err 75 | } 76 | // 判断运行目录是否存在tmp目录没有则创建 77 | _, err = os.Stat(CurrentPath + "\\tmp") 78 | if err != nil { 79 | err = os.Mkdir(CurrentPath+"\\tmp", os.ModePerm) 80 | if err != nil { 81 | return err 82 | } 83 | } 84 | // 正则匹配,将所有MSG数字.db文件拷贝到tmp目录,不扫描子目录 85 | err = filepath.Walk(dataDir, func(path string, info os.FileInfo, err error) error { 86 | if err != nil { 87 | return err 88 | } 89 | if ok, _ := filepath.Match("MSG*.db", info.Name()); ok { 90 | err = CopyFile(path, CurrentPath+"\\tmp\\"+info.Name()) 91 | if err != nil { 92 | return err 93 | } 94 | } 95 | // 复制MicroMsg.db到tmp目录 96 | if ok, _ := filepath.Match("MicroMsg.db", info.Name()); ok { 97 | err = CopyFile(path, CurrentPath+"\\tmp\\"+info.Name()) 98 | if err != nil { 99 | return err 100 | } 101 | } 102 | // 语音消息 103 | if ok, _ := filepath.Match("MediaMSG*.db", info.Name()); ok { 104 | err = CopyFile(path, CurrentPath+"\\tmp\\"+info.Name()) 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | // 朋友圈数据 110 | if ok, _ := filepath.Match("Sns.db", info.Name()); ok { 111 | err = CopyFile(path, CurrentPath+"\\tmp\\"+info.Name()) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | return nil 117 | }) 118 | if err != nil { 119 | return err 120 | } 121 | // 如果不存在decrypted目录则创建 122 | _, err = os.Stat(CurrentPath + "\\decrypted") 123 | if err != nil { 124 | err = os.Mkdir(CurrentPath+"\\decrypted", os.ModePerm) 125 | if err != nil { 126 | return err 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func DecryptDb(key string) error { 134 | // 判断tmp目录是否存在 135 | _, err := os.Stat(CurrentPath + "\\tmp") 136 | if err != nil { 137 | return fmt.Errorf("please put db files in tmp dir") 138 | } 139 | // 判断decrypted目录是否存在 140 | _, err = os.Stat(CurrentPath + "\\decrypted") 141 | if err != nil { 142 | return err 143 | } 144 | // 正则匹配,将所有MSG数字.db文件解密到decrypted目录,不扫描子目录 145 | err = filepath.Walk(CurrentPath+"\\tmp", func(path string, info os.FileInfo, err error) error { 146 | if err != nil { 147 | return err 148 | } 149 | if ok, _ := filepath.Match("*.db", info.Name()); ok { 150 | err = Decrypt(key, path, CurrentPath+"\\decrypted\\"+info.Name()) 151 | if err != nil { 152 | return err 153 | } 154 | } 155 | return nil 156 | }) 157 | if err != nil { 158 | return err 159 | } 160 | return nil 161 | } 162 | 163 | func Decrypt(key string, filePath string, decryptedPath string) error { 164 | password, err := hex.DecodeString(strings.Replace(key, " ", "", -1)) 165 | if err != nil { 166 | return err 167 | } 168 | file, err := os.Open(filePath) 169 | if err != nil { 170 | return err 171 | } 172 | defer file.Close() 173 | blist, err := ioutil.ReadAll(file) 174 | if err != nil { 175 | return err 176 | } 177 | salt := blist[:16] 178 | byteKey := pbkdf2.Key(password, salt, DEFAULT_ITER, KEY_SIZE, sha1.New) 179 | first := blist[16:DEFAULT_PAGESIZE] 180 | mac_salt := make([]byte, 16) 181 | for i := 0; i < 16; i++ { 182 | mac_salt[i] = salt[i] ^ 58 183 | } 184 | mac_key := pbkdf2.Key(byteKey, mac_salt, 2, KEY_SIZE, sha1.New) 185 | hash_mac := hmac.New(sha1.New, mac_key) 186 | hash_mac.Write(first[:len(first)-32]) 187 | hash_mac.Write([]byte{1, 0, 0, 0}) 188 | if !bytes.Equal(hash_mac.Sum(nil), first[len(first)-32:len(first)-12]) { 189 | log.Fatalf("sqlite Key Wrong.") 190 | } 191 | 192 | // 将python代码:blist = [blist[i:i + DEFAULT_PAGESIZE] for i in range(DEFAULT_PAGESIZE, len(blist), DEFAULT_PAGESIZE)] 转成go语言 193 | newblist := make([][]byte, 0) 194 | for i := DEFAULT_PAGESIZE; i < len(blist); i += DEFAULT_PAGESIZE { 195 | newblist = append(newblist, blist[i:i+DEFAULT_PAGESIZE]) 196 | } 197 | 198 | // 将文件写入decryptePath 199 | deFile, err := os.OpenFile(decryptedPath, os.O_CREATE|os.O_WRONLY, 0666) 200 | if err != nil { 201 | return err 202 | } 203 | defer deFile.Close() 204 | deFile.Write([]byte(SQLITE_FILE_HEADER)) 205 | t, err := aes.NewCipher(byteKey) 206 | if err != nil { 207 | return err 208 | } 209 | iv := first[len(first)-48 : len(first)-32] 210 | blockMode := cipher.NewCBCDecrypter(t, iv) 211 | decrypted := make([]byte, len(first)-48) 212 | blockMode.CryptBlocks(decrypted, first[:len(first)-48]) 213 | deFile.Write(decrypted) 214 | deFile.Write(first[len(first)-48:]) 215 | 216 | for _, i := range newblist { 217 | t, err := aes.NewCipher(byteKey) 218 | if err != nil { 219 | return err 220 | } 221 | blockMode := cipher.NewCBCDecrypter(t, i[len(i)-48:len(i)-32]) 222 | decrypted := make([]byte, len(i)-48) 223 | blockMode.CryptBlocks(decrypted, i[:len(i)-48]) 224 | deFile.Write(decrypted) 225 | deFile.Write(i[len(i)-48:]) 226 | } 227 | return nil 228 | } 229 | 230 | func CopyFile(src, dst string) error { 231 | // 判断源文件是否存在 232 | _, err := os.Stat(src) 233 | if err != nil { 234 | return err 235 | } 236 | // 读取源文件 237 | srcFile, err := os.Open(src) 238 | if err != nil { 239 | return err 240 | } 241 | defer srcFile.Close() 242 | // 创建目标文件 243 | dstFile, err := os.Create(dst) 244 | if err != nil { 245 | return err 246 | } 247 | defer dstFile.Close() 248 | // 拷贝文件 249 | _, err = io.Copy(dstFile, srcFile) 250 | if err != nil { 251 | return err 252 | } 253 | return nil 254 | } 255 | 256 | func CompressFolder(source, target string) error { 257 | // 创建目标 ZIP 文件 258 | zipFile, err := os.Create(target) 259 | if err != nil { 260 | return err 261 | } 262 | defer zipFile.Close() 263 | 264 | // 创建 ZIP 写入器 265 | zipWriter := zip.NewWriter(zipFile) 266 | defer zipWriter.Close() 267 | 268 | // 遍历文件夹 269 | return filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 270 | if err != nil { 271 | return err 272 | } 273 | 274 | // 创建 ZIP 文件头 275 | header, err := zip.FileInfoHeader(info) 276 | if err != nil { 277 | return err 278 | } 279 | 280 | // 设置文件头中的文件名 281 | header.Name, err = filepath.Rel(source, path) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | // 如果是目录,需要在 ZIP 文件中创建一个目录条目 287 | if info.IsDir() { 288 | header.Name += "/" 289 | } else { 290 | // 设置文件压缩方法 291 | header.Method = zip.Deflate 292 | } 293 | 294 | // 写入文件头 295 | writer, err := zipWriter.CreateHeader(header) 296 | if err != nil { 297 | return err 298 | } 299 | 300 | // 如果是文件,将文件内容写入 ZIP 文件 301 | if !info.IsDir() { 302 | file, err := os.Open(path) 303 | if err != nil { 304 | return err 305 | } 306 | defer file.Close() 307 | 308 | _, err = io.Copy(writer, file) 309 | if err != nil { 310 | return err 311 | } 312 | } 313 | 314 | return nil 315 | }) 316 | } 317 | -------------------------------------------------------------------------------- /util/wechat_util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "golang.org/x/sys/windows" 8 | "golang.org/x/sys/windows/registry" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | "syscall" 14 | "unsafe" 15 | ) 16 | 17 | type SystemInfo struct { 18 | dwPageSize uint32 19 | lpMinimumApplicationAddress uintptr 20 | lpMaximumApplicationAddress uintptr 21 | dwActiveProcessorMask uintptr 22 | dwNumberOfProcessors uint32 23 | dwProcessorType uint32 24 | dwAllocationGranularity uint32 25 | wProcessorLevel uint16 26 | wProcessorRevision uint16 27 | } 28 | 29 | var ( 30 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 31 | GetSystemInfo = kernel32.NewProc("GetSystemInfo") 32 | ) 33 | 34 | func GetWeChatProcess() (windows.ProcessEntry32, error) { 35 | var process windows.ProcessEntry32 36 | process.Size = uint32(unsafe.Sizeof(process)) 37 | snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) 38 | if err != nil { 39 | return process, err 40 | } 41 | defer windows.CloseHandle(snapshot) 42 | for { 43 | err = windows.Process32Next(snapshot, &process) 44 | if err != nil { 45 | return process, err 46 | } 47 | if windows.UTF16ToString(process.ExeFile[:]) == "WeChat.exe" { 48 | return process, nil 49 | } 50 | } 51 | } 52 | 53 | func GetWeChatWinModule(process windows.ProcessEntry32) (windows.ModuleEntry32, error) { 54 | var module windows.ModuleEntry32 55 | module.Size = uint32(unsafe.Sizeof(module)) 56 | snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE, process.ProcessID) 57 | if err != nil { 58 | return module, err 59 | } 60 | defer windows.CloseHandle(snapshot) 61 | for { 62 | err = windows.Module32Next(snapshot, &module) 63 | if err != nil { 64 | return module, err 65 | } 66 | if windows.UTF16ToString(module.Module[:]) == "WeChatWin.dll" { 67 | return module, nil 68 | } 69 | } 70 | } 71 | 72 | func GetPEBits(processHandle windows.Handle, dllBase uintptr) (int, error) { 73 | // 读取 e_lfanew 字段(PE 文件头的偏移量) 74 | var eLfanew uint32 75 | err := windows.ReadProcessMemory(processHandle, dllBase+60, (*byte)(unsafe.Pointer(&eLfanew)), unsafe.Sizeof(eLfanew), nil) 76 | if err != nil { 77 | return 0, fmt.Errorf("read e_lfanew error: %v", err) 78 | } 79 | 80 | // 计算 SizeOfOptionalHeader 的地址 81 | address := dllBase + uintptr(eLfanew) + 4 + 16 82 | 83 | // 读取 SizeOfOptionalHeader 字段 84 | var sizeOfOptionalHeader uint16 85 | err = windows.ReadProcessMemory(processHandle, address, (*byte)(unsafe.Pointer(&sizeOfOptionalHeader)), unsafe.Sizeof(sizeOfOptionalHeader), nil) 86 | if err != nil { 87 | return 0, fmt.Errorf("read SizeOfOptionalHeader error: %v", err) 88 | } 89 | 90 | if sizeOfOptionalHeader == 0xF0 { 91 | return 64, nil 92 | } 93 | return 32, nil 94 | } 95 | 96 | func PatternScanAll(processHandle windows.Handle, pattern []byte) ([]uintptr, error) { 97 | var addresses []uintptr 98 | 99 | var systemInfo SystemInfo 100 | GetSystemInfo.Call(uintptr(unsafe.Pointer(&systemInfo))) 101 | 102 | // 遍历内存区域 103 | var baseAddress uintptr 104 | for baseAddress < systemInfo.lpMaximumApplicationAddress { 105 | var memoryBasicInfo windows.MemoryBasicInformation 106 | err := windows.VirtualQueryEx(processHandle, baseAddress, &memoryBasicInfo, unsafe.Sizeof(memoryBasicInfo)) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | if memoryBasicInfo.Protect&windows.PAGE_READONLY != 0 || memoryBasicInfo.Protect&windows.PAGE_READWRITE != 0 { 112 | buffer := make([]byte, memoryBasicInfo.RegionSize) 113 | var bytesRead uintptr 114 | err := windows.ReadProcessMemory(processHandle, baseAddress, &buffer[0], uintptr(len(buffer)), &bytesRead) 115 | if err != nil { 116 | baseAddress += memoryBasicInfo.RegionSize 117 | continue 118 | } 119 | 120 | // 在内存区域中搜索模式 121 | for offset := 0; offset <= len(buffer)-len(pattern); offset++ { 122 | if bytes.Equal(buffer[offset:offset+len(pattern)], pattern) { 123 | addresses = append(addresses, baseAddress+uintptr(offset)) 124 | } 125 | } 126 | } 127 | 128 | baseAddress += memoryBasicInfo.RegionSize 129 | } 130 | 131 | return addresses, nil 132 | } 133 | 134 | func GetKeyAddr(processHandle windows.Handle, dllBase uintptr, sizeOfImage uint32, bits int, publicKeyList []uintptr) ([]uintptr, error) { 135 | var keyAddr []uintptr 136 | 137 | buffer := make([]byte, sizeOfImage) 138 | err := windows.ReadProcessMemory(processHandle, dllBase, &buffer[0], uintptr(sizeOfImage), nil) 139 | if err != nil { 140 | return nil, fmt.Errorf("read WeChatWin.dll error: %v", err) 141 | } 142 | 143 | byteLen := 4 144 | if bits == 64 { 145 | byteLen = 8 146 | } 147 | 148 | for _, publicKeyAddr := range publicKeyList { 149 | keyBytes := make([]byte, byteLen) 150 | for i := 0; i < byteLen; i++ { 151 | keyBytes[i] = byte(publicKeyAddr >> (i * 8)) 152 | } 153 | 154 | offsets := SearchMemory(buffer, keyBytes) 155 | if len(offsets) == 0 { 156 | continue 157 | } 158 | 159 | for _, offset := range offsets { 160 | keyAddr = append(keyAddr, dllBase+offset) 161 | } 162 | } 163 | 164 | if len(keyAddr) == 0 { 165 | return nil, nil 166 | } 167 | 168 | return keyAddr, nil 169 | } 170 | 171 | func SearchMemory(parent []byte, child []byte) []uintptr { 172 | var offsets []uintptr 173 | index := 0 174 | 175 | for { 176 | foundIndex := bytes.Index(parent[index:], child) 177 | if foundIndex == -1 { 178 | break 179 | } 180 | 181 | // 计算实际偏移量 182 | actualOffset := index + foundIndex 183 | offsets = append(offsets, uintptr(actualOffset)) 184 | 185 | // 更新 index,继续查找 186 | index = actualOffset + 1 187 | } 188 | 189 | return offsets 190 | } 191 | 192 | func ReadKey(processHandle windows.Handle, keyAddr []uintptr, bits int) string { 193 | keyLenOffset := uintptr(0x8c) 194 | if bits == 64 { 195 | keyLenOffset = 0xd0 196 | } 197 | 198 | for _, addr := range keyAddr { 199 | keyLen := make([]byte, 1) 200 | var bytesRead uintptr 201 | err := windows.ReadProcessMemory(processHandle, addr-keyLenOffset, &keyLen[0], 1, &bytesRead) 202 | if err != nil { 203 | continue 204 | } 205 | if keyLen[0] == 0 { 206 | continue 207 | } 208 | var keyAddrBuf []byte 209 | if bits == 32 { 210 | keyAddrBuf = make([]byte, 4) 211 | } else { 212 | keyAddrBuf = make([]byte, 8) 213 | } 214 | offset := uintptr(0x90) 215 | if bits == 64 { 216 | offset = 0xd8 217 | } 218 | err = windows.ReadProcessMemory(processHandle, addr-offset, &keyAddrBuf[0], uintptr(len(keyAddrBuf)), &bytesRead) 219 | if err != nil { 220 | continue 221 | } 222 | 223 | // 将 keyAddrBuf 转换为 uintptr 224 | var keyPtr uintptr 225 | if bits == 32 { 226 | keyPtr = uintptr(keyAddrBuf[0]) | uintptr(keyAddrBuf[1])<<8 | uintptr(keyAddrBuf[2])<<16 | uintptr(keyAddrBuf[3])<<24 227 | } else { 228 | keyPtr = uintptr(keyAddrBuf[0]) | uintptr(keyAddrBuf[1])<<8 | uintptr(keyAddrBuf[2])<<16 | uintptr(keyAddrBuf[3])<<24 | 229 | uintptr(keyAddrBuf[4])<<32 | uintptr(keyAddrBuf[5])<<40 | uintptr(keyAddrBuf[6])<<48 | uintptr(keyAddrBuf[7])<<56 230 | } 231 | 232 | key := make([]byte, keyLen[0]) 233 | err = windows.ReadProcessMemory(processHandle, keyPtr, &key[0], uintptr(len(key)), &bytesRead) 234 | if err != nil { 235 | continue 236 | } 237 | 238 | keyHex := hex.EncodeToString(key) 239 | if CheckKey(keyHex) { 240 | return keyHex 241 | } 242 | } 243 | 244 | return "" 245 | } 246 | 247 | func CheckKey(key string) bool { 248 | if key == "" || len(key) != 64 { 249 | return false 250 | } 251 | return true 252 | } 253 | 254 | func GetInfoFilePath(wxid string) string { 255 | if wxid == "" { 256 | return "" 257 | } 258 | 259 | var wDir string 260 | isWDir := false 261 | 262 | // 尝试从注册表获取 WeChat 文件保存路径 263 | key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Tencent\WeChat`, registry.READ) 264 | if err == nil { 265 | value, _, err := key.GetStringValue("FileSavePath") 266 | if err == nil { 267 | wDir = value 268 | isWDir = true 269 | } 270 | key.Close() 271 | } 272 | 273 | // 如果注册表没有找到路径,尝试从配置文件读取 274 | if !isWDir { 275 | userProfile := os.Getenv("USERPROFILE") 276 | path3ebffe94 := filepath.Join(userProfile, "AppData", "Roaming", "Tencent", "WeChat", "All Users", "config", "3ebffe94.ini") 277 | content, err := os.ReadFile(path3ebffe94) 278 | if err == nil { 279 | wDir = string(content) 280 | isWDir = true 281 | } else { 282 | wDir = "MyDocument:" 283 | } 284 | } 285 | 286 | // 如果路径是 "MyDocument:",尝试从注册表获取文档路径 287 | if wDir == "MyDocument:" { 288 | key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders`, registry.READ) 289 | if err == nil { 290 | documentsPath, _, err := key.GetStringValue("Personal") 291 | if err == nil { 292 | documentsPaths := strings.Split(documentsPath, "\\") 293 | if strings.Contains(documentsPaths[0], "%") { 294 | envVar := strings.Trim(documentsPaths[0], "%") 295 | envValue := os.Getenv(envVar) 296 | wDir = filepath.Join(envValue, filepath.Join(documentsPaths[1:]...)) 297 | } else { 298 | wDir = documentsPath 299 | } 300 | } 301 | key.Close() 302 | } else { 303 | profile := os.Getenv("USERPROFILE") 304 | wDir = filepath.Join(profile, "Documents") 305 | } 306 | } 307 | 308 | msgDir := filepath.Join(wDir, "WeChat Files") 309 | filePath := filepath.Join(msgDir, wxid) 310 | 311 | if _, err := os.Stat(filePath); err == nil { 312 | return filePath 313 | } 314 | 315 | return "" 316 | } 317 | 318 | func ScanPatternPage(handle windows.Handle, address uintptr, pattern []byte, returnMultiple bool, checkMemoryProtection bool) (uintptr, interface{}, error) { 319 | var mbi windows.MemoryBasicInformation 320 | err := windows.VirtualQueryEx(handle, address, &mbi, unsafe.Sizeof(mbi)) 321 | if err != nil { 322 | return 0, nil, err 323 | } 324 | 325 | nextRegion := mbi.BaseAddress + mbi.RegionSize 326 | 327 | var havePermissionToScanMemoryPage bool 328 | if checkMemoryProtection { 329 | allowedProtections := []uint32{ 330 | windows.PAGE_EXECUTE, 331 | windows.PAGE_EXECUTE_READ, 332 | windows.PAGE_EXECUTE_READWRITE, 333 | windows.PAGE_READWRITE, 334 | windows.PAGE_READONLY, 335 | } 336 | 337 | havePermissionToScanMemoryPage = false 338 | for _, protection := range allowedProtections { 339 | if mbi.Protect == protection { 340 | havePermissionToScanMemoryPage = true 341 | break 342 | } 343 | } 344 | } else { 345 | havePermissionToScanMemoryPage = true 346 | } 347 | 348 | if mbi.State != windows.MEM_COMMIT || !havePermissionToScanMemoryPage { 349 | return nextRegion, nil, nil 350 | } 351 | 352 | pageBytes := make([]byte, mbi.RegionSize-(address-mbi.BaseAddress)) 353 | var bytesRead uintptr 354 | err = windows.ReadProcessMemory(handle, address, &pageBytes[0], uintptr(len(pageBytes)), &bytesRead) 355 | if err != nil { 356 | return nextRegion, nil, err 357 | } 358 | 359 | var found interface{} 360 | if !returnMultiple { 361 | // 查找第一个匹配项 362 | re := regexp.MustCompile(string(pattern)) 363 | match := re.FindIndex(pageBytes) 364 | if match != nil { 365 | found = address + uintptr(match[0]) 366 | } 367 | } else { 368 | // 查找所有匹配项 369 | var foundAddresses []uintptr 370 | re := regexp.MustCompile(string(pattern)) 371 | matches := re.FindAllIndex(pageBytes, -1) 372 | for _, match := range matches { 373 | foundAddresses = append(foundAddresses, address+uintptr(match[0])) 374 | } 375 | found = foundAddresses 376 | } 377 | 378 | return nextRegion, found, nil 379 | } 380 | 381 | func SearchMemoryForWxid(handle windows.Handle, pattern []byte, returnMultiple bool, findNum int) ([]uintptr, error) { 382 | var nextRegion uintptr 383 | var found []uintptr 384 | 385 | // 设置用户空间的内存限制 386 | userSpaceLimit := uintptr(0x7FFFFFFF0000) 387 | if uintptr(unsafe.Sizeof(uintptr(0))) <= 4 { 388 | userSpaceLimit = 0x7fff0000 389 | } 390 | 391 | for nextRegion < userSpaceLimit { 392 | nextRegion1, pageFound, err := ScanPatternPage(handle, nextRegion, pattern, returnMultiple, true) 393 | nextRegion = nextRegion1 394 | if err != nil { 395 | return nil, fmt.Errorf("ScanPatternPage error: %v", err) 396 | } 397 | 398 | if !returnMultiple && pageFound != nil { 399 | return []uintptr{pageFound.(uintptr)}, nil 400 | } 401 | 402 | if pageFound != nil { 403 | switch v := pageFound.(type) { 404 | case []uintptr: 405 | found = append(found, v...) 406 | case uintptr: 407 | found = append(found, v) 408 | } 409 | } 410 | 411 | if len(found) > findNum { 412 | break 413 | } 414 | } 415 | 416 | return found, nil 417 | } 418 | 419 | func GetInfoWxid(hProcess windows.Handle) string { 420 | addrs, err := SearchMemoryForWxid(hProcess, []byte(`\\Msg\\FTSContact`), true, 100) 421 | if err != nil { 422 | return "" 423 | } 424 | 425 | var wxids []string 426 | for _, addr := range addrs { 427 | array := make([]byte, 80) 428 | err := windows.ReadProcessMemory(hProcess, addr-30, &array[0], uintptr(len(array)), nil) 429 | if err != nil { 430 | return "" 431 | } 432 | 433 | array = bytes.Split(array, []byte("\\Msg"))[0] 434 | parts := bytes.Split(array, []byte("\\")) 435 | if len(parts) > 0 { 436 | wxids = append(wxids, string(parts[len(parts)-1])) 437 | } 438 | } 439 | 440 | if len(wxids) == 0 { 441 | return "" 442 | } 443 | 444 | counts := make(map[string]int) 445 | for _, wxid := range wxids { 446 | counts[wxid]++ 447 | } 448 | 449 | // 查找出现最多的 wxid 450 | var maxWxid string 451 | var maxCount int 452 | for wxid, count := range counts { 453 | if count > maxCount { 454 | maxWxid = wxid 455 | maxCount = count 456 | } 457 | } 458 | 459 | return maxWxid 460 | } 461 | 462 | func GetVersion(module windows.ModuleEntry32) (string, error) { 463 | image, imgErr := windows.LoadLibraryEx(windows.UTF16ToString(module.ExePath[:]), 0, windows.LOAD_LIBRARY_AS_DATAFILE) 464 | if imgErr != nil { 465 | return "", fmt.Errorf("LoadLibraryEx error: %v", imgErr) 466 | } 467 | resInfo, infoErr := windows.FindResource(image, windows.ResourceID(1), windows.RT_VERSION) 468 | if infoErr != nil { 469 | return "", fmt.Errorf("FindResource error: %v", infoErr) 470 | } 471 | resData, dataErr := windows.LoadResourceData(image, resInfo) 472 | if dataErr != nil { 473 | return "", fmt.Errorf("LoadResourceData error: %v", dataErr) 474 | } 475 | var info *windows.VS_FIXEDFILEINFO 476 | size := uint32(unsafe.Sizeof(*info)) 477 | err := windows.VerQueryValue(unsafe.Pointer(&resData[0]), `\`, unsafe.Pointer(&info), &size) 478 | if err != nil { 479 | return "", fmt.Errorf("VerQueryValue error: %v", err) 480 | } 481 | // 从低位到高位,分别为主版本号、次版本号、修订号、编译号 482 | version := fmt.Sprintf("%d.%d.%d.%d", info.FileVersionMS>>16, info.FileVersionMS&0xffff, info.FileVersionLS>>16, info.FileVersionLS&0xffff) 483 | return version, nil 484 | } 485 | --------------------------------------------------------------------------------