x
This commit is contained in:
@@ -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)
|
||||
}
|
||||
138
internal/wechat/decrypt/common/common.go
Normal file
138
internal/wechat/decrypt/common/common.go
Normal 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
|
||||
}
|
||||
184
internal/wechat/decrypt/darwin/v3.go
Normal file
184
internal/wechat/decrypt/darwin/v3.go
Normal 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
|
||||
}
|
||||
194
internal/wechat/decrypt/darwin/v4.go
Normal file
194
internal/wechat/decrypt/darwin/v4.go
Normal 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
|
||||
}
|
||||
55
internal/wechat/decrypt/decryptor.go
Normal file
55
internal/wechat/decrypt/decryptor.go
Normal 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)
|
||||
}
|
||||
}
|
||||
56
internal/wechat/decrypt/validator.go
Normal file
56
internal/wechat/decrypt/validator.go
Normal 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 ""
|
||||
|
||||
}
|
||||
192
internal/wechat/decrypt/windows/v3.go
Normal file
192
internal/wechat/decrypt/windows/v3.go
Normal 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
|
||||
}
|
||||
190
internal/wechat/decrypt/windows/v4.go
Normal file
190
internal/wechat/decrypt/windows/v4.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
118
internal/wechat/key/darwin/glance/glance.go
Normal file
118
internal/wechat/key/darwin/glance/glance.go
Normal 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
|
||||
}
|
||||
37
internal/wechat/key/darwin/glance/sip.go
Normal file
37
internal/wechat/key/darwin/glance/sip.go
Normal 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
|
||||
}
|
||||
158
internal/wechat/key/darwin/glance/vmmap.go
Normal file
158
internal/wechat/key/darwin/glance/vmmap.go
Normal 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)
|
||||
}
|
||||
187
internal/wechat/key/darwin/v3.go
Normal file
187
internal/wechat/key/darwin/v3.go
Normal 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
|
||||
}
|
||||
184
internal/wechat/key/darwin/v4.go
Normal file
184
internal/wechat/key/darwin/v4.go
Normal 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
|
||||
}
|
||||
41
internal/wechat/key/extractor.go
Normal file
41
internal/wechat/key/extractor.go
Normal 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)
|
||||
}
|
||||
}
|
||||
28
internal/wechat/key/windows/v3.go
Normal file
28
internal/wechat/key/windows/v3.go
Normal 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
|
||||
}
|
||||
13
internal/wechat/key/windows/v3_others.go
Normal file
13
internal/wechat/key/windows/v3_others.go
Normal 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
|
||||
}
|
||||
254
internal/wechat/key/windows/v3_windows.go
Normal file
254
internal/wechat/key/windows/v3_windows.go
Normal 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
|
||||
}
|
||||
17
internal/wechat/key/windows/v4.go
Normal file
17
internal/wechat/key/windows/v4.go
Normal 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
|
||||
}
|
||||
13
internal/wechat/key/windows/v4_others.go
Normal file
13
internal/wechat/key/windows/v4_others.go
Normal 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
|
||||
}
|
||||
213
internal/wechat/key/windows/v4_windows.go
Normal file
213
internal/wechat/key/windows/v4_windows.go
Normal 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 ""
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
24
internal/wechat/model/process.go
Normal file
24
internal/wechat/model/process.go
Normal 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"
|
||||
)
|
||||
164
internal/wechat/process/darwin/detector.go
Normal file
164
internal/wechat/process/darwin/detector.go
Normal 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
|
||||
}
|
||||
36
internal/wechat/process/detector.go
Normal file
36
internal/wechat/process/detector.go
Normal 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
|
||||
}
|
||||
101
internal/wechat/process/windows/detector.go
Normal file
101
internal/wechat/process/windows/detector.go
Normal 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
|
||||
}
|
||||
12
internal/wechat/process/windows/detector_others.go
Normal file
12
internal/wechat/process/windows/detector_others.go
Normal 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
|
||||
}
|
||||
48
internal/wechat/process/windows/detector_windows.go
Normal file
48
internal/wechat/process/windows/detector_windows.go
Normal 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
134
internal/wechat/wechat.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user