This commit is contained in:
Shen Junzheng
2025-03-21 21:45:08 +08:00
parent 78cce92ce3
commit 80c7e67106
86 changed files with 7061 additions and 2316 deletions

View File

@@ -1,415 +0,0 @@
package wechat
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha1"
"crypto/sha512"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
"os"
"golang.org/x/crypto/pbkdf2"
)
// Constants for WeChat database decryption
const (
// Common constants
PageSize = 4096
KeySize = 32
SaltSize = 16
AESBlockSize = 16
SQLiteHeader = "SQLite format 3\x00"
// Version specific constants
V3IterCount = 64000
V4IterCount = 256000
IVSize = 16 // Same for both versions
HmacSHA1Size = 20 // Used in V3
HmacSHA512Size = 64 // Used in V4
)
// Error definitions
var (
ErrHashVerificationFailed = errors.New("hash verification failed")
ErrInvalidVersion = errors.New("invalid version, must be 3 or 4")
ErrInvalidKey = errors.New("invalid key format")
ErrIncorrectKey = errors.New("incorrect decryption key")
ErrReadFile = errors.New("failed to read database file")
ErrOpenFile = errors.New("failed to open database file")
ErrIncompleteRead = errors.New("incomplete header read")
ErrCreateCipher = errors.New("failed to create cipher")
ErrDecodeKey = errors.New("failed to decode hex key")
ErrWriteOutput = errors.New("failed to write output")
ErrSeekFile = errors.New("failed to seek in file")
ErrOperationCanceled = errors.New("operation was canceled")
ErrAlreadyDecrypted = errors.New("file is already decrypted")
)
// Decryptor handles the decryption of WeChat database files
type Decryptor struct {
// Database file path
dbPath string
// Database properties
version int
salt []byte
page1 []byte
reserve int
// Calculated fields
hashFunc func() hash.Hash
hmacSize int
currentPage int64
totalPages int64
}
// NewDecryptor creates a new Decryptor for the specified database file and version
func NewDecryptor(dbPath string, version int) (*Decryptor, error) {
// Validate version
if version != 3 && version != 4 {
return nil, ErrInvalidVersion
}
// Open database file
fp, err := os.Open(dbPath)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOpenFile, err)
}
defer fp.Close()
// Get file size
fileInfo, err := fp.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file info: %v", err)
}
// Calculate total pages
fileSize := fileInfo.Size()
totalPages := fileSize / PageSize
if fileSize%PageSize > 0 {
totalPages++
}
// Read first page
buffer := make([]byte, PageSize)
n, err := io.ReadFull(fp, buffer)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrReadFile, err)
}
if n != PageSize {
return nil, fmt.Errorf("%w: expected %d bytes, got %d", ErrIncompleteRead, PageSize, n)
}
// Check if file is already decrypted
if bytes.Equal(buffer[:len(SQLiteHeader)-1], []byte(SQLiteHeader[:len(SQLiteHeader)-1])) {
return nil, ErrAlreadyDecrypted
}
// Initialize hash function and HMAC size based on version
var hashFunc func() hash.Hash
var hmacSize int
if version == 4 {
hashFunc = sha512.New
hmacSize = HmacSHA512Size
} else {
hashFunc = sha1.New
hmacSize = HmacSHA1Size
}
// Calculate reserve size and MAC offset
reserve := IVSize + hmacSize
if reserve%AESBlockSize != 0 {
reserve = ((reserve / AESBlockSize) + 1) * AESBlockSize
}
return &Decryptor{
dbPath: dbPath,
version: version,
salt: buffer[:SaltSize],
page1: buffer,
reserve: reserve,
hashFunc: hashFunc,
hmacSize: hmacSize,
totalPages: totalPages,
}, nil
}
// GetTotalPages returns the total number of pages in the database
func (d *Decryptor) GetTotalPages() int64 {
return d.totalPages
}
// Validate checks if the provided key is valid for this database
func (d *Decryptor) Validate(key []byte) bool {
if len(key) != KeySize {
return false
}
_, macKey := d.calcPBKDF2Key(key)
return d.validate(macKey)
}
func (d *Decryptor) calcPBKDF2Key(key []byte) ([]byte, []byte) {
// Generate encryption key from password
var encKey []byte
if d.version == 4 {
encKey = pbkdf2.Key(key, d.salt, V4IterCount, KeySize, sha512.New)
} else {
encKey = pbkdf2.Key(key, d.salt, V3IterCount, KeySize, sha1.New)
}
// Generate MAC key
macSalt := xorBytes(d.salt, 0x3a)
macKey := pbkdf2.Key(encKey, macSalt, 2, KeySize, d.hashFunc)
return encKey, macKey
}
func (d *Decryptor) validate(macKey []byte) bool {
// Calculate HMAC
hashMac := hmac.New(d.hashFunc, macKey)
dataEnd := PageSize - d.reserve + IVSize
hashMac.Write(d.page1[SaltSize:dataEnd])
// Page number is fixed as 1
pageNoBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(pageNoBytes, 1)
hashMac.Write(pageNoBytes)
calculatedMAC := hashMac.Sum(nil)
storedMAC := d.page1[dataEnd : dataEnd+d.hmacSize]
return hmac.Equal(calculatedMAC, storedMAC)
}
// Decrypt decrypts the database using the provided key and writes the result to the writer
func (d *Decryptor) Decrypt(ctx context.Context, hexKey string, w io.Writer) error {
// Decode key
key, err := hex.DecodeString(hexKey)
if err != nil {
return fmt.Errorf("%w: %v", ErrDecodeKey, err)
}
encKey, macKey := d.calcPBKDF2Key(key)
// Validate key first
if !d.validate(macKey) {
return ErrIncorrectKey
}
// Open input file
dbFile, err := os.Open(d.dbPath)
if err != nil {
return fmt.Errorf("%w: %v", ErrOpenFile, err)
}
defer dbFile.Close()
// Write SQLite header to output
_, err = w.Write([]byte(SQLiteHeader))
if err != nil {
return fmt.Errorf("%w: %v", ErrWriteOutput, err)
}
// Process each page
pageBuf := make([]byte, PageSize)
d.currentPage = 0
for curPage := int64(0); curPage < d.totalPages; curPage++ {
// Check for cancellation before processing each page
select {
case <-ctx.Done():
return ErrOperationCanceled
default:
// Continue processing
}
// For the first page, we need to skip the salt
if curPage == 0 {
// Read the first page
_, err = io.ReadFull(dbFile, pageBuf)
if err != nil {
return fmt.Errorf("%w: %v", ErrReadFile, err)
}
} else {
// Read a full page
n, err := io.ReadFull(dbFile, pageBuf)
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
// Handle last partial page
if n > 0 {
// Process partial page
// For simplicity, we'll just break here
break
}
}
return fmt.Errorf("%w: %v", ErrReadFile, err)
}
}
// check if page contains only zeros (v3 & v4 both have this behavior)
allZeros := true
for _, b := range pageBuf {
if b != 0 {
allZeros = false
break
}
}
if allZeros {
// Write the zeros page to output
_, err = w.Write(pageBuf)
if err != nil {
return fmt.Errorf("%w: %v", ErrWriteOutput, err)
}
// Update progress
d.currentPage = curPage + 1
continue
// // Set current page to total pages to indicate completion
// d.currentPage = d.totalPages
// return nil
}
// Decrypt the page
decryptedPage, err := d.decryptPage(encKey, macKey, pageBuf, curPage)
if err != nil {
return err
}
// Write decrypted page to output
_, err = w.Write(decryptedPage)
if err != nil {
return fmt.Errorf("%w: %v", ErrWriteOutput, err)
}
// Update progress
d.currentPage = curPage + 1
}
return nil
}
// decryptPage decrypts a single page of the database
func (d *Decryptor) decryptPage(key, macKey []byte, pageBuf []byte, pageNum int64) ([]byte, error) {
offset := 0
if pageNum == 0 {
offset = SaltSize
}
// Verify HMAC
mac := hmac.New(d.hashFunc, macKey)
mac.Write(pageBuf[offset : PageSize-d.reserve+IVSize])
// Convert page number and update HMAC
pageNumBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(pageNumBytes, uint32(pageNum+1))
mac.Write(pageNumBytes)
hashMac := mac.Sum(nil)
hashMacStartOffset := PageSize - d.reserve + IVSize
hashMacEndOffset := hashMacStartOffset + len(hashMac)
if !bytes.Equal(hashMac, pageBuf[hashMacStartOffset:hashMacEndOffset]) {
return nil, ErrHashVerificationFailed
}
// Decrypt content using AES-256-CBC
iv := pageBuf[PageSize-d.reserve : PageSize-d.reserve+IVSize]
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrCreateCipher, err)
}
mode := cipher.NewCBCDecrypter(block, iv)
// Create a copy of encrypted data for decryption
encrypted := make([]byte, PageSize-d.reserve-offset)
copy(encrypted, pageBuf[offset:PageSize-d.reserve])
// Decrypt in place
mode.CryptBlocks(encrypted, encrypted)
// Combine decrypted data with reserve part
decryptedPage := append(encrypted, pageBuf[PageSize-d.reserve:PageSize]...)
return decryptedPage, nil
}
// xorBytes performs XOR operation on each byte of the array with the specified byte
func xorBytes(a []byte, b byte) []byte {
result := make([]byte, len(a))
for i := range a {
result[i] = a[i] ^ b
}
return result
}
// Utility functions for backward compatibility
// DecryptDBFile decrypts a WeChat database file and returns the decrypted content
func DecryptDBFile(dbPath string, hexKey string, version int) ([]byte, error) {
// Create a buffer to store the decrypted content
var buf bytes.Buffer
// Create a decryptor
d, err := NewDecryptor(dbPath, version)
if err != nil {
return nil, err
}
// Decrypt the database
err = d.Decrypt(context.Background(), hexKey, &buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// DecryptDBFileToFile decrypts a WeChat database file and saves the result to the specified output file
func DecryptDBFileToFile(dbPath, outputPath, hexKey string, version int) error {
// Create output file
outputFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %v", err)
}
defer outputFile.Close()
// Create a decryptor
d, err := NewDecryptor(dbPath, version)
if err != nil {
return err
}
// Decrypt the database
return d.Decrypt(context.Background(), hexKey, outputFile)
}
// ValidateDBKey validates if the provided key is correct for the database
func ValidateDBKey(dbPath string, hexKey string, version int) bool {
// Create a decryptor
d, err := NewDecryptor(dbPath, version)
if err != nil {
return false
}
// Decode key
key, err := hex.DecodeString(hexKey)
if err != nil {
return false
}
// Validate the key
return d.Validate(key)
}

View File

@@ -0,0 +1,138 @@
package common
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"encoding/binary"
"fmt"
"hash"
"io"
"os"
"github.com/sjzar/chatlog/internal/errors"
)
const (
KeySize = 32
SaltSize = 16
AESBlockSize = 16
SQLiteHeader = "SQLite format 3\x00"
IVSize = 16
)
type DBFile struct {
Path string
Salt []byte
TotalPages int64
FirstPage []byte
}
func OpenDBFile(dbPath string, pageSize int) (*DBFile, error) {
fp, err := os.Open(dbPath)
if err != nil {
return nil, errors.DecryptOpenFileFailed(dbPath, err)
}
defer fp.Close()
fileInfo, err := fp.Stat()
if err != nil {
return nil, errors.WeChatDecryptFailed(err)
}
fileSize := fileInfo.Size()
totalPages := fileSize / int64(pageSize)
if fileSize%int64(pageSize) > 0 {
totalPages++
}
buffer := make([]byte, pageSize)
n, err := io.ReadFull(fp, buffer)
if err != nil {
return nil, errors.DecryptReadFileFailed(dbPath, err)
}
if n != pageSize {
return nil, errors.DecryptIncompleteRead(fmt.Errorf("read %d bytes, expected %d", n, pageSize))
}
if bytes.Equal(buffer[:len(SQLiteHeader)-1], []byte(SQLiteHeader[:len(SQLiteHeader)-1])) {
return nil, errors.ErrAlreadyDecrypted
}
return &DBFile{
Path: dbPath,
Salt: buffer[:SaltSize],
FirstPage: buffer,
TotalPages: totalPages,
}, nil
}
func XorBytes(a []byte, b byte) []byte {
result := make([]byte, len(a))
for i := range a {
result[i] = a[i] ^ b
}
return result
}
func ValidateKey(page1 []byte, key []byte, salt []byte, hashFunc func() hash.Hash, hmacSize int, reserve int, pageSize int, deriveKeys func([]byte, []byte) ([]byte, []byte)) bool {
if len(key) != KeySize {
return false
}
_, macKey := deriveKeys(key, salt)
mac := hmac.New(hashFunc, macKey)
dataEnd := pageSize - reserve + IVSize
mac.Write(page1[SaltSize:dataEnd])
pageNoBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(pageNoBytes, 1)
mac.Write(pageNoBytes)
calculatedMAC := mac.Sum(nil)
storedMAC := page1[dataEnd : dataEnd+hmacSize]
return hmac.Equal(calculatedMAC, storedMAC)
}
func DecryptPage(pageBuf []byte, encKey []byte, macKey []byte, pageNum int64, hashFunc func() hash.Hash, hmacSize int, reserve int, pageSize int) ([]byte, error) {
offset := 0
if pageNum == 0 {
offset = SaltSize
}
mac := hmac.New(hashFunc, macKey)
mac.Write(pageBuf[offset : pageSize-reserve+IVSize])
pageNoBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(pageNoBytes, uint32(pageNum+1))
mac.Write(pageNoBytes)
hashMac := mac.Sum(nil)
hashMacStartOffset := pageSize - reserve + IVSize
hashMacEndOffset := hashMacStartOffset + hmacSize
if !bytes.Equal(hashMac, pageBuf[hashMacStartOffset:hashMacEndOffset]) {
return nil, errors.ErrDecryptHashVerificationFailed
}
iv := pageBuf[pageSize-reserve : pageSize-reserve+IVSize]
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, errors.DecryptCreateCipherFailed(err)
}
mode := cipher.NewCBCDecrypter(block, iv)
encrypted := make([]byte, pageSize-reserve-offset)
copy(encrypted, pageBuf[offset:pageSize-reserve])
mode.CryptBlocks(encrypted, encrypted)
decryptedPage := append(encrypted, pageBuf[pageSize-reserve:pageSize]...)
return decryptedPage, nil
}

