Files
chatlog/internal/wechat/info_windows.go
Shen Junzheng 78cce92ce3 x
2025-03-19 13:11:09 +08:00

508 lines
14 KiB
Go

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
}