4 Commits

Author SHA1 Message Date
Sarv
ba3563ad4e fix v4 chatroom display name (#26) 2025-04-10 16:25:09 +08:00
Sarv
4983d27054 support multi key pattern matching (#25) 2025-04-10 14:53:17 +08:00
Shen Junzheng
b64902ecb6 x 2025-04-09 21:46:52 +08:00
Sarv
dc116c50bf dump memory command (#23) 2025-04-09 21:18:56 +08:00
13 changed files with 324 additions and 90 deletions

1
.gitignore vendored
View File

@@ -27,6 +27,5 @@ go.work.sum
# syncthing files # syncthing files
.stfolder .stfolder
chatlog
chatlog.exe# Added by goreleaser init: chatlog.exe# Added by goreleaser init:
dist/ dist/

View File

@@ -0,0 +1,146 @@
package chatlog
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"time"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/sjzar/chatlog/internal/wechat"
"github.com/sjzar/chatlog/internal/wechat/key/darwin/glance"
)
func init() {
rootCmd.AddCommand(dumpmemoryCmd)
}
var dumpmemoryCmd = &cobra.Command{
Use: "dumpmemory",
Short: "dump memory",
Run: func(cmd *cobra.Command, args []string) {
if runtime.GOOS != "darwin" {
log.Info().Msg("dump memory only support macOS")
}
session := time.Now().Format("20060102150405")
dir, err := os.Getwd()
if err != nil {
log.Fatal().Err(err).Msg("get current directory failed")
return
}
log.Info().Msgf("current directory: %s", dir)
// step 1. check pid
if err = wechat.Load(); err != nil {
log.Fatal().Err(err).Msg("load wechat failed")
return
}
accounts := wechat.GetAccounts()
if len(accounts) == 0 {
log.Fatal().Msg("no wechat account found")
return
}
log.Info().Msgf("found %d wechat account", len(accounts))
for i, a := range accounts {
log.Info().Msgf("%d. %s %d %s", i, a.FullVersion, a.PID, a.DataDir)
}
// step 2. dump memory
account := accounts[0]
file := fmt.Sprintf("wechat_%s_%d_%s.bin", account.FullVersion, account.PID, session)
path := filepath.Join(dir, file)
log.Info().Msgf("dumping memory to %s", path)
g := glance.NewGlance(account.PID)
b, err := g.Read()
if err != nil {
log.Fatal().Err(err).Msg("read memory failed")
return
}
if err = os.WriteFile(path, b, 0644); err != nil {
log.Fatal().Err(err).Msg("write memory failed")
return
}
log.Info().Msg("dump memory success")
// step 3. copy encrypted database file
dbFile := "db_storage/session/session.db"
if account.Version == 3 {
dbFile = "Session/session_new.db"
}
from := filepath.Join(account.DataDir, dbFile)
to := filepath.Join(dir, fmt.Sprintf("wechat_%s_%d_session.db", account.FullVersion, account.PID))
log.Info().Msgf("copying %s to %s", from, to)
b, err = os.ReadFile(from)
if err != nil {
log.Fatal().Err(err).Msg("read session.db failed")
return
}
if err = os.WriteFile(to, b, 0644); err != nil {
log.Fatal().Err(err).Msg("write session.db failed")
return
}
log.Info().Msg("copy session.db success")
// step 4. package
zipFile := fmt.Sprintf("wechat_%s_%d_%s.zip", account.FullVersion, account.PID, session)
zipPath := filepath.Join(dir, zipFile)
log.Info().Msgf("packaging to %s", zipPath)
zf, err := os.Create(zipPath)
if err != nil {
log.Fatal().Err(err).Msg("create zip file failed")
return
}
defer zf.Close()
zw := zip.NewWriter(zf)
for _, file := range []string{file, to} {
f, err := os.Open(file)
if err != nil {
log.Fatal().Err(err).Msg("open file failed")
return
}
defer f.Close()
info, err := f.Stat()
if err != nil {
log.Fatal().Err(err).Msg("get file info failed")
return
}
header, err := zip.FileInfoHeader(info)
if err != nil {
log.Fatal().Err(err).Msg("create zip file info header failed")
return
}
header.Name = filepath.Base(file)
header.Method = zip.Deflate
writer, err := zw.CreateHeader(header)
if err != nil {
log.Fatal().Err(err).Msg("create zip file header failed")
return
}
if _, err = io.Copy(writer, f); err != nil {
log.Fatal().Err(err).Msg("copy file to zip failed")
return
}
}
if err = zw.Close(); err != nil {
log.Fatal().Err(err).Msg("close zip writer failed")
return
}
log.Info().Msgf("package success, please send %s to developer", zipPath)
},
}

View File

@@ -22,6 +22,8 @@ func initLog(cmd *cobra.Command, args []string) {
if Debug { if Debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.SetGlobalLevel(zerolog.DebugLevel)
} }
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
} }
func initTuiLog(cmd *cobra.Command, args []string) { func initTuiLog(cmd *cobra.Command, args []string) {

View File

@@ -74,6 +74,16 @@ func (c *Context) SwitchHistory(account string) {
c.WorkDir = history.WorkDir c.WorkDir = history.WorkDir
c.HTTPEnabled = history.HTTPEnabled c.HTTPEnabled = history.HTTPEnabled
c.HTTPAddr = history.HTTPAddr c.HTTPAddr = history.HTTPAddr
} else {
c.Account = ""
c.Platform = ""
c.Version = 0
c.FullVersion = ""
c.DataKey = ""
c.DataDir = ""
c.WorkDir = ""
c.HTTPEnabled = false
c.HTTPAddr = ""
} }
} }