View File

@@ -0,0 +1,184 @@
package darwin
import (
"context"
"crypto/sha1"
"encoding/hex"
"hash"
"io"
"os"
"github.com/sjzar/chatlog/internal/errors"
"github.com/sjzar/chatlog/internal/wechat/decrypt/common"
"golang.org/x/crypto/pbkdf2"
)
// 常量定义
const (
V3PageSize = 1024
HmacSHA1Size = 20
)
// V3Decryptor 实现 macOS V3 版本的解密器
type V3Decryptor struct {
// macOS V3 特定参数
hmacSize int
hashFunc func() hash.Hash
reserve int
pageSize int
version string
}
// NewV3Decryptor 创建 macOS V3 解密器
func NewV3Decryptor() *V3Decryptor {
hashFunc := sha1.New
hmacSize := HmacSHA1Size
reserve := common.IVSize + hmacSize
if reserve%common.AESBlockSize != 0 {
reserve = ((reserve / common.AESBlockSize) + 1) * common.AESBlockSize
}
return &V3Decryptor{
hmacSize: hmacSize,
hashFunc: hashFunc,
reserve: reserve,
pageSize: V3PageSize,
version: "macOS v3",
}
}
// deriveKeys 派生 MAC 密钥
// 注意macOS V3 版本直接使用提供的密钥作为加密密钥,不进行 PBKDF2 派生
func (d *V3Decryptor) deriveKeys(key []byte, salt []byte) ([]byte, []byte) {
// 对于 macOS V3直接使用密钥作为加密密钥
encKey := key
// 生成 MAC 密钥
macSalt := common.XorBytes(salt, 0x3a)
macKey := pbkdf2.Key(encKey, macSalt, 2, common.KeySize, d.hashFunc)
return encKey, macKey
}
// Validate 验证密钥是否有效
func (d *V3Decryptor) Validate(page1 []byte, key []byte) bool {
if len(page1) < d.pageSize || len(key) != common.KeySize {
return false
}
salt := page1[:common.SaltSize]
return common.ValidateKey(page1, key, salt, d.hashFunc, d.hmacSize, d.reserve, d.pageSize, d.deriveKeys)
}
// Decrypt 解密数据库
func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string, output io.Writer) error {
// 解码密钥
key, err := hex.DecodeString(hexKey)
if err != nil {
return errors.DecryptDecodeKeyFailed(err)
}
// 打开数据库文件并读取基本信息
dbInfo, err := common.OpenDBFile(dbfile, d.pageSize)
if err != nil {
return err
}
// 验证密钥
if !d.Validate(dbInfo.FirstPage, key) {
return errors.ErrDecryptIncorrectKey
}
// 计算密钥
encKey, macKey := d.deriveKeys(key, dbInfo.Salt)
// 打开数据库文件
dbFile, err := os.Open(dbfile)
if err != nil {
return errors.DecryptOpenFileFailed(dbfile, err)
}
defer dbFile.Close()
// 写入 SQLite 头
_, err = output.Write([]byte(common.SQLiteHeader))
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
// 处理每一页
pageBuf := make([]byte, d.pageSize)
for curPage := int64(0); curPage < dbInfo.TotalPages; curPage++ {
// 检查是否取消
select {
case <-ctx.Done():
return errors.DecryptOperationCanceled()
default:
// 继续处理
}
// 读取一页
n, err := io.ReadFull(dbFile, pageBuf)
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
// 处理最后一部分页面
if n > 0 {
break
}
}
return errors.DecryptReadFileFailed(dbfile, err)
}
// 检查页面是否全为零
allZeros := true
for _, b := range pageBuf {
if b != 0 {
allZeros = false
break
}
}
if allZeros {
// 写入零页面
_, err = output.Write(pageBuf)
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
continue
}
// 解密页面
decryptedData, err := common.DecryptPage(pageBuf, encKey, macKey, curPage, d.hashFunc, d.hmacSize, d.reserve, d.pageSize)
if err != nil {
return err
}
// 写入解密后的页面
_, err = output.Write(decryptedData)
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
}
return nil
}
// GetPageSize 返回页面大小
func (d *V3Decryptor) GetPageSize() int {
return d.pageSize
}
// GetReserve 返回保留字节数
func (d *V3Decryptor) GetReserve() int {
return d.reserve
}
// GetHMACSize 返回HMAC大小
func (d *V3Decryptor) GetHMACSize() int {
return d.hmacSize
}
// GetVersion 返回解密器版本
func (d *V3Decryptor) GetVersion() string {
return d.version
}

View File

@@ -0,0 +1,194 @@
package darwin
import (
"context"
"crypto/sha512"
"encoding/hex"
"hash"
"io"
"os"
"github.com/sjzar/chatlog/internal/errors"
"github.com/sjzar/chatlog/internal/wechat/decrypt/common"
"golang.org/x/crypto/pbkdf2"
)
// Darwin Version 4 same as WIndows Version 4
// V4 版本特定常量
const (
V4PageSize = 4096
V4IterCount = 256000
HmacSHA512Size = 64
)
// V4Decryptor 实现Windows V4版本的解密器
type V4Decryptor struct {
// V4 特定参数
iterCount int
hmacSize int
hashFunc func() hash.Hash
reserve int
pageSize int
version string
}
// NewV4Decryptor 创建Windows V4解密器
func NewV4Decryptor() *V4Decryptor {
hashFunc := sha512.New
hmacSize := HmacSHA512Size
reserve := common.IVSize + hmacSize
if reserve%common.AESBlockSize != 0 {
reserve = ((reserve / common.AESBlockSize) + 1) * common.AESBlockSize
}
return &V4Decryptor{
iterCount: V4IterCount,
hmacSize: hmacSize,
hashFunc: hashFunc,
reserve: reserve,
pageSize: V4PageSize,
version: "macOS v4",
}
}
// deriveKeys 派生加密密钥和MAC密钥
func (d *V4Decryptor) deriveKeys(key []byte, salt []byte) ([]byte, []byte) {
// 生成加密密钥
encKey := pbkdf2.Key(key, salt, d.iterCount, common.KeySize, d.hashFunc)
// 生成MAC密钥
macSalt := common.XorBytes(salt, 0x3a)
macKey := pbkdf2.Key(encKey, macSalt, 2, common.KeySize, d.hashFunc)
return encKey, macKey
}
// Validate 验证密钥是否有效
func (d *V4Decryptor) Validate(page1 []byte, key []byte) bool {
if len(page1) < d.pageSize || len(key) != common.KeySize {
return false
}
salt := page1[:common.SaltSize]
return common.ValidateKey(page1, key, salt, d.hashFunc, d.hmacSize, d.reserve, d.pageSize, d.deriveKeys)
}
// Decrypt 解密数据库
func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string, output io.Writer) error {
// 解码密钥
key, err := hex.DecodeString(hexKey)
if err != nil {
return errors.DecryptDecodeKeyFailed(err)
}
// 打开数据库文件并读取基本信息
dbInfo, err := common.OpenDBFile(dbfile, d.pageSize)
if err != nil {
return err
}
// 验证密钥
if !d.Validate(dbInfo.FirstPage, key) {
return errors.ErrDecryptIncorrectKey
}
// 计算密钥
encKey, macKey := d.deriveKeys(key, dbInfo.Salt)
// 打开数据库文件
dbFile, err := os.Open(dbfile)
if err != nil {
return errors.DecryptOpenFileFailed(dbfile, err)
}
defer dbFile.Close()
// 写入SQLite头
_, err = output.Write([]byte(common.SQLiteHeader))
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
// 处理每一页
pageBuf := make([]byte, d.pageSize)
for curPage := int64(0); curPage < dbInfo.TotalPages; curPage++ {
// 检查是否取消
select {
case <-ctx.Done():
return errors.DecryptOperationCanceled()
default:
// 继续处理
}
// 读取一页
n, err := io.ReadFull(dbFile, pageBuf)
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
// 处理最后一部分页面
if n > 0 {
break
}
}
return errors.DecryptReadFileFailed(dbfile, err)
}
// 检查页面是否全为零
allZeros := true
for _, b := range pageBuf {
if b != 0 {
allZeros = false
break
}
}
if allZeros {
// 写入零页面
_, err = output.Write(pageBuf)
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
continue
}
// 解密页面
decryptedData, err := common.DecryptPage(pageBuf, encKey, macKey, curPage, d.hashFunc, d.hmacSize, d.reserve, d.pageSize)
if err != nil {
return err
}
// 写入解密后的页面
_, err = output.Write(decryptedData)
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
}
return nil
}
// GetPageSize 返回页面大小
func (d *V4Decryptor) GetPageSize() int {
return d.pageSize
}
// GetReserve 返回保留字节数
func (d *V4Decryptor) GetReserve() int {
return d.reserve
}
// GetHMACSize 返回HMAC大小
func (d *V4Decryptor) GetHMACSize() int {
return d.hmacSize
}
// GetVersion 返回解密器版本
func (d *V4Decryptor) GetVersion() string {
return d.version
}
// GetIterCount 返回迭代次数Windows特有
func (d *V4Decryptor) GetIterCount() int {
return d.iterCount
}

View File

@@ -0,0 +1,55 @@
package decrypt
import (
"context"
"fmt"
"io"
"github.com/sjzar/chatlog/internal/errors"
"github.com/sjzar/chatlog/internal/wechat/decrypt/darwin"
"github.com/sjzar/chatlog/internal/wechat/decrypt/windows"
)
// 错误定义
var (
ErrInvalidVersion = fmt.Errorf("invalid version, must be 3 or 4")
ErrUnsupportedPlatform = fmt.Errorf("unsupported platform")
)
// Decryptor 定义数据库解密的接口
type Decryptor interface {
// Decrypt 解密数据库
Decrypt(ctx context.Context, dbfile string, key string, output io.Writer) error
// Validate 验证密钥是否有效
Validate(page1 []byte, key []byte) bool
// GetPageSize 返回页面大小
GetPageSize() int
// GetReserve 返回保留字节数
GetReserve() int
// GetHMACSize 返回HMAC大小
GetHMACSize() int
// GetVersion 返回解密器版本
GetVersion() string
}
// NewDecryptor 创建一个新的解密器
func NewDecryptor(platform string, version int) (Decryptor, error) {
// 根据平台返回对应的实现
switch {
case platform == "windows" && version == 3:
return windows.NewV3Decryptor(), nil
case platform == "windows" && version == 4:
return windows.NewV4Decryptor(), nil
case platform == "darwin" && version == 3:
return darwin.NewV3Decryptor(), nil
case platform == "darwin" && version == 4:
return darwin.NewV4Decryptor(), nil
default:
return nil, errors.PlatformUnsupported(platform, version)
}
}

