Compare commits
3 Commits
feature/da
...
feature/ke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c15ad45d89 | ||
|
|
b64902ecb6 | ||
|
|
dc116c50bf |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,5 @@ go.work.sum
|
||||
# syncthing files
|
||||
.stfolder
|
||||
|
||||
chatlog
|
||||
chatlog.exe# Added by goreleaser init:
|
||||
dist/
|
||||
|
||||
146
cmd/chatlog/cmd_dumpmemory.go
Normal file
146
cmd/chatlog/cmd_dumpmemory.go
Normal 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)
|
||||
},
|
||||
}
|
||||
@@ -22,6 +22,8 @@ func initLog(cmd *cobra.Command, args []string) {
|
||||
if Debug {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
|
||||
}
|
||||
|
||||
func initTuiLog(cmd *cobra.Command, args []string) {
|
||||
|
||||
@@ -74,6 +74,16 @@ func (c *Context) SwitchHistory(account string) {
|
||||
c.WorkDir = history.WorkDir
|
||||
c.HTTPEnabled = history.HTTPEnabled
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,17 @@ type Validator struct {
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbFile := GetSimpleDBFile(platform, version)
|
||||
dbPath := filepath.Join(dataDir + "/" + dbFile)
|
||||
d, err := common.OpenDBFile(dbPath, decryptor.GetPageSize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -19,12 +19,22 @@ const (
|
||||
MaxWorkersV3 = 8
|
||||
)
|
||||
|
||||
var V3KeyPatterns = []KeyPatternInfo{
|
||||
{
|
||||
Pattern: []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32},
|
||||
Offset: 24,
|
||||
},
|
||||
}
|
||||
|
||||
type V3Extractor struct {
|
||||
validator *decrypt.Validator
|
||||
keyPatterns []KeyPatternInfo
|
||||
}
|
||||
|
||||
func NewV3Extractor() *V3Extractor {
|
||||
return &V3Extractor{}
|
||||
return &V3Extractor{
|
||||
keyPatterns: V3KeyPatterns,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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():
|
||||
@@ -137,49 +146,58 @@ func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
|
||||
return
|
||||
}
|
||||
|
||||
if key, ok := e.SearchKey(ctx, memory); ok {
|
||||
select {
|
||||
case resultChannel <- key:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 // Exit if context cancelled
|
||||
return "", false
|
||||
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)
|
||||
index = bytes.LastIndex(memory[:index], keyPattern.Pattern)
|
||||
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) {
|
||||
// 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 right after the pattern and 32 bytes long
|
||||
keyOffset := index + 24
|
||||
// Extract the key data, which is 32 bytes long
|
||||
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:
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -19,12 +19,26 @@ const (
|
||||
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 {
|
||||
validator *decrypt.Validator
|
||||
keyPatterns []KeyPatternInfo
|
||||
}
|
||||
|
||||
func NewV4Extractor() *V4Extractor {
|
||||
return &V4Extractor{}
|
||||
return &V4Extractor{
|
||||
keyPatterns: V4KeyPatterns,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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():
|
||||
@@ -138,47 +150,65 @@ func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
|
||||
return
|
||||
}
|
||||
|
||||
if key, ok := e.SearchKey(ctx, memory); ok {
|
||||
select {
|
||||
case resultChannel <- key:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 // Exit if context cancelled
|
||||
return "", false
|
||||
default:
|
||||
}
|
||||
|
||||
// Find pattern from end to beginning
|
||||
index = bytes.LastIndex(memory[:index], keyPattern)
|
||||
index = bytes.LastIndex(memory[:index], keyPattern.Pattern)
|
||||
if index == -1 {
|
||||
break // No more matches found
|
||||
}
|
||||
|
||||
// Check if we have enough space for the key
|
||||
if index+16+32 > len(memory) {
|
||||
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
|
||||
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:
|
||||
}
|
||||
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) {
|
||||
e.validator = validator
|
||||
}
|
||||
|
||||
type KeyPatternInfo struct {
|
||||
Pattern []byte
|
||||
Offset int
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ type Extractor interface {
|
||||
// Extract 从进程中提取密钥
|
||||
Extract(ctx context.Context, proc *model.Process) (string, error)
|
||||
|
||||
// SearchKey 在内存中搜索密钥
|
||||
SearchKey(ctx context.Context, memory []byte) (string, bool)
|
||||
|
||||
SetValidate(validator *decrypt.Validator)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package windows
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
)
|
||||
|
||||
@@ -12,6 +14,11 @@ func NewV3Extractor() *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) {
|
||||
e.validator = validator
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package windows
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
)
|
||||
|
||||
@@ -12,6 +14,11 @@ func NewV4Extractor() *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) {
|
||||
e.validator = validator
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
V3ProcessName = "WeChat"
|
||||
V4ProcessName = "Weixin"
|
||||
ProcessNameOfficial = "WeChat"
|
||||
ProcessNameBeta = "Weixin"
|
||||
V3DBFile = "Message/msg_0.db"
|
||||
V4DBFile = "db_storage/message/message_0.db"
|
||||
V4DBFile = "db_storage/session/session.db"
|
||||
)
|
||||
|
||||
// Detector 实现 macOS 平台的进程检测器
|
||||
@@ -40,7 +40,7 @@ func (d *Detector) FindProcesses() ([]*model.Process, error) {
|
||||
var result []*model.Process
|
||||
for _, p := range processes {
|
||||
name, err := p.Name()
|
||||
if err != nil || (name != V3ProcessName && name != V4ProcessName) {
|
||||
if err != nil || (name != ProcessNameOfficial && name != ProcessNameBeta) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ func (a *Account) GetKey(ctx context.Context) (string, error) {
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user