View File

@@ -20,9 +20,17 @@ func (c *ChatRoomV4) Wrap() *ChatRoom {
users = ParseRoomData(c.ExtBuffer) users = ParseRoomData(c.ExtBuffer)
} }
user2DisplayName := make(map[string]string, len(users))
for _, user := range users {
if user.DisplayName != "" {
user2DisplayName[user.UserName] = user.DisplayName
}
}
return &ChatRoom{ return &ChatRoom{
Name: c.UserName, Name: c.UserName,
Owner: c.Owner, Owner: c.Owner,
Users: users, Users: users,
User2DisplayName: user2DisplayName,
} }
} }

View File

@@ -15,13 +15,17 @@ type Validator struct {
} }
// NewValidator 创建一个仅用于验证的验证器 // NewValidator 创建一个仅用于验证的验证器
func NewValidator(dataDir string, platform string, version int) (*Validator, error) { func NewValidator(platform string, version int, dataDir string) (*Validator, error) {
dbFile := GetSimpleDBFile(platform, version)
dbPath := filepath.Join(dataDir + "/" + dbFile)
return NewValidatorWithFile(platform, version, dbPath)
}
func NewValidatorWithFile(platform string, version int, dbPath string) (*Validator, error) {
decryptor, err := NewDecryptor(platform, version) decryptor, err := NewDecryptor(platform, version)
if err != nil { if err != nil {
return nil, err return nil, err
} }
dbFile := GetSimpleDBFile(platform, version)
dbPath := filepath.Join(dataDir + "/" + dbFile)
d, err := common.OpenDBFile(dbPath, decryptor.GetPageSize()) d, err := common.OpenDBFile(dbPath, decryptor.GetPageSize())
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -19,12 +19,22 @@ const (
MaxWorkersV3 = 8 MaxWorkersV3 = 8
) )
var V3KeyPatterns = []KeyPatternInfo{
{
Pattern: []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32},
Offset: 24,
},
}
type V3Extractor struct { type V3Extractor struct {
validator *decrypt.Validator validator *decrypt.Validator
keyPatterns []KeyPatternInfo
} }
func NewV3Extractor() *V3Extractor { func NewV3Extractor() *V3Extractor {
return &V3Extractor{} return &V3Extractor{
keyPatterns: V3KeyPatterns,
}
} }
func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) { func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
@@ -127,7 +137,6 @@ func (e *V3Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
// worker processes memory regions to find V3 version key // worker processes memory regions to find V3 version key
func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) { 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 { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -137,51 +146,60 @@ func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
return return
} }
index := len(memory) if key, ok := e.SearchKey(ctx, memory); ok {
for {
select { select {
case <-ctx.Done(): case resultChannel <- key:
return // Exit if context cancelled
default: default:
} }
log.Debug().Msgf("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
}
log.Debug().Msgf("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):
log.Debug().Msg("Key found: " + hex.EncodeToString(keyData))
return
default:
}
}
index -= 1
} }
} }
} }
} }
func (e *V3Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
for _, keyPattern := range e.keyPatterns {
index := len(memory)
for {
select {
case <-ctx.Done():
return "", false
default:
}
// Find pattern from end to beginning
index = bytes.LastIndex(memory[:index], keyPattern.Pattern)
if index == -1 {
break // No more matches found
}
// Check if we have enough space for the key
keyOffset := index + keyPattern.Offset
if keyOffset < 0 || keyOffset+32 > len(memory) {
index -= 1
continue
}
// Extract the key data, which is 32 bytes long
keyData := memory[keyOffset : keyOffset+32]
// Validate key against database header
if e.validator.Validate(keyData) {
log.Debug().
Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
Int("offset", keyPattern.Offset).
Str("key", hex.EncodeToString(keyData)).
Msg("Key found")
return hex.EncodeToString(keyData), true
}
index -= 1
}
}
return "", false
}
func (e *V3Extractor) SetValidate(validator *decrypt.Validator) { func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
e.validator = validator e.validator = validator
} }

View File