View File

@@ -0,0 +1,56 @@
package decrypt
import (
"path/filepath"
"github.com/sjzar/chatlog/internal/wechat/decrypt/common"
)
type Validator struct {
platform string
version int
dbPath string
decryptor Decryptor
dbFile *common.DBFile
}
// NewValidator 创建一个仅用于验证的验证器
func NewValidator(dataDir string, platform string, version int) (*Validator, error) {
decryptor, err := NewDecryptor(platform, version)
if err != nil {
return nil, err
}
dbFile := GetSimpleDBFile(platform, version)
dbPath := filepath.Join(dataDir + "/" + dbFile)
d, err := common.OpenDBFile(dbPath, decryptor.GetPageSize())
if err != nil {
return nil, err
}
return &Validator{
platform: platform,
version: version,
dbPath: dbPath,
decryptor: decryptor,
dbFile: d,
}, nil
}
func (v *Validator) Validate(key []byte) bool {
return v.decryptor.Validate(v.dbFile.FirstPage, key)
}
func GetSimpleDBFile(platform string, version int) string {
switch {
case platform == "windows" && version == 3:
return "Msg\\Misc.db"
case platform == "windows" && version == 4:
return "db_storage\\message\\message_0.db"
case platform == "darwin" && version == 3:
return "Message/msg_0.db"
case platform == "darwin" && version == 4:
return "db_storage/message/message_0.db"
}
return ""
}

View File

@@ -0,0 +1,192 @@
package windows
import (
"context"
"crypto/sha1"
"encoding/hex"
"hash"
"io"
"os"
"github.com/sjzar/chatlog/internal/errors"
"github.com/sjzar/chatlog/internal/wechat/decrypt/common"
"golang.org/x/crypto/pbkdf2"
)
// V3 版本特定常量
const (
PageSize = 4096
V3IterCount = 64000
HmacSHA1Size = 20
)
// V3Decryptor 实现Windows V3版本的解密器
type V3Decryptor struct {
// V3 特定参数
iterCount int
hmacSize int
hashFunc func() hash.Hash
reserve int
pageSize int
version string
}
// NewV3Decryptor 创建Windows V3解密器
func NewV3Decryptor() *V3Decryptor {
hashFunc := sha1.New
hmacSize := HmacSHA1Size
reserve := common.IVSize + hmacSize
if reserve%common.AESBlockSize != 0 {
reserve = ((reserve / common.AESBlockSize) + 1) * common.AESBlockSize
}
return &V3Decryptor{
iterCount: V3IterCount,
hmacSize: hmacSize,
hashFunc: hashFunc,
reserve: reserve,
pageSize: PageSize,
version: "Windows v3",
}
}
// deriveKeys 派生加密密钥和MAC密钥
func (d *V3Decryptor) deriveKeys(key []byte, salt []byte) ([]byte, []byte) {
// 生成加密密钥
encKey := pbkdf2.Key(key, salt, d.iterCount, common.KeySize, d.hashFunc)
// 生成MAC密钥
macSalt := common.XorBytes(salt, 0x3a)
macKey := pbkdf2.Key(encKey, macSalt, 2, common.KeySize, d.hashFunc)
return encKey, macKey
}
// Validate 验证密钥是否有效
func (d *V3Decryptor) Validate(page1 []byte, key []byte) bool {
if len(page1) < d.pageSize || len(key) != common.KeySize {
return false
}
salt := page1[:common.SaltSize]
return common.ValidateKey(page1, key, salt, d.hashFunc, d.hmacSize, d.reserve, d.pageSize, d.deriveKeys)
}
// Decrypt 解密数据库
func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string, output io.Writer) error {
// 解码密钥
key, err := hex.DecodeString(hexKey)
if err != nil {
return errors.DecryptDecodeKeyFailed(err)
}
// 打开数据库文件并读取基本信息
dbInfo, err := common.OpenDBFile(dbfile, d.pageSize)
if err != nil {
return err
}
// 验证密钥
if !d.Validate(dbInfo.FirstPage, key) {
return errors.ErrDecryptIncorrectKey
}
// 计算密钥
encKey, macKey := d.deriveKeys(key, dbInfo.Salt)
// 打开数据库文件
dbFile, err := os.Open(dbfile)
if err != nil {
return errors.DecryptOpenFileFailed(dbfile, err)
}
defer dbFile.Close()
// 写入SQLite头
_, err = output.Write([]byte(common.SQLiteHeader))
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
// 处理每一页
pageBuf := make([]byte, d.pageSize)
for curPage := int64(0); curPage < dbInfo.TotalPages; curPage++ {
// 检查是否取消
select {
case <-ctx.Done():
return errors.DecryptOperationCanceled()
default:
// 继续处理
}
// 读取一页
n, err := io.ReadFull(dbFile, pageBuf)
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
// 处理最后一部分页面
if n > 0 {
break
}
}
return errors.DecryptReadFileFailed(dbfile, err)
}
// 检查页面是否全为零
allZeros := true
for _, b := range pageBuf {
if b != 0 {
allZeros = false
break
}
}
if allZeros {
// 写入零页面
_, err = output.Write(pageBuf)
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
continue
}
// 解密页面
decryptedData, err := common.DecryptPage(pageBuf, encKey, macKey, curPage, d.hashFunc, d.hmacSize, d.reserve, d.pageSize)
if err != nil {
return err
}
// 写入解密后的页面
_, err = output.Write(decryptedData)
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
}
return nil
}
// GetPageSize 返回页面大小
func (d *V3Decryptor) GetPageSize() int {
return d.pageSize
}
// GetReserve 返回保留字节数
func (d *V3Decryptor) GetReserve() int {
return d.reserve
}
// GetHMACSize 返回HMAC大小
func (d *V3Decryptor) GetHMACSize() int {
return d.hmacSize
}
// GetVersion 返回解密器版本
func (d *V3Decryptor) GetVersion() string {
return d.version
}
// GetIterCount 返回迭代次数Windows特有
func (d *V3Decryptor) GetIterCount() int {
return d.iterCount
}

View File

@@ -0,0 +1,190 @@
package windows
import (
"context"
"crypto/sha512"
"encoding/hex"
"hash"
"io"
"os"
"github.com/sjzar/chatlog/internal/errors"
"github.com/sjzar/chatlog/internal/wechat/decrypt/common"
"golang.org/x/crypto/pbkdf2"
)
// V4 版本特定常量
const (
V4IterCount = 256000
HmacSHA512Size = 64
)
// V4Decryptor 实现Windows V4版本的解密器
type V4Decryptor struct {
// V4 特定参数
iterCount int
hmacSize int
hashFunc func() hash.Hash
reserve int
pageSize int
version string
}
// NewV4Decryptor 创建Windows V4解密器
func NewV4Decryptor() *V4Decryptor {
hashFunc := sha512.New
hmacSize := HmacSHA512Size
reserve := common.IVSize + hmacSize
if reserve%common.AESBlockSize != 0 {
reserve = ((reserve / common.AESBlockSize) + 1) * common.AESBlockSize
}
return &V4Decryptor{
iterCount: V4IterCount,
hmacSize: hmacSize,
hashFunc: hashFunc,
reserve: reserve,
pageSize: PageSize,
version: "Windows v4",
}
}
// deriveKeys 派生加密密钥和MAC密钥
func (d *V4Decryptor) deriveKeys(key []byte, salt []byte) ([]byte, []byte) {
// 生成加密密钥
encKey := pbkdf2.Key(key, salt, d.iterCount, common.KeySize, d.hashFunc)
// 生成MAC密钥
macSalt := common.XorBytes(salt, 0x3a)
macKey := pbkdf2.Key(encKey, macSalt, 2, common.KeySize, d.hashFunc)
return encKey, macKey
}
// Validate 验证密钥是否有效
func (d *V4Decryptor) Validate(page1 []byte, key []byte) bool {
if len(page1) < d.pageSize || len(key) != common.KeySize {
return false
}
salt := page1[:common.SaltSize]
return common.ValidateKey(page1, key, salt, d.hashFunc, d.hmacSize, d.reserve, d.pageSize, d.deriveKeys)
}
// Decrypt 解密数据库
func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string, output io.Writer) error {
// 解码密钥
key, err := hex.DecodeString(hexKey)
if err != nil {
return errors.DecryptDecodeKeyFailed(err)
}
// 打开数据库文件并读取基本信息
dbInfo, err := common.OpenDBFile(dbfile, d.pageSize)
if err != nil {
return err
}
// 验证密钥
if !d.Validate(dbInfo.FirstPage, key) {
return errors.ErrDecryptIncorrectKey
}
// 计算密钥
encKey, macKey := d.deriveKeys(key, dbInfo.Salt)
// 打开数据库文件
dbFile, err := os.Open(dbfile)
if err != nil {
return errors.DecryptOpenFileFailed(dbfile, err)
}
defer dbFile.Close()
// 写入SQLite头
_, err = output.Write([]byte(common.SQLiteHeader))
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
// 处理每一页
pageBuf := make([]byte, d.pageSize)
for curPage := int64(0); curPage < dbInfo.TotalPages; curPage++ {
// 检查是否取消
select {
case <-ctx.Done():
return errors.DecryptOperationCanceled()
default:
// 继续处理
}
// 读取一页
n, err := io.ReadFull(dbFile, pageBuf)
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
// 处理最后一部分页面
if n > 0 {
break
}
}
return errors.DecryptReadFileFailed(dbfile, err)
}
// 检查页面是否全为零
allZeros := true
for _, b := range pageBuf {
if b != 0 {
allZeros = false
break
}
}
if allZeros {
// 写入零页面
_, err = output.Write(pageBuf)
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
continue
}
// 解密页面
decryptedData, err := common.DecryptPage(pageBuf, encKey, macKey, curPage, d.hashFunc, d.hmacSize, d.reserve, d.pageSize)
if err != nil {
return err
}
// 写入解密后的页面
_, err = output.Write(decryptedData)
if err != nil {
return errors.DecryptWriteOutputFailed(err)
}
}
return nil
}
// GetPageSize 返回页面大小
func (d *V4Decryptor) GetPageSize() int {
return d.pageSize
}
// GetReserve 返回保留字节数
func (d *V4Decryptor) GetReserve() int {
return d.reserve
}
// GetHMACSize 返回HMAC大小
func (d *V4Decryptor) GetHMACSize() int {
return d.hmacSize
}
// GetVersion 返回解密器版本
func (d *V4Decryptor) GetVersion() string {
return d.version
}
// GetIterCount 返回迭代次数Windows特有
func (d *V4Decryptor) GetIterCount() int {
return d.iterCount
}

View File

@@ -1,50 +0,0 @@
package wechat
import (
"github.com/sjzar/chatlog/pkg/dllver"
"github.com/shirou/gopsutil/v4/process"
log "github.com/sirupsen/logrus"
)
const (
StatusInit = ""
StatusOffline = "offline"
StatusOnline = "online"
)
type Info struct {
PID uint32
ExePath string
Version *dllver.Info
Status string
DataDir string
AccountName string
Key string
}
func NewInfo(p *process.Process) (*Info, error) {
info := &Info{
PID: uint32(p.Pid),
Status: StatusOffline,
}
var err error
info.ExePath, err = p.Exe()
if err != nil {
log.Error(err)
return nil, err
}
info.Version, err = dllver.New(info.ExePath)
if err != nil {
log.Error(err)
return nil, err
}
if err := info.initialize(p); err != nil {
return nil, err
}
return info, nil
}

View File

