x
This commit is contained in:
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user