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

View File

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

View File

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

View File

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

View File

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

View File

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