@@ -1,16 +0,0 @@
//go:build !windows
package wechat
import "github.com/shirou/gopsutil/v4/process"
// Giao~
// 还没来得及写Mac 版本打算通过 vmmap 检查内存区域,再用 lldb 读取内存来检查 Key需要关 SIP 或自签名应用,稍晚再填坑
func (i *Info) initialize(p *process.Process) error {
return nil
}
func (i *Info) GetKey() (string, error) {
return "mock-key", nil
}

View File

@@ -1,507 +0,0 @@
package wechat
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"path/filepath"
"runtime"
"strings"
"sync"
"unsafe"
"github.com/sjzar/chatlog/pkg/util"
"github.com/shirou/gopsutil/v4/process"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)
const (
V3ModuleName = "WeChatWin.dll"
V3DBFile = "Msg\\Misc.db"
V4DBFile = "db_storage\\message\\message_0.db"
MaxWorkers = 16
// Windows memory protection constants
MEM_PRIVATE = 0x20000
)
// Common error definitions
var (
ErrWeChatOffline = errors.New("wechat is not logged in")
ErrOpenProcess = errors.New("failed to open process")
ErrReaddecryptor = errors.New("failed to read database header")
ErrCheckProcessBits = errors.New("failed to check process architecture")
ErrFindWeChatDLL = errors.New("WeChatWin.dll module not found")
ErrNoValidKey = errors.New("no valid key found")
ErrInvalidFilePath = errors.New("invalid file path format")
)
// GetKey is the entry point for retrieving the WeChat database key
func (i *Info) GetKey() (string, error) {
if i.Status == StatusOffline {
return "", ErrWeChatOffline
}
// Choose key retrieval method based on WeChat version
if i.Version.FileMajorVersion == 4 {
return i.getKeyV4()
}
return i.getKeyV3()
}
// initialize initializes WeChat information
func (i *Info) initialize(p *process.Process) error {
files, err := p.OpenFiles()
if err != nil {
log.Error("Failed to get open file list: ", err)
return err
}
dbPath := V3DBFile
if i.Version.FileMajorVersion == 4 {
dbPath = V4DBFile
}
for _, f := range files {
if strings.HasSuffix(f.Path, dbPath) {
filePath := f.Path[4:] // Remove "\\?\" prefix
parts := strings.Split(filePath, string(filepath.Separator))
if len(parts) < 4 {
log.Debug("Invalid file path format: " + filePath)
continue
}
i.Status = StatusOnline
if i.Version.FileMajorVersion == 4 {
i.DataDir = strings.Join(parts[:len(parts)-3], string(filepath.Separator))
i.AccountName = parts[len(parts)-4]
} else {
i.DataDir = strings.Join(parts[:len(parts)-2], string(filepath.Separator))
i.AccountName = parts[len(parts)-3]
}
}
}
return nil
}
// getKeyV3 retrieves the database key for WeChat V3 version
func (i *Info) getKeyV3() (string, error) {
// Read database header for key validation
dbPath := filepath.Join(i.DataDir, V3DBFile)
decryptor, err := NewDecryptor(dbPath, i.Version.FileMajorVersion)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrReaddecryptor, err)
}
log.Debug("V3 database path: ", dbPath)
// Open WeChat process
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, i.PID)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrOpenProcess, err)
}
defer windows.CloseHandle(handle)
// Check process architecture
is64Bit, err := util.Is64Bit(handle)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrCheckProcessBits, err)
}
// Create context to control all goroutines
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create channels for memory data and results
memoryChannel := make(chan []byte, 100)
resultChannel := make(chan string, 1)
// Determine number of worker goroutines
workerCount := runtime.NumCPU()
if workerCount < 2 {
workerCount = 2
}
if workerCount > MaxWorkers {
workerCount = MaxWorkers
}
log.Debug("Starting ", workerCount, " workers for V3 key search")
// Start consumer goroutines
var workerWaitGroup sync.WaitGroup
workerWaitGroup.Add(workerCount)
for index := 0; index < workerCount; index++ {
go func() {
defer workerWaitGroup.Done()
workerV3(ctx, handle, decryptor, is64Bit, memoryChannel, resultChannel)
}()
}
// Start producer goroutine
var producerWaitGroup sync.WaitGroup
producerWaitGroup.Add(1)
go func() {
defer producerWaitGroup.Done()
defer close(memoryChannel) // Close channel when producer is done
err := i.findMemoryV3(ctx, handle, memoryChannel)
if err != nil {
log.Error(err)
}
}()
// Wait for producer and consumers to complete
go func() {
producerWaitGroup.Wait()
workerWaitGroup.Wait()
close(resultChannel)
}()
// Wait for result
result, ok := <-resultChannel
if ok && result != "" {
i.Key = result
return result, nil
}
return "", ErrNoValidKey
}
// getKeyV4 retrieves the database key for WeChat V4 version
func (i *Info) getKeyV4() (string, error) {
// Read database header for key validation
dbPath := filepath.Join(i.DataDir, V4DBFile)
decryptor, err := NewDecryptor(dbPath, i.Version.FileMajorVersion)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrReaddecryptor, err)
}
log.Debug("V4 database path: ", dbPath)
// Open process handle
handle, err := windows.OpenProcess(windows.PROCESS_VM_READ|windows.PROCESS_QUERY_INFORMATION, false, i.PID)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrOpenProcess, err)
}
defer windows.CloseHandle(handle)
// Create context to control all goroutines
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create channels for memory data and results
memoryChannel := make(chan []byte, 100)
resultChannel := make(chan string, 1)
// Determine number of worker goroutines
workerCount := runtime.NumCPU()
if workerCount < 2 {
workerCount = 2
}
if workerCount > MaxWorkers {
workerCount = MaxWorkers
}
log.Debug("Starting ", workerCount, " workers for V4 key search")
// Start consumer goroutines
var workerWaitGroup sync.WaitGroup
workerWaitGroup.Add(workerCount)
for index := 0; index < workerCount; index++ {
go func() {
defer workerWaitGroup.Done()
workerV4(ctx, handle, decryptor, memoryChannel, resultChannel)
}()
}
// Start producer goroutine
var producerWaitGroup sync.WaitGroup
producerWaitGroup.Add(1)
go func() {
defer producerWaitGroup.Done()
defer close(memoryChannel) // Close channel when producer is done
err := i.findMemoryV4(ctx, handle, memoryChannel)
if err != nil {
log.Error(err)
}
}()
// Wait for producer and consumers to complete
go func() {
producerWaitGroup.Wait()
workerWaitGroup.Wait()
close(resultChannel)
}()
// Wait for result
result, ok := <-resultChannel
if ok && result != "" {
i.Key = result
return result, nil
}
return "", ErrNoValidKey
}
// findMemoryV3 searches for writable memory regions in WeChatWin.dll for V3 version
func (i *Info) findMemoryV3(ctx context.Context, handle windows.Handle, memoryChannel chan<- []byte) error {
// Find WeChatWin.dll module
module, isFound := FindModule(i.PID, V3ModuleName)
if !isFound {
return ErrFindWeChatDLL
}
log.Debug("Found WeChatWin.dll module at base address: 0x", fmt.Sprintf("%X", module.ModBaseAddr))
// Read writable memory regions
baseAddr := uintptr(module.ModBaseAddr)
endAddr := baseAddr + uintptr(module.ModBaseSize)
currentAddr := baseAddr
for currentAddr < endAddr {
var mbi windows.MemoryBasicInformation
err := windows.VirtualQueryEx(handle, currentAddr, &mbi, unsafe.Sizeof(mbi))
if err != nil {
break
}
// Skip small memory regions
if mbi.RegionSize < 100*1024 {
currentAddr += uintptr(mbi.RegionSize)
continue
}
// Check if memory region is writable
isWritable := (mbi.Protect & (windows.PAGE_READWRITE | windows.PAGE_WRITECOPY | windows.PAGE_EXECUTE_READWRITE | windows.PAGE_EXECUTE_WRITECOPY)) > 0
if isWritable && uint32(mbi.State) == windows.MEM_COMMIT {
// Calculate region size, ensure it doesn't exceed DLL bounds
regionSize := uintptr(mbi.RegionSize)
if currentAddr+regionSize > endAddr {
regionSize = endAddr - currentAddr
}
// Read writable memory region
memory := make([]byte, regionSize)
if err = windows.ReadProcessMemory(handle, currentAddr, &memory[0], regionSize, nil); err == nil {
select {
case memoryChannel <- memory:
log.Debug("Sent memory region for analysis, size: ", regionSize, " bytes")
case <-ctx.Done():
return nil
}
}
}
// Move to next memory region
currentAddr = uintptr(mbi.BaseAddress) + uintptr(mbi.RegionSize)
}
return nil
}
// workerV3 processes memory regions to find V3 version key
func workerV3(ctx context.Context, handle windows.Handle, decryptor *Decryptor, is64Bit bool, memoryChannel <-chan []byte, resultChannel chan<- string) {
// Define search pattern
keyPattern := []byte{0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
ptrSize := 8
littleEndianFunc := binary.LittleEndian.Uint64
// Adjust for 32-bit process
if !is64Bit {
keyPattern = keyPattern[:4]
ptrSize = 4
littleEndianFunc = func(b []byte) uint64 { return uint64(binary.LittleEndian.Uint32(b)) }
}
for {
select {
case <-ctx.Done():
return
case memory, ok := <-memoryChannel:
if !ok {
return
}
index := len(memory)
for {
select {
case <-ctx.Done():
return // Exit if result found
default:
}
// Find pattern from end to beginning
index = bytes.LastIndex(memory[:index], keyPattern)
if index == -1 || index-ptrSize < 0 {
break
}
// Extract and validate pointer value
ptrValue := littleEndianFunc(memory[index-ptrSize : index])
if ptrValue > 0x10000 && ptrValue < 0x7FFFFFFFFFFF {
if key := validateKey(handle, decryptor, ptrValue); key != "" {
select {
case resultChannel <- key:
log.Debug("Valid key found for V3 database")
default:
}
return
}
}
index -= 1 // Continue searching from previous position
}
}
}
}
// findMemoryV4 searches for writable memory regions for V4 version
func (i *Info) findMemoryV4(ctx context.Context, handle windows.Handle, memoryChannel chan<- []byte) error {
// Define search range
minAddr := uintptr(0x10000) // Process space usually starts from 0x10000
maxAddr := uintptr(0x7FFFFFFF) // 32-bit process space limit
if runtime.GOARCH == "amd64" {
maxAddr = uintptr(0x7FFFFFFFFFFF) // 64-bit process space limit
}
log.Debug("Scanning memory regions from 0x", fmt.Sprintf("%X", minAddr), " to 0x", fmt.Sprintf("%X", maxAddr))
currentAddr := minAddr
for currentAddr < maxAddr {
var memInfo windows.MemoryBasicInformation
err := windows.VirtualQueryEx(handle, currentAddr, &memInfo, unsafe.Sizeof(memInfo))
if err != nil {
break
}
// Skip small memory regions
if memInfo.RegionSize < 1024*1024 {
currentAddr += uintptr(memInfo.RegionSize)
continue
}
// Check if memory region is readable and private
if memInfo.State == windows.MEM_COMMIT && (memInfo.Protect&windows.PAGE_READWRITE) != 0 && memInfo.Type == MEM_PRIVATE {
// Calculate region size, ensure it doesn't exceed limit
regionSize := uintptr(memInfo.RegionSize)
if currentAddr+regionSize > maxAddr {
regionSize = maxAddr - currentAddr
}
// Read memory region
memory := make([]byte, regionSize)
if err = windows.ReadProcessMemory(handle, currentAddr, &memory[0], regionSize, nil); err == nil {
select {
case memoryChannel <- memory:
log.Debug("Sent memory region for analysis, size: ", regionSize, " bytes")
case <-ctx.Done():
return nil
}
}
}
// Move to next memory region
currentAddr = uintptr(memInfo.BaseAddress) + uintptr(memInfo.RegionSize)
}
return nil
}
// workerV4 processes memory regions to find V4 version key
func workerV4(ctx context.Context, handle windows.Handle, decryptor *Decryptor, memoryChannel <-chan []byte, resultChannel chan<- string) {
// Define search pattern for V4
keyPattern := []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
ptrSize := 8
littleEndianFunc := binary.LittleEndian.Uint64
for {
select {
case <-ctx.Done():
return
case memory, ok := <-memoryChannel:
if !ok {
return
}
index := len(memory)
for {
select {
case <-ctx.Done():
return // Exit if result found
default:
}
// Find pattern from end to beginning
index = bytes.LastIndex(memory[:index], keyPattern)
if index == -1 || index-ptrSize < 0 {
break
}
// Extract and validate pointer value
ptrValue := littleEndianFunc(memory[index-ptrSize : index])
if ptrValue > 0x10000 && ptrValue < 0x7FFFFFFFFFFF {
if key := validateKey(handle, decryptor, ptrValue); key != "" {
select {
case resultChannel <- key:
log.Debug("Valid key found for V4 database")
default:
}
return
}
}
index -= 1 // Continue searching from previous position
}
}
}
}
// validateKey validates a single key candidate
func validateKey(handle windows.Handle, decryptor *Decryptor, addr uint64) string {
keyData := make([]byte, 0x20) // 32-byte key
if err := windows.ReadProcessMemory(handle, uintptr(addr), &keyData[0], uintptr(len(keyData)), nil); err != nil {
return ""
}
// Validate key against database header
if decryptor.Validate(keyData) {
return hex.EncodeToString(keyData)
}
return ""
}
// FindModule searches for a specified module in the process
// Used to find WeChatWin.dll module for V3 version
func FindModule(pid uint32, name string) (module windows.ModuleEntry32, isFound bool) {
// Create module snapshot
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, pid)
if err != nil {
log.Debug("Failed to create module snapshot: ", err)
return module, false
}
defer windows.CloseHandle(snapshot)
// Initialize module entry structure
module.Size = uint32(windows.SizeofModuleEntry32)
// Get the first module
if err := windows.Module32First(snapshot, &module); err != nil {
log.Debug("Failed to get first module: ", err)
return module, false
}
// Iterate through all modules to find WeChatWin.dll
for ; err == nil; err = windows.Module32Next(snapshot, &module) {
if windows.UTF16ToString(module.Module[:]) == name {
return module, true
}
}
return module, false
}