@@ -19,12 +19,26 @@ const (
MaxWorkers = 8 MaxWorkers = 8
) )
var V4KeyPatterns = []KeyPatternInfo{
{
Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
Offset: 16,
},
{
Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
Offset: -80,
},
}
type V4Extractor struct { type V4Extractor struct {
validator *decrypt.Validator validator *decrypt.Validator
keyPatterns []KeyPatternInfo
} }
func NewV4Extractor() *V4Extractor { func NewV4Extractor() *V4Extractor {
return &V4Extractor{} return &V4Extractor{
keyPatterns: V4KeyPatterns,
}
} }
func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) { func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
@@ -127,8 +141,6 @@ func (e *V4Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
// worker processes memory regions to find V4 version key // worker processes memory regions to find V4 version key
func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) { func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) {
keyPattern := []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00}
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -138,47 +150,65 @@ func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
return return
} }
index := len(memory) if key, ok := e.SearchKey(ctx, memory); ok {
for {
select { select {
case <-ctx.Done(): case resultChannel <- key:
return // Exit if context cancelled
default: 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):
log.Debug().Msg("Key found: " + hex.EncodeToString(keyData))
return
default:
}
}
index -= 1
} }
} }
} }
} }
func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
for _, keyPattern := range e.keyPatterns {
index := len(memory)
for {
select {
case <-ctx.Done():
return "", false
default:
}
// Find pattern from end to beginning
index = bytes.LastIndex(memory[:index], keyPattern.Pattern)
if index == -1 {
break // No more matches found
}
// Check if we have enough space for the key
keyOffset := index + keyPattern.Offset
if keyOffset < 0 || keyOffset+32 > len(memory) {
index -= 1
continue
}
// Extract the key data, which is 16 bytes after the pattern and 32 bytes long
keyData := memory[keyOffset : keyOffset+32]
// Validate key against database header
if e.validator.Validate(keyData) {
log.Debug().
Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
Int("offset", keyPattern.Offset).
Str("key", hex.EncodeToString(keyData)).
Msg("Key found")
return hex.EncodeToString(keyData), true
}
index -= 1
}
}
return "", false
}
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) { func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
e.validator = validator e.validator = validator
} }
type KeyPatternInfo struct {
Pattern []byte
Offset int
}

View File

@@ -15,6 +15,9 @@ type Extractor interface {
// Extract 从进程中提取密钥 // Extract 从进程中提取密钥
Extract(ctx context.Context, proc *model.Process) (string, error) Extract(ctx context.Context, proc *model.Process) (string, error)
// SearchKey 在内存中搜索密钥
SearchKey(ctx context.Context, memory []byte) (string, bool)
SetValidate(validator *decrypt.Validator) SetValidate(validator *decrypt.Validator)
} }

View File

@@ -1,6 +1,8 @@
package windows package windows
import ( import (
"context"
"github.com/sjzar/chatlog/internal/wechat/decrypt" "github.com/sjzar/chatlog/internal/wechat/decrypt"
) )
@@ -12,6 +14,11 @@ func NewV3Extractor() *V3Extractor {
return &V3Extractor{} return &V3Extractor{}
} }
func (e *V3Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
// TODO : Implement the key search logic for V3
return "", false
}
func (e *V3Extractor) SetValidate(validator *decrypt.Validator) { func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
e.validator = validator e.validator = validator
} }

View File

@@ -1,6 +1,8 @@
package windows package windows
import ( import (
"context"
"github.com/sjzar/chatlog/internal/wechat/decrypt" "github.com/sjzar/chatlog/internal/wechat/decrypt"
) )
@@ -12,6 +14,11 @@ func NewV4Extractor() *V4Extractor {
return &V4Extractor{} return &V4Extractor{}
} }
func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
// TODO : Implement the key search logic for V4
return "", false
}
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) { func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
e.validator = validator e.validator = validator
} }

View File

@@ -15,10 +15,10 @@ import (
) )
const ( const (
V3ProcessName = "WeChat" ProcessNameOfficial = "WeChat"
V4ProcessName = "Weixin" ProcessNameBeta = "Weixin"
V3DBFile = "Message/msg_0.db" V3DBFile = "Message/msg_0.db"
V4DBFile = "db_storage/message/message_0.db" V4DBFile = "db_storage/session/session.db"
) )
// Detector 实现 macOS 平台的进程检测器 // Detector 实现 macOS 平台的进程检测器
@@ -40,7 +40,7 @@ func (d *Detector) FindProcesses() ([]*model.Process, error) {
var result []*model.Process var result []*model.Process
for _, p := range processes { for _, p := range processes {
name, err := p.Name() name, err := p.Name()
if err != nil || (name != V3ProcessName && name != V4ProcessName) { if err != nil || (name != ProcessNameOfficial && name != ProcessNameBeta) {
continue continue
} }

View File

@@ -90,7 +90,7 @@ func (a *Account) GetKey(ctx context.Context) (string, error) {
return "", err return "", err
} }
validator, err := decrypt.NewValidator(process.DataDir, process.Platform, process.Version) validator, err := decrypt.NewValidator(process.Platform, process.Version, process.DataDir)
if err != nil { if err != nil {
return "", err return "", err
} }