x
This commit is contained in:
415
internal/wechat/decrypt.go
Normal file
415
internal/wechat/decrypt.go
Normal file
@@ -0,0 +1,415 @@
|
||||
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)
|
||||
}
|
||||
50
internal/wechat/info.go
Normal file
50
internal/wechat/info.go
Normal file
@@ -0,0 +1,50 @@
|
||||
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
|
||||
}
|
||||
16
internal/wechat/info_others.go
Normal file
16
internal/wechat/info_others.go
Normal file
@@ -0,0 +1,16 @@
|
||||
//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
|
||||
}
|
||||
507
internal/wechat/info_windows.go
Normal file
507
internal/wechat/info_windows.go
Normal file
@@ -0,0 +1,507 @@
|
||||
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
|
||||
}
|
||||
57
internal/wechat/manager.go
Normal file
57
internal/wechat/manager.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
V3ProcessName = "WeChat"
|
||||
V4ProcessName = "Weixin"
|
||||
)
|
||||
|
||||
var (
|
||||
Items []*Info
|
||||
ItemMap map[string]*Info
|
||||
)
|
||||
|
||||
func Load() {
|
||||
Items = make([]*Info, 0, 2)
|
||||
ItemMap = make(map[string]*Info)
|
||||
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
log.Println("获取进程列表失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
info, err := NewInfo(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
Items = append(Items, info)
|
||||
ItemMap[info.AccountName] = info
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user