View File

@@ -0,0 +1,118 @@
package glance
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"time"
)
// FIXME 按照 region 读取效率较低512MB 内存读取耗时约 18s
type Glance struct {
PID uint32
MemRegions []MemRegion
pipePath string
data []byte
}
func NewGlance(pid uint32) *Glance {
return &Glance{
PID: pid,
pipePath: filepath.Join(os.TempDir(), fmt.Sprintf("chatlog_pipe_%d", time.Now().UnixNano())),
}
}
func (g *Glance) Read() ([]byte, error) {
if g.data != nil {
return g.data, nil
}
regions, err := GetVmmap(g.PID)
if err != nil {
return nil, err
}
g.MemRegions = MemRegionsFilter(regions)
if len(g.MemRegions) == 0 {
return nil, fmt.Errorf("no memory regions found")
}
region := g.MemRegions[0]
// 1. Create pipe file
if err := exec.Command("mkfifo", g.pipePath).Run(); err != nil {
return nil, fmt.Errorf("failed to create pipe file: %w", err)
}
defer os.Remove(g.pipePath)
// Start a goroutine to read from the pipe
dataCh := make(chan []byte, 1)
errCh := make(chan error, 1)
go func() {
// Open pipe for reading
file, err := os.OpenFile(g.pipePath, os.O_RDONLY, 0600)
if err != nil {
errCh <- fmt.Errorf("failed to open pipe for reading: %w", err)
return
}
defer file.Close()
// Read all data from pipe
data, err := io.ReadAll(file)
if err != nil {
errCh <- fmt.Errorf("failed to read from pipe: %w", err)
return
}
dataCh <- data
}()
// 2 & 3. Execute lldb command to read memory directly with all parameters
size := region.End - region.Start
lldbCmd := fmt.Sprintf("lldb -p %d -o \"memory read --binary --force --outfile %s --count %d 0x%x\" -o \"quit\"",
g.PID, g.pipePath, size, region.Start)
cmd := exec.Command("bash", "-c", lldbCmd)
// Set up stdout pipe for monitoring (optional)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
}
// Start the command
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start lldb: %w", err)
}
// Monitor lldb output (optional)
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
// Uncomment for debugging:
// fmt.Println(scanner.Text())
}
}()
// Wait for data with timeout
select {
case data := <-dataCh:
g.data = data
case err := <-errCh:
return nil, fmt.Errorf("failed to read memory: %w", err)
case <-time.After(30 * time.Second):
cmd.Process.Kill()
return nil, fmt.Errorf("timeout waiting for memory data")
}
// Wait for the command to finish
if err := cmd.Wait(); err != nil {
// We already have the data, so just log the error
fmt.Printf("Warning: lldb process exited with error: %v\n", err)
}
return g.data, nil
}

View File

@@ -0,0 +1,37 @@
package glance
import (
"os/exec"
"strings"
)
// IsSIPDisabled checks if System Integrity Protection (SIP) is disabled on macOS.
// Returns true if SIP is disabled, false if it's enabled or if the status cannot be determined.
func IsSIPDisabled() bool {
// Run the csrutil status command to check SIP status
cmd := exec.Command("csrutil", "status")
output, err := cmd.CombinedOutput()
if err != nil {
// If there's an error running the command, assume SIP is enabled
return false
}
// Convert output to string and check if SIP is disabled
outputStr := strings.ToLower(string(output))
// $ csrutil status
// System Integrity Protection status: disabled.
// If the output contains "disabled", SIP is disabled
if strings.Contains(outputStr, "system integrity protection status: disabled") {
return true
}
// Check for partial SIP disabling - some configurations might have specific protections disabled
if strings.Contains(outputStr, "disabled") && strings.Contains(outputStr, "debugging") {
return true
}
// By default, assume SIP is enabled
return false
}

View File

@@ -0,0 +1,158 @@
package glance
import (
"bufio"
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
)
const (
FilterRegionType = "MALLOC_NANO"
FilterSHRMOD = "SM=PRV"
CommandVmmap = "vmmap"
)
type MemRegion struct {
RegionType string
Start uint64
End uint64
VSize uint64 // Size in bytes
RSDNT uint64 // Resident memory size in bytes (new field)
SHRMOD string
Permissions string
RegionDetail string
}
func GetVmmap(pid uint32) ([]MemRegion, error) {
// Execute vmmap command
cmd := exec.Command(CommandVmmap, "-wide", fmt.Sprintf("%d", pid))
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("error executing vmmap command: %w", err)
}
// Parse the output using the existing LoadVmmap function
return LoadVmmap(string(output))
}
func LoadVmmap(output string) ([]MemRegion, error) {
var regions []MemRegion
scanner := bufio.NewScanner(strings.NewReader(output))
// Skip lines until we find the header
foundHeader := false
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "==== Writable regions for") {
foundHeader = true
// Skip the column headers line
scanner.Scan()
break
}
}
if !foundHeader {
return nil, nil // No vmmap data found
}
// Regular expression to parse the vmmap output lines
// Format: REGION TYPE START - END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL
// Updated regex to capture RSDNT value (second value in brackets)
re := regexp.MustCompile(`^(\S+)\s+([0-9a-f]+)-([0-9a-f]+)\s+\[\s*(\S+)\s+(\S+)(?:\s+\S+){2}\]\s+(\S+)\s+(\S+)(?:\s+\S+)?\s+(.*)$`)
// Parse each line
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
matches := re.FindStringSubmatch(line)
if len(matches) >= 9 { // Updated to check for at least 9 matches
// Parse start and end addresses
start, _ := strconv.ParseUint(matches[2], 16, 64)
end, _ := strconv.ParseUint(matches[3], 16, 64)
// Parse VSize as numeric value
vsize := parseSize(matches[4])
// Parse RSDNT as numeric value (new)
rsdnt := parseSize(matches[5])
region := MemRegion{
RegionType: strings.TrimSpace(matches[1]),
Start: start,
End: end,
VSize: vsize,
RSDNT: rsdnt, // Add the new RSDNT field
Permissions: matches[6], // Shifted index
SHRMOD: matches[7], // Shifted index
RegionDetail: strings.TrimSpace(matches[8]), // Shifted index
}
regions = append(regions, region)
}
}
return regions, nil
}
func MemRegionsFilter(regions []MemRegion) []MemRegion {
var filteredRegions []MemRegion
for _, region := range regions {
if region.RegionType == FilterRegionType {
filteredRegions = append(filteredRegions, region)
}
}
return filteredRegions
}
// parseSize converts size strings like "5616K" or "128.0M" to bytes (uint64)
func parseSize(sizeStr string) uint64 {
// Remove any whitespace
sizeStr = strings.TrimSpace(sizeStr)
// Define multipliers for different units
multipliers := map[string]uint64{
"B": 1,
"K": 1024,
"KB": 1024,
"M": 1024 * 1024,
"MB": 1024 * 1024,
"G": 1024 * 1024 * 1024,
"GB": 1024 * 1024 * 1024,
}
// Regular expression to match numbers with optional decimal point and unit
// This will match formats like: "5616K", "128.0M", "1.5G", etc.
re := regexp.MustCompile(`^(\d+(?:\.\d+)?)([KMGB]+)?$`)
matches := re.FindStringSubmatch(sizeStr)
if len(matches) < 2 {
return 0 // No match found
}
// Parse the numeric part (which may include a decimal point)
numStr := matches[1]
numVal, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0
}
// Determine the multiplier based on the unit
multiplier := uint64(1) // Default if no unit specified
if len(matches) >= 3 && matches[2] != "" {
unit := matches[2]
if m, ok := multipliers[unit]; ok {
multiplier = m
}
}
// Calculate final size in bytes (rounding to nearest integer)
return uint64(numVal*float64(multiplier) + 0.5)
}

View File

@@ -0,0 +1,187 @@
package darwin
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"runtime"
"sync"
"github.com/sirupsen/logrus"
"github.com/sjzar/chatlog/internal/wechat/decrypt"
"github.com/sjzar/chatlog/internal/wechat/key/darwin/glance"
"github.com/sjzar/chatlog/internal/wechat/model"
)
const (
MaxWorkersV3 = 8
)
type V3Extractor struct {
validator *decrypt.Validator
}
func NewV3Extractor() *V3Extractor {
return &V3Extractor{}
}
func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
if proc.Status == model.StatusOffline {
return "", fmt.Errorf("WeChat is offline")
}
// Check if SIP is disabled, as it's required for memory reading on macOS
if !glance.IsSIPDisabled() {
return "", fmt.Errorf("System Integrity Protection (SIP) is enabled, cannot read process memory")
}
if e.validator == nil {
return "", fmt.Errorf("validator not set")
}
// Create context to control all goroutines
searchCtx, cancel := context.WithCancel(ctx)
defer cancel()
// Create channels for memory data and results
memoryChannel := make(chan []byte, 100)
resultChannel := make(chan string, 1)
// Determine number of worker goroutines
workerCount := runtime.NumCPU()
if workerCount < 2 {
workerCount = 2
}
if workerCount > MaxWorkersV3 {
workerCount = MaxWorkersV3
}
logrus.Debug("Starting ", workerCount, " workers for V3 key search")
// Start consumer goroutines
var workerWaitGroup sync.WaitGroup
workerWaitGroup.Add(workerCount)
for index := 0; index < workerCount; index++ {
go func() {
defer workerWaitGroup.Done()
e.worker(searchCtx, memoryChannel, resultChannel)
}()
}
// Start producer goroutine
var producerWaitGroup sync.WaitGroup
producerWaitGroup.Add(1)
go func() {
defer producerWaitGroup.Done()
defer close(memoryChannel) // Close channel when producer is done
err := e.findMemory(searchCtx, uint32(proc.PID), memoryChannel)
if err != nil {
logrus.Error(err)
}
}()
// Wait for producer and consumers to complete
go func() {
producerWaitGroup.Wait()
workerWaitGroup.Wait()
close(resultChannel)
}()
// Wait for result
select {
case <-ctx.Done():
return "", ctx.Err()
case result, ok := <-resultChannel:
if ok && result != "" {
return result, nil
}
}
return "", fmt.Errorf("no valid key found")
}
// findMemory searches for memory regions using Glance
func (e *V3Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel chan<- []byte) error {
// Initialize a Glance instance to read process memory
g := glance.NewGlance(pid)
// Read memory data
memory, err := g.Read()
if err != nil {
return fmt.Errorf("failed to read process memory: %w", err)
}
logrus.Debug("Read memory region, size: ", len(memory), " bytes")
// Send memory data to channel for processing
select {
case memoryChannel <- memory:
logrus.Debug("Sent memory region for analysis")
case <-ctx.Done():
return ctx.Err()
}
return nil
}
// worker processes memory regions to find V3 version key
func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) {
keyPattern := []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32}
for {
select {
case <-ctx.Done():
return
case memory, ok := <-memoryChannel:
if !ok {
return
}
index := len(memory)
for {
select {
case <-ctx.Done():
return // Exit if context cancelled
default:
}
logrus.Debugf("Searching for V3 key in memory region, size: %d bytes", len(memory))
// Find pattern from end to beginning
index = bytes.LastIndex(memory[:index], keyPattern)
if index == -1 {
break // No more matches found
}
logrus.Debugf("Found potential V3 key pattern in memory region, index: %d", index)
// For V3, the key is 32 bytes and starts right after the pattern
if index+24+32 > len(memory) {
index -= 1
continue
}
// Extract the key data, which is right after the pattern and 32 bytes long
keyOffset := index + 24
keyData := memory[keyOffset : keyOffset+32]
// Validate key against database header
if e.validator.Validate(keyData) {
select {
case resultChannel <- hex.EncodeToString(keyData):
logrus.Debug("Valid key found for V3 database")
return
default:
}
}
index -= 1
}
}
}
}
func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
e.validator = validator
}

View File

@@ -0,0 +1,184 @@
package darwin
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"runtime"
"sync"
"github.com/sirupsen/logrus"
"github.com/sjzar/chatlog/internal/wechat/decrypt"
"github.com/sjzar/chatlog/internal/wechat/key/darwin/glance"
"github.com/sjzar/chatlog/internal/wechat/model"
)
const (
MaxWorkers = 8
)
type V4Extractor struct {
validator *decrypt.Validator
}
func NewV4Extractor() *V4Extractor {
return &V4Extractor{}
}
func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
if proc.Status == model.StatusOffline {
return "", fmt.Errorf("WeChat is offline")
}
// Check if SIP is disabled, as it's required for memory reading on macOS
if !glance.IsSIPDisabled() {
return "", fmt.Errorf("System Integrity Protection (SIP) is enabled, cannot read process memory")
}
if e.validator == nil {
return "", fmt.Errorf("validator not set")
}
// Create context to control all goroutines
searchCtx, cancel := context.WithCancel(ctx)
defer cancel()
// Create channels for memory data and results
memoryChannel := make(chan []byte, 100)
resultChannel := make(chan string, 1)
// Determine number of worker goroutines
workerCount := runtime.NumCPU()
if workerCount < 2 {
workerCount = 2
}
if workerCount > MaxWorkers {
workerCount = MaxWorkers
}
logrus.Debug("Starting ", workerCount, " workers for V4 key search")
// Start consumer goroutines
var workerWaitGroup sync.WaitGroup
workerWaitGroup.Add(workerCount)
for index := 0; index < workerCount; index++ {
go func() {
defer workerWaitGroup.Done()
e.worker(searchCtx, memoryChannel, resultChannel)
}()
}
// Start producer goroutine
var producerWaitGroup sync.WaitGroup
producerWaitGroup.Add(1)
go func() {
defer producerWaitGroup.Done()
defer close(memoryChannel) // Close channel when producer is done
err := e.findMemory(searchCtx, uint32(proc.PID), memoryChannel)
if err != nil {
logrus.Error(err)
}
}()
// Wait for producer and consumers to complete
go func() {
producerWaitGroup.Wait()
workerWaitGroup.Wait()
close(resultChannel)
}()
// Wait for result
select {
case <-ctx.Done():
return "", ctx.Err()
case result, ok := <-resultChannel:
if ok && result != "" {
return result, nil
}
}
return "", fmt.Errorf("no valid key found")
}
// findMemory searches for memory regions using Glance
func (e *V4Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel chan<- []byte) error {
// Initialize a Glance instance to read process memory
g := glance.NewGlance(pid)
// Read memory data
memory, err := g.Read()
if err != nil {
return fmt.Errorf("failed to read process memory: %w", err)
}
logrus.Debug("Read memory region, size: ", len(memory), " bytes")
// Send memory data to channel for processing
select {
case memoryChannel <- memory:
logrus.Debug("Sent memory region for analysis")
case <-ctx.Done():
return ctx.Err()
}
return nil
}
// worker processes memory regions to find V4 version key
func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) {
keyPattern := []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00}
for {
select {
case <-ctx.Done():
return
case memory, ok := <-memoryChannel:
if !ok {
return
}
index := len(memory)
for {
select {
case <-ctx.Done():
return // Exit if context cancelled
default:
}
// Find pattern from end to beginning
index = bytes.LastIndex(memory[:index], keyPattern)
if index == -1 {
break // No more matches found
}
// Check if we have enough space for the key
if index+16+32 > len(memory) {
index -= 1
continue
}
// Extract the key data, which is 16 bytes after the pattern and 32 bytes long
keyOffset := index + 16
keyData := memory[keyOffset : keyOffset+32]
// Validate key against database header
if e.validator.Validate(keyData) {
select {
case resultChannel <- hex.EncodeToString(keyData):
logrus.Debug("Valid key found for V4 database")
return
default:
}
}
index -= 1
}
}
}
}
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
e.validator = validator
}

View File

@@ -0,0 +1,41 @@
package key
import (
"context"
"fmt"
"github.com/sjzar/chatlog/internal/wechat/decrypt"
"github.com/sjzar/chatlog/internal/wechat/key/darwin"
"github.com/sjzar/chatlog/internal/wechat/key/windows"
"github.com/sjzar/chatlog/internal/wechat/model"
)
// 错误定义
var (
ErrInvalidVersion = fmt.Errorf("invalid version, must be 3 or 4")
ErrUnsupportedPlatform = fmt.Errorf("unsupported platform")
)
// Extractor 定义密钥提取器接口
type Extractor interface {
// Extract 从进程中提取密钥
Extract(ctx context.Context, proc *model.Process) (string, error)
SetValidate(validator *decrypt.Validator)
}
// NewExtractor 创建适合当前平台的密钥提取器
func NewExtractor(platform string, version int) (Extractor, error) {
switch {
case platform == "windows" && version == 3:
return windows.NewV3Extractor(), nil
case platform == "windows" && version == 4:
return windows.NewV4Extractor(), nil
case platform == "darwin" && version == 3:
return darwin.NewV3Extractor(), nil
case platform == "darwin" && version == 4:
return darwin.NewV4Extractor(), nil
default:
return nil, fmt.Errorf("%w: %s v%d", ErrUnsupportedPlatform, platform, version)
}
}

View File

@@ -0,0 +1,28 @@
package windows
import (
"errors"
"github.com/sjzar/chatlog/internal/wechat/decrypt"
)
// Common error definitions
var (
ErrWeChatOffline = errors.New("wechat is not logged in")
ErrOpenProcess = errors.New("failed to open process")
ErrCheckProcessBits = errors.New("failed to check process architecture")
ErrFindWeChatDLL = errors.New("WeChatWin.dll module not found")
ErrNoValidKey = errors.New("no valid key found")
)
type V3Extractor struct {
validator *decrypt.Validator
}
func NewV3Extractor() *V3Extractor {
return &V3Extractor{}
}
func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
e.validator = validator
}

View File

@@ -0,0 +1,13 @@
//go:build !windows
package windows
import (
"context"
"github.com/sjzar/chatlog/internal/wechat/model"
)
func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
return "", nil
}

View File

@@ -0,0 +1,254 @@
package windows
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"runtime"
"sync"
"unsafe"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
"github.com/sjzar/chatlog/internal/wechat/model"
"github.com/sjzar/chatlog/pkg/util"
)
const (
V3ModuleName = "WeChatWin.dll"
MaxWorkers = 16
)
func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
if proc.Status == model.StatusOffline {
return "", ErrWeChatOffline
}
// Open WeChat process
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, proc.PID)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrOpenProcess, err)
}
defer windows.CloseHandle(handle)
// Check process architecture
is64Bit, err := util.Is64Bit(handle)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrCheckProcessBits, err)
}
// Create context to control all goroutines
searchCtx, cancel := context.WithCancel(ctx)
defer cancel()
// Create channels for memory data and results
memoryChannel := make(chan []byte, 100)
resultChannel := make(chan string, 1)
// Determine number of worker goroutines
workerCount := runtime.NumCPU()
if workerCount < 2 {
workerCount = 2
}
if workerCount > MaxWorkers {
workerCount = MaxWorkers
}
logrus.Debug("Starting ", workerCount, " workers for V3 key search")
// Start consumer goroutines
var workerWaitGroup sync.WaitGroup
workerWaitGroup.Add(workerCount)
for index := 0; index < workerCount; index++ {
go func() {
defer workerWaitGroup.Done()
e.worker(searchCtx, handle, is64Bit, memoryChannel, resultChannel)
}()
}
// Start producer goroutine
var producerWaitGroup sync.WaitGroup
producerWaitGroup.Add(1)
go func() {
defer producerWaitGroup.Done()
defer close(memoryChannel) // Close channel when producer is done
err := e.findMemory(searchCtx, handle, proc.PID, memoryChannel)
if err != nil {
logrus.Error(err)
}
}()
// Wait for producer and consumers to complete
go func() {
producerWaitGroup.Wait()
workerWaitGroup.Wait()
close(resultChannel)
}()
// Wait for result
select {
case <-ctx.Done():
return "", ctx.Err()
case result, ok := <-resultChannel:
if ok && result != "" {
return result, nil
}
}
return "", ErrNoValidKey
}
// findMemoryV3 searches for writable memory regions in WeChatWin.dll for V3 version
func (e *V3Extractor) findMemory(ctx context.Context, handle windows.Handle, pid uint32, memoryChannel chan<- []byte) error {
// Find WeChatWin.dll module
module, isFound := FindModule(pid, V3ModuleName)
if !isFound {
return ErrFindWeChatDLL
}
logrus.Debug("Found WeChatWin.dll module at base address: 0x", fmt.Sprintf("%X", module.ModBaseAddr))
// Read writable memory regions
baseAddr := uintptr(module.ModBaseAddr)
endAddr := baseAddr + uintptr(module.ModBaseSize)
currentAddr := baseAddr
for currentAddr < endAddr {
var mbi windows.MemoryBasicInformation
err := windows.VirtualQueryEx(handle, currentAddr, &mbi, unsafe.Sizeof(mbi))
if err != nil {
break
}
// Skip small memory regions
if mbi.RegionSize < 100*1024 {
currentAddr += uintptr(mbi.RegionSize)
continue
}
// Check if memory region is writable
isWritable := (mbi.Protect & (windows.PAGE_READWRITE | windows.PAGE_WRITECOPY | windows.PAGE_EXECUTE_READWRITE | windows.PAGE_EXECUTE_WRITECOPY)) > 0
if isWritable && uint32(mbi.State) == windows.MEM_COMMIT {
// Calculate region size, ensure it doesn't exceed DLL bounds
regionSize := uintptr(mbi.RegionSize)
if currentAddr+regionSize > endAddr {
regionSize = endAddr - currentAddr
}
// Read writable memory region
memory := make([]byte, regionSize)
if err = windows.ReadProcessMemory(handle, currentAddr, &memory[0], regionSize, nil); err == nil {
select {
case memoryChannel <- memory:
logrus.Debug("Sent memory region for analysis, size: ", regionSize, " bytes")
case <-ctx.Done():
return nil
}
}
}
// Move to next memory region
currentAddr = uintptr(mbi.BaseAddress) + uintptr(mbi.RegionSize)
}
return nil
}
// workerV3 processes memory regions to find V3 version key
func (e *V3Extractor) worker(ctx context.Context, handle windows.Handle, is64Bit bool, memoryChannel <-chan []byte, resultChannel chan<- string) {
// Define search pattern
keyPattern := []byte{0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
ptrSize := 8
littleEndianFunc := binary.LittleEndian.Uint64
// Adjust for 32-bit process
if !is64Bit {
keyPattern = keyPattern[:4]
ptrSize = 4
littleEndianFunc = func(b []byte) uint64 { return uint64(binary.LittleEndian.Uint32(b)) }
}
for {
select {
case <-ctx.Done():
return
case memory, ok := <-memoryChannel:
if !ok {
return
}
index := len(memory)
for {
select {
case <-ctx.Done():
return // Exit if context cancelled
default:
}
// Find pattern from end to beginning
index = bytes.LastIndex(memory[:index], keyPattern)
if index == -1 || index-ptrSize < 0 {
break
}
// Extract and validate pointer value
ptrValue := littleEndianFunc(memory[index-ptrSize : index])
if ptrValue > 0x10000 && ptrValue < 0x7FFFFFFFFFFF {
if key := e.validateKey(handle, ptrValue); key != "" {
select {
case resultChannel <- key:
logrus.Debug("Valid key found for V3 database")
return
default:
}
}
}
index -= 1 // Continue searching from previous position
}
}
}
}
// validateKey validates a single key candidate
func (e *V3Extractor) validateKey(handle windows.Handle, addr uint64) string {
keyData := make([]byte, 0x20) // 32-byte key
if err := windows.ReadProcessMemory(handle, uintptr(addr), &keyData[0], uintptr(len(keyData)), nil); err != nil {
return ""
}
// Validate key against database header
if e.validator.Validate(keyData) {
return hex.EncodeToString(keyData)
}
return ""
}
// FindModule searches for a specified module in the process
func FindModule(pid uint32, name string) (module windows.ModuleEntry32, isFound bool) {
// Create module snapshot
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, pid)
if err != nil {
logrus.Debug("Failed to create module snapshot: ", err)
return module, false
}
defer windows.CloseHandle(snapshot)
// Initialize module entry structure
module.Size = uint32(windows.SizeofModuleEntry32)
// Get the first module
if err := windows.Module32First(snapshot, &module); err != nil {
logrus.Debug("Failed to get first module: ", err)
return module, false
}
// Iterate through all modules to find WeChatWin.dll
for ; err == nil; err = windows.Module32Next(snapshot, &module) {
if windows.UTF16ToString(module.Module[:]) == name {
return module, true
}
}
return module, false
}

View File

@@ -0,0 +1,17 @@
package windows
import (
"github.com/sjzar/chatlog/internal/wechat/decrypt"
)
type V4Extractor struct {
validator *decrypt.Validator
}
func NewV4Extractor() *V4Extractor {
return &V4Extractor{}
}
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
e.validator = validator
}

View File

@@ -0,0 +1,13 @@
//go:build !windows
package windows
import (
"context"
"github.com/sjzar/chatlog/internal/wechat/model"
)
func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
return "", nil
}

View File

@@ -0,0 +1,213 @@
package windows
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"runtime"
"sync"
"unsafe"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
"github.com/sjzar/chatlog/internal/wechat/model"
)
const (
MEM_PRIVATE = 0x20000
)
func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
if proc.Status == model.StatusOffline {
return "", ErrWeChatOffline
}
// Open process handle
handle, err := windows.OpenProcess(windows.PROCESS_VM_READ|windows.PROCESS_QUERY_INFORMATION, false, proc.PID)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrOpenProcess, err)
}
defer windows.CloseHandle(handle)
// Create context to control all goroutines
searchCtx, cancel := context.WithCancel(ctx)
defer cancel()
// Create channels for memory data and results
memoryChannel := make(chan []byte, 100)
resultChannel := make(chan string, 1)
// Determine number of worker goroutines
workerCount := runtime.NumCPU()
if workerCount < 2 {
workerCount = 2
}
if workerCount > MaxWorkers {
workerCount = MaxWorkers
}
logrus.Debug("Starting ", workerCount, " workers for V4 key search")
// Start consumer goroutines
var workerWaitGroup sync.WaitGroup
workerWaitGroup.Add(workerCount)
for index := 0; index < workerCount; index++ {
go func() {
defer workerWaitGroup.Done()
e.worker(searchCtx, handle, memoryChannel, resultChannel)
}()
}
// Start producer goroutine
var producerWaitGroup sync.WaitGroup
producerWaitGroup.Add(1)
go func() {
defer producerWaitGroup.Done()
defer close(memoryChannel) // Close channel when producer is done
err := e.findMemory(searchCtx, handle, memoryChannel)
if err != nil {
logrus.Error(err)
}
}()
// Wait for producer and consumers to complete
go func() {
producerWaitGroup.Wait()
workerWaitGroup.Wait()
close(resultChannel)
}()
// Wait for result
select {
case <-ctx.Done():
return "", ctx.Err()
case result, ok := <-resultChannel:
if ok && result != "" {
return result, nil
}
}
return "", ErrNoValidKey
}
// findMemoryV4 searches for writable memory regions for V4 version
func (e *V4Extractor) findMemory(ctx context.Context, handle windows.Handle, memoryChannel chan<- []byte) error {
// Define search range
minAddr := uintptr(0x10000) // Process space usually starts from 0x10000
maxAddr := uintptr(0x7FFFFFFF) // 32-bit process space limit
if runtime.GOARCH == "amd64" {
maxAddr = uintptr(0x7FFFFFFFFFFF) // 64-bit process space limit
}
logrus.Debug("Scanning memory regions from 0x", fmt.Sprintf("%X", minAddr), " to 0x", fmt.Sprintf("%X", maxAddr))
currentAddr := minAddr
for currentAddr < maxAddr {
var memInfo windows.MemoryBasicInformation
err := windows.VirtualQueryEx(handle, currentAddr, &memInfo, unsafe.Sizeof(memInfo))
if err != nil {
break
}
// Skip small memory regions
if memInfo.RegionSize < 1024*1024 {
currentAddr += uintptr(memInfo.RegionSize)
continue
}
// Check if memory region is readable and private
if memInfo.State == windows.MEM_COMMIT && (memInfo.Protect&windows.PAGE_READWRITE) != 0 && memInfo.Type == MEM_PRIVATE {
// Calculate region size, ensure it doesn't exceed limit
regionSize := uintptr(memInfo.RegionSize)
if currentAddr+regionSize > maxAddr {
regionSize = maxAddr - currentAddr
}
// Read memory region
memory := make([]byte, regionSize)
if err = windows.ReadProcessMemory(handle, currentAddr, &memory[0], regionSize, nil); err == nil {
select {
case memoryChannel <- memory:
logrus.Debug("Sent memory region for analysis, size: ", regionSize, " bytes")
case <-ctx.Done():
return nil
}
}
}
// Move to next memory region
currentAddr = uintptr(memInfo.BaseAddress) + uintptr(memInfo.RegionSize)
}
return nil
}
// workerV4 processes memory regions to find V4 version key
func (e *V4Extractor) worker(ctx context.Context, handle windows.Handle, memoryChannel <-chan []byte, resultChannel chan<- string) {
// Define search pattern for V4
keyPattern := []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
ptrSize := 8
littleEndianFunc := binary.LittleEndian.Uint64
for {
select {
case <-ctx.Done():
return
case memory, ok := <-memoryChannel:
if !ok {
return
}
index := len(memory)
for {
select {
case <-ctx.Done():
return // Exit if context cancelled
default:
}
// Find pattern from end to beginning
index = bytes.LastIndex(memory[:index], keyPattern)
if index == -1 || index-ptrSize < 0 {
break
}
// Extract and validate pointer value
ptrValue := littleEndianFunc(memory[index-ptrSize : index])
if ptrValue > 0x10000 && ptrValue < 0x7FFFFFFFFFFF {
if key := e.validateKey(handle, ptrValue); key != "" {
select {
case resultChannel <- key:
logrus.Debug("Valid key found for V4 database")
return
default:
}
}
}
index -= 1 // Continue searching from previous position
}
}
}
}
// validateKey validates a single key candidate
func (e *V4Extractor) validateKey(handle windows.Handle, addr uint64) string {
keyData := make([]byte, 0x20) // 32-byte key
if err := windows.ReadProcessMemory(handle, uintptr(addr), &keyData[0], uintptr(len(keyData)), nil); err != nil {
return ""
}
// Validate key against database header
if e.validator.Validate(keyData) {
return hex.EncodeToString(keyData)
}
return ""
}

View File

@@ -1,57 +1,110 @@
package wechat
import (
"strings"
"context"
"fmt"
"runtime"
"github.com/shirou/gopsutil/v4/process"
log "github.com/sirupsen/logrus"
"github.com/sjzar/chatlog/internal/wechat/model"
"github.com/sjzar/chatlog/internal/wechat/process"
)
const (
V3ProcessName = "WeChat"
V4ProcessName = "Weixin"
)
var DefaultManager *Manager
var (
Items []*Info
ItemMap map[string]*Info
)
func init() {
DefaultManager = NewManager()
DefaultManager.Load()
}
func Load() {
Items = make([]*Info, 0, 2)
ItemMap = make(map[string]*Info)
func Load() error {
return DefaultManager.Load()
}
processes, err := process.Processes()
if err != nil {
log.Println("获取进程列表失败:", err)
return
}
func GetAccount(name string) (*Account, error) {
return DefaultManager.GetAccount(name)
}
for _, p := range processes {
name, err := p.Name()
name = strings.TrimSuffix(name, ".exe")
if err != nil || name != V3ProcessName && name != V4ProcessName {
continue
}
func GetProcess(name string) (*model.Process, error) {
return DefaultManager.GetProcess(name)
}
// v4 存在同名进程,需要继续判断 cmdline
if name == V4ProcessName {
cmdline, err := p.Cmdline()
if err != nil {
log.Error(err)
continue
}
if strings.Contains(cmdline, "--") {
continue
}
}
func GetAccounts() []*Account {
return DefaultManager.GetAccounts()
}
info, err := NewInfo(p)
if err != nil {
continue
}
// Manager 微信管理器
type Manager struct {
detector process.Detector
accounts []*Account
processMap map[string]*model.Process
}
Items = append(Items, info)
ItemMap[info.AccountName] = info
// NewManager 创建新的微信管理器
func NewManager() *Manager {
return &Manager{
detector: process.NewDetector(runtime.GOOS),
accounts: make([]*Account, 0),
processMap: make(map[string]*model.Process),
}
}
// Load 加载微信进程信息
func (m *Manager) Load() error {
// 查找微信进程
processes, err := m.detector.FindProcesses()
if err != nil {
return err
}
// 转换为账号信息
accounts := make([]*Account, 0, len(processes))
processMap := make(map[string]*model.Process, len(processes))
for _, p := range processes {
account := NewAccount(p)
accounts = append(accounts, account)
if account.Name != "" {
processMap[account.Name] = p
}
}
m.accounts = accounts
m.processMap = processMap
return nil
}
// GetAccount 获取指定名称的账号
func (m *Manager) GetAccount(name string) (*Account, error) {
p, err := m.GetProcess(name)
if err != nil {
return nil, err
}
return NewAccount(p), nil
}
func (m *Manager) GetProcess(name string) (*model.Process, error) {
p, ok := m.processMap[name]
if !ok {
return nil, fmt.Errorf("account not found: %s", name)
}
return p, nil
}
// GetAccounts 获取所有账号
func (m *Manager) GetAccounts() []*Account {
return m.accounts
}
// DecryptDatabase 便捷方法:通过账号名解密数据库
func (m *Manager) DecryptDatabase(ctx context.Context, accountName, dbPath, outputPath string) error {
// 获取账号
account, err := m.GetAccount(accountName)
if err != nil {
return err
}
// 使用账号解密数据库
return account.DecryptDatabase(ctx, dbPath, outputPath)
}

View File

@@ -0,0 +1,24 @@
package model
type Process struct {
PID uint32
ExePath string
Platform string
Version int
FullVersion string
Status string
DataDir string
AccountName string
}
// 平台常量定义
const (
PlatformWindows = "windows"
PlatformMacOS = "darwin"
)
const (
StatusInit = ""
StatusOffline = "offline"
StatusOnline = "online"
)

View File

@@ -0,0 +1,164 @@
package darwin
import (
"fmt"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/shirou/gopsutil/v4/process"
log "github.com/sirupsen/logrus"
"github.com/sjzar/chatlog/internal/wechat/model"
"github.com/sjzar/chatlog/pkg/appver"
)
const (
V3ProcessName = "WeChat"
V4ProcessName = "Weixin"
V3DBFile = "Message/msg_0.db"
V4DBFile = "db_storage/message/message_0.db"
)
// Detector 实现 macOS 平台的进程检测器
type Detector struct{}
// NewDetector 创建一个新的 macOS 检测器
func NewDetector() *Detector {
return &Detector{}
}
// FindProcesses 查找所有微信进程并返回它们的信息
func (d *Detector) FindProcesses() ([]*model.Process, error) {
processes, err := process.Processes()
if err != nil {
log.Errorf("获取进程列表失败: %v", err)
return nil, err
}
var result []*model.Process
for _, p := range processes {
name, err := p.Name()
if err != nil || (name != V3ProcessName && name != V4ProcessName) {
continue
}
// 获取进程信息
procInfo, err := d.getProcessInfo(p)
if err != nil {
log.Errorf("获取进程 %d 的信息失败: %v", p.Pid, err)
continue
}
result = append(result, procInfo)
}
return result, nil
}
// getProcessInfo 获取微信进程的详细信息
func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
procInfo := &model.Process{
PID: uint32(p.Pid),
Status: model.StatusOffline,
Platform: model.PlatformMacOS,
}
// 获取可执行文件路径
exePath, err := p.Exe()
if err != nil {
log.Error(err)
return nil, err
}
procInfo.ExePath = exePath
// 获取版本信息
// 注意macOS 的版本获取方式可能与 Windows 不同
versionInfo, err := appver.New(exePath)
if err != nil {
log.Error(err)
procInfo.Version = 3
procInfo.FullVersion = "3.0.0"
} else {
procInfo.Version = versionInfo.Version
procInfo.FullVersion = versionInfo.FullVersion
}
// 初始化附加信息(数据目录、账户名)
if err := d.initializeProcessInfo(p, procInfo); err != nil {
log.Errorf("初始化进程信息失败: %v", err)
// 即使初始化失败也返回部分信息
}
return procInfo, nil
}
// initializeProcessInfo 获取进程的数据目录和账户名
func (d *Detector) initializeProcessInfo(p *process.Process, info *model.Process) error {
// 使用 lsof 命令获取进程打开的文件
files, err := d.getOpenFiles(int(p.Pid))
if err != nil {
log.Error("获取打开文件列表失败: ", err)
return err
}
dbPath := V3DBFile
if info.Version == 4 {
dbPath = V4DBFile
}
for _, filePath := range files {
if strings.Contains(filePath, dbPath) {
parts := strings.Split(filePath, string(filepath.Separator))
if len(parts) < 4 {
log.Debug("无效的文件路径格式: " + filePath)
continue
}
// v3:
// /Users/sarv/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/<id>/Message/msg_0.db
// v4:
// /Users/sarv/Library/Containers/com.tencent.xWeChat/Data/Documents/xwechat_files/<id>/db_storage/message/message_0.db
info.Status = model.StatusOnline
if info.Version == 4 {
info.DataDir = strings.Join(parts[:len(parts)-3], string(filepath.Separator))
info.AccountName = parts[len(parts)-4]
} else {
info.DataDir = strings.Join(parts[:len(parts)-2], string(filepath.Separator))
info.AccountName = parts[len(parts)-3]
}
return nil
}
}
return nil
}
// getOpenFiles 使用 lsof 命令获取进程打开的文件列表
func (d *Detector) getOpenFiles(pid int) ([]string, error) {
// 执行 lsof -p <pid> 命令,使用 -F n 选项只输出文件名
cmd := exec.Command("lsof", "-p", strconv.Itoa(pid), "-F", "n")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("执行 lsof 命令失败: %v", err)
}
// 解析 lsof -F n 输出
// 格式为: n/path/to/file
lines := strings.Split(string(output), "\n")
var files []string
for _, line := range lines {
if strings.HasPrefix(line, "n") {
// 移除前缀 'n' 获取文件路径
filePath := line[1:]
if filePath != "" {
files = append(files, filePath)
}
}
}
return files, nil
}

View File

@@ -0,0 +1,36 @@
package process
import (
"github.com/sjzar/chatlog/internal/wechat/model"
"github.com/sjzar/chatlog/internal/wechat/process/darwin"
"github.com/sjzar/chatlog/internal/wechat/process/windows"
)
type Detector interface {
FindProcesses() ([]*model.Process, error)
}
// NewDetector 创建适合当前平台的检测器
func NewDetector(platform string) Detector {
// 根据平台返回对应的实现
switch platform {
case "windows":
return windows.NewDetector()
case "darwin":
return darwin.NewDetector()
default:
// 默认返回一个空实现
return &nullDetector{}
}
}
// nullDetector 空实现
type nullDetector struct{}
func (d *nullDetector) FindProcesses() ([]*model.Process, error) {
return nil, nil
}
func (d *nullDetector) GetProcessInfo(pid uint32) (*model.Process, error) {
return nil, nil
}

View File

@@ -0,0 +1,101 @@
package windows
import (
"strings"
"github.com/shirou/gopsutil/v4/process"
log "github.com/sirupsen/logrus"
"github.com/sjzar/chatlog/internal/wechat/model"
"github.com/sjzar/chatlog/pkg/appver"
)
const (
V3ProcessName = "WeChat"
V4ProcessName = "Weixin"
V3DBFile = "Msg\\Misc.db"
V4DBFile = "db_storage\\message\\message_0.db"
)
// Detector 实现 Windows 平台的进程检测器
type Detector struct{}
// NewDetector 创建一个新的 Windows 检测器
func NewDetector() *Detector {
return &Detector{}
}
// FindProcesses 查找所有微信进程并返回它们的信息
func (d *Detector) FindProcesses() ([]*model.Process, error) {
processes, err := process.Processes()
if err != nil {
log.Errorf("获取进程列表失败: %v", err)
return nil, err
}
var result []*model.Process
for _, p := range processes {
name, err := p.Name()
name = strings.TrimSuffix(name, ".exe")
if err != nil || (name != V3ProcessName && name != V4ProcessName) {
continue
}
// v4 存在同名进程,需要继续判断 cmdline
if name == V4ProcessName {
cmdline, err := p.Cmdline()
if err != nil {
log.Error(err)
continue
}
if strings.Contains(cmdline, "--") {
continue
}
}
// 获取进程信息
procInfo, err := d.getProcessInfo(p)
if err != nil {
log.Errorf("获取进程 %d 的信息失败: %v", p.Pid, err)
continue
}
result = append(result, procInfo)
}
return result, nil
}
// getProcessInfo 获取微信进程的详细信息
func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
procInfo := &model.Process{
PID: uint32(p.Pid),
Status: model.StatusOffline,
Platform: model.PlatformWindows,
}
// 获取可执行文件路径
exePath, err := p.Exe()
if err != nil {
log.Error(err)
return nil, err
}
procInfo.ExePath = exePath
// 获取版本信息
versionInfo, err := appver.New(exePath)
if err != nil {
log.Error(err)
return nil, err
}
procInfo.Version = versionInfo.Version
procInfo.FullVersion = versionInfo.FullVersion
// 初始化附加信息(数据目录、账户名)
if err := initializeProcessInfo(p, procInfo); err != nil {
log.Errorf("初始化进程信息失败: %v", err)
// 即使初始化失败也返回部分信息
}
return procInfo, nil
}

View File

@@ -0,0 +1,12 @@
//go:build !windows
package windows
import (
"github.com/shirou/gopsutil/v4/process"
"github.com/sjzar/chatlog/internal/wechat/model"
)
func initializeProcessInfo(p *process.Process, info *model.Process) error {
return nil
}

View File

@@ -0,0 +1,48 @@
package windows
import (
"path/filepath"
"strings"
"github.com/shirou/gopsutil/v4/process"
log "github.com/sirupsen/logrus"
"github.com/sjzar/chatlog/internal/wechat/model"
)
// initializeProcessInfo 获取进程的数据目录和账户名
func initializeProcessInfo(p *process.Process, info *model.Process) error {
files, err := p.OpenFiles()
if err != nil {
log.Error("获取打开文件列表失败: ", err)
return err
}
dbPath := V3DBFile
if info.Version == 4 {
dbPath = V4DBFile
}
for _, f := range files {
if strings.HasSuffix(f.Path, dbPath) {
filePath := f.Path[4:] // 移除 "\\?\" 前缀
parts := strings.Split(filePath, string(filepath.Separator))
if len(parts) < 4 {
log.Debug("无效的文件路径格式: " + filePath)
continue
}
info.Status = model.StatusOnline
if info.Version == 4 {
info.DataDir = strings.Join(parts[:len(parts)-3], string(filepath.Separator))
info.AccountName = parts[len(parts)-4]
} else {
info.DataDir = strings.Join(parts[:len(parts)-2], string(filepath.Separator))
info.AccountName = parts[len(parts)-3]
}
return nil
}
}
return nil
}

134
internal/wechat/wechat.go Normal file
View File

@@ -0,0 +1,134 @@
package wechat
import (
"context"
"fmt"
"os"
"github.com/sjzar/chatlog/internal/wechat/decrypt"
"github.com/sjzar/chatlog/internal/wechat/key"
"github.com/sjzar/chatlog/internal/wechat/model"
)
// Account 表示一个微信账号
type Account struct {
Name string
Platform string
Version int
FullVersion string
DataDir string
Key string
PID uint32
ExePath string
Status string
}
// NewAccount 创建新的账号对象
func NewAccount(proc *model.Process) *Account {
return &Account{
Name: proc.AccountName,
Platform: proc.Platform,
Version: proc.Version,
FullVersion: proc.FullVersion,
DataDir: proc.DataDir,
PID: proc.PID,
ExePath: proc.ExePath,
Status: proc.Status,
}
}
// RefreshStatus 刷新账号的进程状态
func (a *Account) RefreshStatus() error {
// 查找所有微信进程
Load()
process, err := GetProcess(a.Name)
if err != nil {
a.Status = model.StatusOffline
return nil
}
if process.AccountName == a.Name {
// 更新进程信息
a.PID = process.PID
a.ExePath = process.ExePath
a.Platform = process.Platform
a.Version = process.Version
a.FullVersion = process.FullVersion
a.Status = process.Status
a.DataDir = process.DataDir
}
return nil
}
// GetKey 获取账号的密钥
func (a *Account) GetKey(ctx context.Context) (string, error) {
// 如果已经有密钥,直接返回
if a.Key != "" {
return a.Key, nil
}
// 刷新进程状态
if err := a.RefreshStatus(); err != nil {
return "", fmt.Errorf("failed to refresh process status: %w", err)
}
// 检查账号状态
if a.Status != model.StatusOnline {
return "", fmt.Errorf("account %s is not online", a.Name)
}
// 创建密钥提取器 - 使用新的接口,传入平台和版本信息
extractor, err := key.NewExtractor(a.Platform, a.Version)
if err != nil {
return "", fmt.Errorf("failed to create key extractor: %w", err)
}
process, err := GetProcess(a.Name)
if err != nil {
return "", fmt.Errorf("failed to get process: %w", err)
}
validator, err := decrypt.NewValidator(process.DataDir, process.Platform, process.Version)
if err != nil {
return "", fmt.Errorf("failed to create validator: %w", err)
}
extractor.SetValidate(validator)
// 提取密钥
key, err := extractor.Extract(ctx, process)
if err != nil {
return "", err
}
// 保存密钥
a.Key = key
return key, nil
}
// DecryptDatabase 解密数据库
func (a *Account) DecryptDatabase(ctx context.Context, dbPath, outputPath string) error {
// 获取密钥
hexKey, err := a.GetKey(ctx)
if err != nil {
return err
}
// 创建解密器 - 传入平台信息和版本
decryptor, err := decrypt.NewDecryptor(a.Platform, a.Version)
if err != nil {
return err
}
// 创建输出文件
output, err := os.Create(outputPath)
if err != nil {
return err
}
defer output.Close()
// 解密数据库
return decryptor.Decrypt(ctx, dbPath, hexKey, output)
}