mirror of
https://github.com/teest114514/chatlog_alpha.git
synced 2026-03-24 07:32:07 +08:00
Refactor key extraction: drop V3 support, parallelize V4
Removed all code and logic related to WeChat V3 key extraction, including darwin and windows V3 extractors. Updated extractor selection to return an error for V3. Refactored Windows DLL extractor to run DLL and native (Dart-style) memory scan in parallel for V4, reporting keys as soon as found. Improved image key acquisition flow in the TUI and updated documentation to reflect the new focus on V4, parallel key extraction, and the deprecation of V3 support.
This commit is contained in:
@@ -3,7 +3,6 @@ package chatlog
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -225,29 +224,25 @@ func (a *App) inputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||||
func (a *App) initMenu() {
|
||||
getDataKey := &menu.Item{
|
||||
Index: 2,
|
||||
Name: "获取密钥",
|
||||
Description: "从进程获取数据密钥 & 图片密钥",
|
||||
Name: "获取图片密钥",
|
||||
Description: "扫描内存获取图片密钥(需微信V4)",
|
||||
Selected: func(i *menu.Item) {
|
||||
modal := tview.NewModal()
|
||||
if runtime.GOOS == "darwin" {
|
||||
modal.SetText("获取密钥中...\n预计需要 20 秒左右的时间,期间微信会卡住,请耐心等待")
|
||||
} else {
|
||||
modal.SetText("获取密钥中...")
|
||||
}
|
||||
modal.SetText("正在扫描内存获取图片密钥...\n请确保微信已登录并浏览过图片")
|
||||
a.mainPages.AddPage("modal", modal, true, true)
|
||||
a.SetFocus(modal)
|
||||
|
||||
go func() {
|
||||
err := a.m.GetDataKey()
|
||||
err := a.m.GetImageKey()
|
||||
|
||||
// 在主线程中更新UI
|
||||
a.QueueUpdateDraw(func() {
|
||||
if err != nil {
|
||||
// 解密失败
|
||||
modal.SetText("获取密钥失败: " + err.Error())
|
||||
modal.SetText("获取图片密钥失败: " + err.Error())
|
||||
} else {
|
||||
// 解密成功
|
||||
modal.SetText("获取密钥成功")
|
||||
modal.SetText("获取图片密钥成功")
|
||||
}
|
||||
|
||||
// 添加确认按钮
|
||||
|
||||
@@ -179,6 +179,18 @@ func (m *Manager) GetDataKey() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetImageKey() error {
|
||||
if m.ctx.Current == nil {
|
||||
return fmt.Errorf("未选择任何账号")
|
||||
}
|
||||
if _, err := m.wechat.GetImageKey(m.ctx.Current); err != nil {
|
||||
return err
|
||||
}
|
||||
m.ctx.Refresh()
|
||||
m.ctx.UpdateConfig()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) RestartAndGetDataKey() error {
|
||||
if m.ctx.Current == nil {
|
||||
return fmt.Errorf("未选择任何账号")
|
||||
|
||||
@@ -67,6 +67,15 @@ func (s *Service) GetDataKey(info *wechat.Account) (string, error) {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// GetImageKey extracts the image key from a WeChat process
|
||||
func (s *Service) GetImageKey(info *wechat.Account) (string, error) {
|
||||
if info == nil {
|
||||
return "", fmt.Errorf("no WeChat instance selected")
|
||||
}
|
||||
|
||||
return info.GetImageKey(context.Background())
|
||||
}
|
||||
|
||||
func (s *Service) StartAutoDecrypt() error {
|
||||
log.Info().Msgf("start auto decrypt, data dir: %s", s.conf.GetDataDir())
|
||||
dbGroup, err := filemonitor.NewFileGroup("wechat", s.conf.GetDataDir(), `.*\.db$`, []string{"fts"})
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
package darwin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"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
|
||||
)
|
||||
|
||||
var V3KeyPatterns = []KeyPatternInfo{
|
||||
{
|
||||
Pattern: []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32},
|
||||
Offsets: []int{24},
|
||||
},
|
||||
}
|
||||
|
||||
type V3Extractor struct {
|
||||
validator *decrypt.Validator
|
||||
keyPatterns []KeyPatternInfo
|
||||
}
|
||||
|
||||
func NewV3Extractor() *V3Extractor {
|
||||
return &V3Extractor{
|
||||
keyPatterns: V3KeyPatterns,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, string, error) {
|
||||
if proc.Status == model.StatusOffline {
|
||||
return "", "", errors.ErrWeChatOffline
|
||||
}
|
||||
|
||||
// Check if SIP is disabled, as it's required for memory reading on macOS
|
||||
if !glance.IsSIPDisabled() {
|
||||
return "", "", errors.ErrSIPEnabled
|
||||
}
|
||||
|
||||
if e.validator == nil {
|
||||
return "", "", errors.ErrValidatorNotSet
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
log.Debug().Msgf("Starting %d workers for V3 key search", workerCount)
|
||||
|
||||
// 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 {
|
||||
log.Err(err).Msg("Failed to read memory")
|
||||
}
|
||||
}()
|
||||
|
||||
// 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 "", "", errors.ErrNoValidKey
|
||||
}
|
||||
|
||||
// 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 err
|
||||
}
|
||||
|
||||
totalSize := len(memory)
|
||||
log.Debug().Msgf("Read memory region, size: %d bytes", totalSize)
|
||||
|
||||
// If memory is small enough, process it as a single chunk
|
||||
if totalSize <= MinChunkSize {
|
||||
select {
|
||||
case memoryChannel <- memory:
|
||||
log.Debug().Msg("Memory sent as a single chunk for analysis")
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
chunkCount := MaxWorkers * ChunkMultiplier
|
||||
|
||||
// Calculate chunk size based on fixed chunk count
|
||||
chunkSize := totalSize / chunkCount
|
||||
if chunkSize < MinChunkSize {
|
||||
// Reduce number of chunks if each would be too small
|
||||
chunkCount = totalSize / MinChunkSize
|
||||
if chunkCount == 0 {
|
||||
chunkCount = 1
|
||||
}
|
||||
chunkSize = totalSize / chunkCount
|
||||
}
|
||||
|
||||
// Process memory in chunks from end to beginning
|
||||
for i := chunkCount - 1; i >= 0; i-- {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
// Calculate start and end positions for this chunk
|
||||
start := i * chunkSize
|
||||
end := (i + 1) * chunkSize
|
||||
|
||||
// Ensure the last chunk includes all remaining memory
|
||||
if i == chunkCount-1 {
|
||||
end = totalSize
|
||||
}
|
||||
|
||||
// Add overlap area to catch patterns at chunk boundaries
|
||||
if i > 0 {
|
||||
start -= ChunkOverlapBytes
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
}
|
||||
|
||||
chunk := memory[start:end]
|
||||
|
||||
log.Debug().
|
||||
Int("chunk_index", i+1).
|
||||
Int("total_chunks", chunkCount).
|
||||
Int("chunk_size", len(chunk)).
|
||||
Int("start_offset", start).
|
||||
Int("end_offset", end).
|
||||
Msg("Processing memory chunk")
|
||||
|
||||
select {
|
||||
case memoryChannel <- chunk:
|
||||
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) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case memory, ok := <-memoryChannel:
|
||||
if !ok {
|
||||
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 "", false
|
||||
default:
|
||||
}
|
||||
|
||||
// Find pattern from end to beginning
|
||||
index = bytes.LastIndex(memory[:index], keyPattern.Pattern)
|
||||
if index == -1 {
|
||||
break // No more matches found
|
||||
}
|
||||
|
||||
// Try each offset for this pattern
|
||||
for _, offset := range keyPattern.Offsets {
|
||||
// Check if we have enough space for the key
|
||||
keyOffset := index + offset
|
||||
if keyOffset < 0 || keyOffset+32 > len(memory) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract the key data, which is at the offset position 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", 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) {
|
||||
e.validator = validator
|
||||
}
|
||||
@@ -26,14 +26,12 @@ type Extractor interface {
|
||||
// NewExtractor 创建适合当前平台的密钥提取器
|
||||
// 对于Windows平台,优先使用DLL方式(如果DLL存在)
|
||||
func NewExtractor(platform string, version int) (Extractor, error) {
|
||||
// 暂停对V3版本的支持
|
||||
if version == 3 {
|
||||
return nil, fmt.Errorf("当前已暂停对微信V3版本的支持")
|
||||
}
|
||||
|
||||
switch {
|
||||
case platform == "windows" && version == 3:
|
||||
// 尝试使用DLL方式
|
||||
if extractor, err := NewDLLExtractor(platform, version); err == nil {
|
||||
return extractor, nil
|
||||
}
|
||||
// 如果DLL方式失败,回退到原来的方式
|
||||
return windows.NewV3Extractor(), nil
|
||||
case platform == "windows" && version == 4:
|
||||
// 尝试使用DLL方式
|
||||
if extractor, err := NewDLLExtractor(platform, version); err == nil {
|
||||
@@ -41,8 +39,6 @@ func NewExtractor(platform string, version int) (Extractor, error) {
|
||||
}
|
||||
// 如果DLL方式失败,回退到原来的方式
|
||||
return windows.NewV4Extractor(), nil
|
||||
case platform == "darwin" && version == 3:
|
||||
return darwin.NewV3Extractor(), nil
|
||||
case platform == "darwin" && version == 4:
|
||||
return darwin.NewV4Extractor(), nil
|
||||
default:
|
||||
|
||||
@@ -93,28 +93,123 @@ func (e *DLLExtractor) Extract(ctx context.Context, proc *model.Process) (string
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
// 清理之前的初始化(如果存在)
|
||||
if e.initialized {
|
||||
e.cleanup()
|
||||
}
|
||||
|
||||
// 初始化DLL
|
||||
// 注意:初始化必须在锁内完成
|
||||
if err := e.initialize(proc.PID); err != nil {
|
||||
e.mu.Unlock()
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 确保无论成功失败都清理
|
||||
// 注意:这里使用defer确保cleanup在函数返回前执行
|
||||
defer func() {
|
||||
// DLL相关的清理函数
|
||||
cleanupDLL := func() {
|
||||
if e.initialized {
|
||||
e.cleanup()
|
||||
}
|
||||
}
|
||||
e.mu.Unlock() // 初始化完成后解锁,允许并行执行(注意pollKeys内部需要处理并发访问,或者我们确保它独占DLL资源)
|
||||
// 查看pollKeys实现,它只访问e.lastKey和DLL函数,且DLLExtractor是单例/单次使用的,所以这里解锁只要保证没有其他Extract调用即可。
|
||||
// 但为了安全,我们在pollKeys的goroutine里重新加锁?不,pollKeys是长时间运行的。
|
||||
// 实际上DLLExtractor的设计似乎假设是单线程使用的。
|
||||
// 为了安全起见,我们在DLL goroutine里持有锁。
|
||||
|
||||
// 准备并行执行
|
||||
var (
|
||||
finalDataKey string
|
||||
finalImgKey string
|
||||
keyMu sync.Mutex // 保护结果更新
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
// 创建可取消的上下文
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// 辅助函数:更新密钥并检查是否完成
|
||||
updateKeys := func(dk, ik, source string) {
|
||||
keyMu.Lock()
|
||||
defer keyMu.Unlock()
|
||||
|
||||
updated := false
|
||||
if dk != "" && finalDataKey == "" {
|
||||
finalDataKey = dk
|
||||
updated = true
|
||||
log.Info().Msgf("通过 %s 获取到数据库密钥", source)
|
||||
}
|
||||
if ik != "" && finalImgKey == "" {
|
||||
finalImgKey = ik
|
||||
updated = true
|
||||
log.Info().Msgf("通过 %s 获取到图片密钥", source)
|
||||
}
|
||||
|
||||
// 检查是否所有需要的密钥都已获取
|
||||
// 对于V4:需要DataKey + ImgKey
|
||||
// 对于V3:只需要DataKey
|
||||
if updated {
|
||||
if proc.Version == 4 {
|
||||
if finalDataKey != "" && finalImgKey != "" {
|
||||
log.Info().Msg("已获取所有所需密钥,提前结束轮询")
|
||||
cancel()
|
||||
}
|
||||
} else {
|
||||
if finalDataKey != "" {
|
||||
log.Info().Msg("已获取所有所需密钥,提前结束轮询")
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 任务 1: DLL 轮询 (运行在Goroutine中)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// DLL操作需要持有锁以保护状态
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
defer cleanupDLL() // 确保退出时清理
|
||||
|
||||
// 执行轮询,传入回调以便立即报告发现的密钥
|
||||
dk, ik, _ := e.pollKeys(ctx, proc.Version, func(d, i string) {
|
||||
updateKeys(d, i, "DLL")
|
||||
})
|
||||
|
||||
// 轮询结束后的最终更新(防止遗漏,虽然回调应该覆盖了大部分情况)
|
||||
if dk != "" || ik != "" {
|
||||
updateKeys(dk, ik, "DLL")
|
||||
}
|
||||
}()
|
||||
|
||||
// 轮询获取密钥
|
||||
return e.pollKeys(ctx, proc.Version)
|
||||
// 任务 2: 原生内存扫描 (仅V4版本,运行在Goroutine中)
|
||||
if proc.Version == 4 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
log.Info().Msg("并行启动原生内存扫描(Dart模式)...")
|
||||
|
||||
v4 := NewV4Extractor()
|
||||
v4.SetValidate(e.validator)
|
||||
|
||||
// 执行扫描
|
||||
dk, ik, _ := v4.Extract(ctx, proc)
|
||||
|
||||
// 更新结果
|
||||
if dk != "" || ik != "" {
|
||||
updateKeys(dk, ik, "内存扫描")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 等待所有任务完成(或超时/被取消)
|
||||
wg.Wait()
|
||||
|
||||
// 只要获取到了任意密钥,就算成功
|
||||
var err error
|
||||
if finalDataKey == "" && finalImgKey == "" {
|
||||
err = fmt.Errorf("未获取到有效密钥")
|
||||
}
|
||||
|
||||
return finalDataKey, finalImgKey, err
|
||||
}
|
||||
|
||||
// initialize 初始化DLL Hook
|
||||
@@ -153,7 +248,7 @@ func (e *DLLExtractor) initialize(pid uint32) error {
|
||||
}
|
||||
|
||||
// pollKeys 轮询获取密钥
|
||||
func (e *DLLExtractor) pollKeys(ctx context.Context, version int) (string, string, error) {
|
||||
func (e *DLLExtractor) pollKeys(ctx context.Context, version int, onKeyFound func(dataKey, imgKey string)) (string, string, error) {
|
||||
if !e.initialized {
|
||||
return "", "", fmt.Errorf("DLL未初始化")
|
||||
}
|
||||
@@ -170,7 +265,7 @@ func (e *DLLExtractor) pollKeys(ctx context.Context, version int) (string, strin
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", "", ctx.Err()
|
||||
return dataKey, imgKey, ctx.Err()
|
||||
case <-timeout:
|
||||
// 检查是否获取到了数据密钥
|
||||
if dataKey != "" {
|
||||
@@ -239,10 +334,13 @@ func (e *DLLExtractor) pollKeys(ctx context.Context, version int) (string, strin
|
||||
continue
|
||||
}
|
||||
|
||||
foundNew := false
|
||||
|
||||
// 检查是否是数据库密钥
|
||||
if e.validator != nil && e.validator.Validate(keyBytes) {
|
||||
if dataKey == "" {
|
||||
dataKey = key
|
||||
foundNew = true
|
||||
msg := "通过DLL找到数据库密钥: " + key
|
||||
log.Info().Msg(msg)
|
||||
// 记录到日志文件
|
||||
@@ -257,6 +355,7 @@ func (e *DLLExtractor) pollKeys(ctx context.Context, version int) (string, strin
|
||||
// 图片密钥通常是16字节(32字符HEX字符串)
|
||||
if len(key) == 64 && dataKey == "" {
|
||||
dataKey = key
|
||||
foundNew = true
|
||||
msg := "通过DLL找到数据库密钥(无验证): " + key
|
||||
log.Info().Msg(msg)
|
||||
// 记录到日志文件
|
||||
@@ -266,6 +365,7 @@ func (e *DLLExtractor) pollKeys(ctx context.Context, version int) (string, strin
|
||||
}
|
||||
} else if len(key) == 32 && imgKey == "" {
|
||||
imgKey = key
|
||||
foundNew = true
|
||||
msg := "通过DLL找到图片密钥(无验证): " + imgKey
|
||||
log.Info().Msg(msg)
|
||||
// 记录到日志文件
|
||||
@@ -280,6 +380,7 @@ func (e *DLLExtractor) pollKeys(ctx context.Context, version int) (string, strin
|
||||
if e.validator != nil && e.validator.ValidateImgKey(keyBytes) {
|
||||
if imgKey == "" {
|
||||
imgKey = key[:32] // 16字节的HEX字符串是32个字符
|
||||
foundNew = true
|
||||
msg := "通过DLL找到图片密钥: " + imgKey
|
||||
log.Info().Msg(msg)
|
||||
// 记录到日志文件
|
||||
@@ -290,6 +391,11 @@ func (e *DLLExtractor) pollKeys(ctx context.Context, version int) (string, strin
|
||||
}
|
||||
}
|
||||
|
||||
// 如果发现了新密钥,立即通过回调报告
|
||||
if foundNew && onKeyFound != nil {
|
||||
onKeyFound(dataKey, imgKey)
|
||||
}
|
||||
|
||||
// 如果两个密钥都找到了,返回
|
||||
if dataKey != "" && imgKey != "" {
|
||||
return dataKey, imgKey, nil
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package windows
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
)
|
||||
|
||||
type V3Extractor struct {
|
||||
validator *decrypt.Validator
|
||||
}
|
||||
|
||||
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,13 +0,0 @@
|
||||
//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, string, error) {
|
||||
return "", "", nil
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
package windows
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"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, string, error) {
|
||||
// 即使状态是offline(未登录),也允许尝试
|
||||
// 因为用户可能在获取密钥过程中登录微信
|
||||
if proc.Status == model.StatusOffline {
|
||||
log.Info().Msg("微信进程存在但未登录,将尝试获取密钥,请登录微信后操作")
|
||||
// 不返回错误,继续执行
|
||||
}
|
||||
|
||||
// Open WeChat process
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, proc.PID)
|
||||
if err != nil {
|
||||
return "", "", errors.OpenProcessFailed(err)
|
||||
}
|
||||
defer windows.CloseHandle(handle)
|
||||
|
||||
// Check process architecture
|
||||
is64Bit, err := util.Is64Bit(handle)
|
||||
if err != nil {
|
||||
return "", "", 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
|
||||
}
|
||||
log.Debug().Msgf("Starting %d workers for V3 key search", workerCount)
|
||||
|
||||
// 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 {
|
||||
log.Err(err).Msg("Failed to find memory regions")
|
||||
}
|
||||
}()
|
||||
|
||||
// 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 "", "", errors.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 errors.ErrWeChatDLLNotFound
|
||||
}
|
||||
log.Debug().Msg("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().Msgf("Memory region: 0x%X - 0x%X, size: %d bytes", currentAddr, currentAddr+regionSize, regionSize)
|
||||
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:
|
||||
log.Debug().Msg("Valid key found: " + key)
|
||||
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 {
|
||||
log.Debug().Msgf("Failed to create module snapshot for PID %d: %v", pid, 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().Msgf("Module32First failed for PID %d: %v", pid, 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
|
||||
}
|
||||
@@ -4,14 +4,18 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
)
|
||||
|
||||
type V4Extractor struct {
|
||||
validator *decrypt.Validator
|
||||
logger *util.DLLLogger
|
||||
}
|
||||
|
||||
func NewV4Extractor() *V4Extractor {
|
||||
return &V4Extractor{}
|
||||
return &V4Extractor{
|
||||
logger: util.GetDLLLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package windows
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
const (
|
||||
MEM_PRIVATE = 0x20000
|
||||
MaxWorkers = 8
|
||||
)
|
||||
|
||||
func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, string, error) {
|
||||
@@ -35,82 +36,107 @@ func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
}
|
||||
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 [2]string, 1)
|
||||
|
||||
// Determine number of worker goroutines
|
||||
workerCount := runtime.NumCPU()
|
||||
if workerCount < 2 {
|
||||
workerCount = 2
|
||||
}
|
||||
if workerCount > MaxWorkers {
|
||||
workerCount = MaxWorkers
|
||||
}
|
||||
log.Debug().Msgf("Starting %d workers for V4 key search", workerCount)
|
||||
|
||||
// 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 {
|
||||
log.Err(err).Msg("Failed to find memory regions")
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for producer and consumers to complete
|
||||
go func() {
|
||||
producerWaitGroup.Wait()
|
||||
workerWaitGroup.Wait()
|
||||
close(resultChannel)
|
||||
}()
|
||||
|
||||
// Wait for result
|
||||
var finalDataKey, finalImgKey string
|
||||
// 设置总超时时间:60秒
|
||||
// 这给用户足够的时间去打开图片
|
||||
timeout := time.After(60 * time.Second)
|
||||
scanRound := 0
|
||||
|
||||
for {
|
||||
scanRound++
|
||||
// Create context to control all goroutines for THIS round
|
||||
scanCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
// 记录提示日志
|
||||
if scanRound == 1 || scanRound % 5 == 0 {
|
||||
msg := fmt.Sprintf("正在进行第 %d 轮内存扫描... 请打开任意图片以触发密钥加载", scanRound)
|
||||
log.Info().Msg(msg)
|
||||
if e.logger != nil {
|
||||
e.logger.LogInfo(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Create channels for memory data and results
|
||||
memoryChannel := make(chan []byte, 100)
|
||||
resultChannel := make(chan [2]string, 1)
|
||||
|
||||
// Determine number of worker goroutines
|
||||
workerCount := runtime.NumCPU()
|
||||
if workerCount < 2 {
|
||||
workerCount = 2
|
||||
}
|
||||
if workerCount > MaxWorkers {
|
||||
workerCount = MaxWorkers
|
||||
}
|
||||
|
||||
// Start consumer goroutines
|
||||
var workerWaitGroup sync.WaitGroup
|
||||
workerWaitGroup.Add(workerCount)
|
||||
for index := 0; index < workerCount; index++ {
|
||||
go func() {
|
||||
defer workerWaitGroup.Done()
|
||||
e.worker(scanCtx, 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(scanCtx, handle, memoryChannel)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to find memory regions")
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for producer and consumers to complete IN BACKGROUND
|
||||
// We need this to close resultChannel
|
||||
go func() {
|
||||
producerWaitGroup.Wait()
|
||||
workerWaitGroup.Wait()
|
||||
close(resultChannel)
|
||||
}()
|
||||
|
||||
// Wait for result of THIS round
|
||||
var roundImgKey string
|
||||
var roundDone bool
|
||||
|
||||
for !roundDone {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
return "", "", ctx.Err()
|
||||
case <-timeout:
|
||||
cancel()
|
||||
return "", "", fmt.Errorf("获取图片密钥超时(60秒),请确保已打开图片")
|
||||
case result, ok := <-resultChannel:
|
||||
if !ok {
|
||||
// Channel closed, round finished
|
||||
roundDone = true
|
||||
break
|
||||
}
|
||||
// Found something?
|
||||
if result[1] != "" {
|
||||
roundImgKey = result[1]
|
||||
// Found it!
|
||||
cancel()
|
||||
return "", roundImgKey, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancel() // Ensure cleanup of this round
|
||||
|
||||
// If we are here, round finished but no key found.
|
||||
// Wait a bit before next round
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", "", ctx.Err()
|
||||
case result, ok := <-resultChannel:
|
||||
if !ok {
|
||||
// Channel closed, all workers finished, return whatever keys we found
|
||||
if finalDataKey != "" || finalImgKey != "" {
|
||||
return finalDataKey, finalImgKey, nil
|
||||
}
|
||||
return "", "", errors.ErrNoValidKey
|
||||
}
|
||||
|
||||
// Update our best found keys
|
||||
if result[0] != "" {
|
||||
finalDataKey = result[0]
|
||||
}
|
||||
if result[1] != "" {
|
||||
finalImgKey = result[1]
|
||||
}
|
||||
|
||||
// If we have both keys, we can return early
|
||||
if finalDataKey != "" && finalImgKey != "" {
|
||||
cancel() // Cancel remaining work
|
||||
return finalDataKey, finalImgKey, nil
|
||||
}
|
||||
case <-timeout:
|
||||
return "", "", fmt.Errorf("获取图片密钥超时(60秒),请确保已打开图片")
|
||||
case <-time.After(1 * time.Second):
|
||||
// Continue to next round
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,8 +167,10 @@ func (e *V4Extractor) findMemory(ctx context.Context, handle windows.Handle, mem
|
||||
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 {
|
||||
// Check if memory region is readable and private (Matching Dart logic)
|
||||
// Dart: _isReadableProtect check (Not NOACCESS, Not GUARD)
|
||||
isReadable := (memInfo.Protect&windows.PAGE_NOACCESS) == 0 && (memInfo.Protect&windows.PAGE_GUARD) == 0
|
||||
if memInfo.State == windows.MEM_COMMIT && isReadable && memInfo.Type == MEM_PRIVATE {
|
||||
// Calculate region size, ensure it doesn't exceed limit
|
||||
regionSize := uintptr(memInfo.RegionSize)
|
||||
if currentAddr+regionSize > maxAddr {
|
||||
@@ -168,20 +196,22 @@ func (e *V4Extractor) findMemory(ctx context.Context, handle windows.Handle, mem
|
||||
return nil
|
||||
}
|
||||
|
||||
// workerV4 processes memory regions to find V4 version key
|
||||
// worker processes memory regions to find V4 version key
|
||||
func (e *V4Extractor) worker(ctx context.Context, handle windows.Handle, memoryChannel <-chan []byte, resultChannel chan<- [2]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,
|
||||
// Data Key scanning logic removed as per requirement.
|
||||
// Native scanner is now exclusively for Image Key (Dart mode).
|
||||
|
||||
// Helper to check if byte is lowercase alphanumeric
|
||||
isAlphaNumLower := func(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9')
|
||||
}
|
||||
ptrSize := 8
|
||||
littleEndianFunc := binary.LittleEndian.Uint64
|
||||
|
||||
// Track found keys
|
||||
var dataKey, imgKey string
|
||||
keysFound := make(map[uint64]bool) // Track processed addresses to avoid duplicates
|
||||
var imgKey string // dataKey removed
|
||||
|
||||
// Logging flags and counters
|
||||
validatorWarned := false
|
||||
candidateCount := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -190,73 +220,106 @@ func (e *V4Extractor) worker(ctx context.Context, handle windows.Handle, memoryC
|
||||
case memory, ok := <-memoryChannel:
|
||||
if !ok {
|
||||
// Memory scanning complete, return whatever keys we found
|
||||
if dataKey != "" || imgKey != "" {
|
||||
if candidateCount > 0 {
|
||||
msg := fmt.Sprintf("内存扫描结束,共检查了 %d 个候选图片密钥字符串", candidateCount)
|
||||
log.Debug().Msg(msg)
|
||||
if e.logger != nil {
|
||||
e.logger.LogDebug(msg)
|
||||
}
|
||||
}
|
||||
if imgKey != "" {
|
||||
select {
|
||||
case resultChannel <- [2]string{dataKey, imgKey}:
|
||||
case resultChannel <- [2]string{"", imgKey}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
index := len(memory)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return // Exit if context cancelled
|
||||
default:
|
||||
// Search for Image Key String (Scan for 32-byte alphanumeric string)
|
||||
// Only if we haven't found ImgKey yet
|
||||
if imgKey == "" {
|
||||
if e.validator == nil {
|
||||
if !validatorWarned {
|
||||
msg := "验证器未初始化(可能未找到*_t.dat文件),跳过图片密钥扫描"
|
||||
log.Warn().Msg(msg)
|
||||
if e.logger != nil {
|
||||
e.logger.LogWarning(msg)
|
||||
}
|
||||
validatorWarned = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Skip if we've already processed this address
|
||||
if keysFound[ptrValue] {
|
||||
index -= 1
|
||||
// We scan the memory block for sequences of 32 alphanumeric characters
|
||||
// The logic mimics img-key.dart: check boundaries and content
|
||||
for i := 0; i <= len(memory)-32; i++ {
|
||||
// Optimization: Check first byte
|
||||
if !isAlphaNumLower(memory[i]) {
|
||||
continue
|
||||
}
|
||||
keysFound[ptrValue] = true
|
||||
|
||||
// Validate key and determine type
|
||||
if key, isImgKey := e.validateKey(handle, ptrValue); key != "" {
|
||||
if isImgKey {
|
||||
if imgKey == "" {
|
||||
imgKey = key
|
||||
log.Debug().Msg("Image key found: " + key)
|
||||
// Report immediately when found
|
||||
select {
|
||||
case resultChannel <- [2]string{dataKey, imgKey}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if dataKey == "" {
|
||||
dataKey = key
|
||||
log.Debug().Msg("Data key found: " + key)
|
||||
// Report immediately when found
|
||||
select {
|
||||
case resultChannel <- [2]string{dataKey, imgKey}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// Boundary check: previous byte must NOT be alphanumeric (unless it's start of block)
|
||||
// Note: strictly speaking we should check across blocks, but here we check within block
|
||||
if i > 0 && isAlphaNumLower(memory[i-1]) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we have both keys, exit worker
|
||||
if dataKey != "" && imgKey != "" {
|
||||
log.Debug().Msg("Both keys found, worker exiting")
|
||||
return
|
||||
// Check if we have 32 valid chars
|
||||
isValid := true
|
||||
for j := 1; j < 32; j++ {
|
||||
if !isAlphaNumLower(memory[i+j]) {
|
||||
isValid = false
|
||||
i += j // Skip forward
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
continue
|
||||
}
|
||||
|
||||
// Boundary check: next byte (33rd) must NOT be alphanumeric
|
||||
if i+32 < len(memory) && isAlphaNumLower(memory[i+32]) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found a candidate 32-byte string
|
||||
candidateCount++
|
||||
if candidateCount % 5000 == 0 {
|
||||
msg := fmt.Sprintf("正在扫描图片密钥... 已检查 %d 个候选字符串", candidateCount)
|
||||
log.Debug().Msg(msg)
|
||||
if e.logger != nil {
|
||||
e.logger.LogDebug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
candidate := memory[i : i+32]
|
||||
|
||||
// Validate using existing validator (which now supports the *_t.dat check)
|
||||
// We pass the full 32 bytes, validator takes first 16
|
||||
if e.validator.ValidateImgKey(candidate) {
|
||||
// Found it!
|
||||
// Return hex encoded first 16 bytes
|
||||
foundKey := hex.EncodeToString(candidate[:16])
|
||||
if imgKey == "" {
|
||||
imgKey = foundKey
|
||||
msg := fmt.Sprintf("通过字符串扫描找到图片密钥! (在检查了 %d 个候选后) Key: %s", candidateCount, foundKey)
|
||||
log.Info().Msg(msg)
|
||||
if e.logger != nil {
|
||||
e.logger.LogStatus(1, msg)
|
||||
}
|
||||
select {
|
||||
case resultChannel <- [2]string{"", imgKey}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Skip past this key
|
||||
i += 32
|
||||
}
|
||||
}
|
||||
index -= 1 // Continue searching from previous position
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,20 +327,21 @@ func (e *V4Extractor) worker(ctx context.Context, handle windows.Handle, memoryC
|
||||
|
||||
// validateKey validates a single key candidate and returns the key and whether it's an image key
|
||||
func (e *V4Extractor) validateKey(handle windows.Handle, addr uint64) (string, bool) {
|
||||
if e.validator == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
keyData := make([]byte, 0x20) // 32-byte key
|
||||
if err := windows.ReadProcessMemory(handle, uintptr(addr), &keyData[0], uintptr(len(keyData)), nil); err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// First check if it's a valid database key
|
||||
if e.validator.Validate(keyData) {
|
||||
return hex.EncodeToString(keyData), false // Data key
|
||||
}
|
||||
|
||||
// Then check if it's a valid image key
|
||||
// Data Key validation removed.
|
||||
|
||||
// Only check if it's a valid image key
|
||||
if e.validator.ValidateImgKey(keyData) {
|
||||
return hex.EncodeToString(keyData[:16]), true // Image key
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
"github.com/sjzar/chatlog/internal/wechat/key"
|
||||
"github.com/sjzar/chatlog/internal/wechat/key/windows"
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
)
|
||||
|
||||
@@ -216,13 +218,61 @@ func (a *Account) clearAccountData() {
|
||||
|
||||
// GetKey 获取账号的密钥
|
||||
func (a *Account) GetKey(ctx context.Context) (string, string, error) {
|
||||
// 如果已经有数据密钥,直接返回(优先使用保存的密钥)
|
||||
// 对于微信V4,图片密钥可能不是必需的,所以即使没有图片密钥也返回数据密钥
|
||||
if a.Key != "" {
|
||||
log.Info().Msgf("使用保存的数据密钥,账号: %s", a.Name)
|
||||
return a.Key, a.ImgKey, nil
|
||||
hasDataKey := a.Key != ""
|
||||
hasImgKey := a.ImgKey != ""
|
||||
isV4 := a.Version == 4
|
||||
|
||||
// 1. 如果已有Data Key
|
||||
if hasDataKey {
|
||||
// 非V4,或者V4且有图片Key -> 完美,直接返回
|
||||
if !isV4 || hasImgKey {
|
||||
log.Info().Msgf("使用保存的密钥,账号: %s", a.Name)
|
||||
return a.Key, a.ImgKey, nil
|
||||
}
|
||||
|
||||
// V4且缺图片Key -> 尝试补全
|
||||
// 此时我们不走标准的Extractor (它会走DLL然后等待30秒),而是直接用原生扫描器
|
||||
if isV4 && !hasImgKey && a.Platform == "windows" {
|
||||
log.Info().Msgf("账号 %s 已有数据库密钥,正在尝试使用内存扫描补全图片密钥...", a.Name)
|
||||
|
||||
// 刷新状态以获取最新的Process对象
|
||||
if err := a.RefreshStatus(); err != nil {
|
||||
log.Warn().Err(err).Msg("刷新进程状态失败,返回现有密钥")
|
||||
return a.Key, a.ImgKey, nil
|
||||
}
|
||||
process, err := GetProcess(a.Name)
|
||||
if err != nil {
|
||||
return a.Key, a.ImgKey, nil
|
||||
}
|
||||
|
||||
// 准备验证器
|
||||
var validator *decrypt.Validator
|
||||
if process.DataDir != "" {
|
||||
validator, err = decrypt.NewValidator(process.Platform, process.Version, process.DataDir)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("创建验证器失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 直接调用原生V4扫描器
|
||||
v4 := windows.NewV4Extractor()
|
||||
if validator != nil {
|
||||
v4.SetValidate(validator)
|
||||
}
|
||||
|
||||
_, imgKey, err := v4.Extract(ctx, process)
|
||||
if err == nil && imgKey != "" {
|
||||
a.ImgKey = imgKey
|
||||
log.Info().Msg("成功补全图片密钥")
|
||||
} else {
|
||||
log.Warn().Msg("补全图片密钥失败,仅返回数据库密钥")
|
||||
}
|
||||
|
||||
return a.Key, a.ImgKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果没有Data Key -> 走标准流程 (DLL)
|
||||
// 刷新进程状态
|
||||
if err := a.RefreshStatus(); err != nil {
|
||||
return "", "", errors.RefreshProcessStatusFailed(err)
|
||||
@@ -243,10 +293,28 @@ func (a *Account) GetKey(ctx context.Context) (string, string, error) {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 如果是V4版本且DataDir为空,等待微信登录及数据目录就绪
|
||||
// 原生扫描器强烈依赖DataDir来查找验证样本
|
||||
if isV4 && process.DataDir == "" {
|
||||
log.Info().Msg("检测到V4版本且数据目录未就绪,等待微信登录...")
|
||||
for i := 0; i < 30; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
if err := a.RefreshStatus(); err == nil {
|
||||
if p, err := GetProcess(a.Name); err == nil && p.DataDir != "" {
|
||||
process = p
|
||||
a.DataDir = p.DataDir
|
||||
log.Info().Msgf("数据目录已就绪: %s", p.DataDir)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在DataDir存在时才创建验证器
|
||||
// 对于DLL方式,微信可能未登录,DataDir可能为空或路径不存在
|
||||
var validator *decrypt.Validator
|
||||
if process.DataDir != "" {
|
||||
log.Info().Msgf("准备创建验证器,DataDir: %s", process.DataDir)
|
||||
validator, err = decrypt.NewValidator(process.Platform, process.Version, process.DataDir)
|
||||
if err != nil {
|
||||
// 如果创建验证器失败,记录警告但不返回错误
|
||||
@@ -261,6 +329,8 @@ func (a *Account) GetKey(ctx context.Context) (string, string, error) {
|
||||
}
|
||||
|
||||
// 提取密钥
|
||||
// 注意:如果这是V4,且DLL只拿到Data Key,dll_extractor内部的fallback机制会被触发
|
||||
// 自动去跑原生扫描来拿Image Key
|
||||
dataKey, imgKey, err := extractor.Extract(ctx, process)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -277,6 +347,67 @@ func (a *Account) GetKey(ctx context.Context) (string, string, error) {
|
||||
return dataKey, imgKey, nil
|
||||
}
|
||||
|
||||
// GetImageKey 仅尝试获取图片密钥(使用原生扫描器)
|
||||
func (a *Account) GetImageKey(ctx context.Context) (string, error) {
|
||||
// 只有Windows V4支持此功能
|
||||
if a.Platform != "windows" || a.Version != 4 {
|
||||
return "", fmt.Errorf("只支持Windows微信V4版本获取图片密钥")
|
||||
}
|
||||
|
||||
// 刷新进程状态
|
||||
if err := a.RefreshStatus(); err != nil {
|
||||
return "", errors.RefreshProcessStatusFailed(err)
|
||||
}
|
||||
|
||||
process, err := GetProcess(a.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 等待DataDir就绪
|
||||
if process.DataDir == "" {
|
||||
log.Info().Msg("检测到数据目录未就绪,等待微信登录...")
|
||||
for i := 0; i < 30; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
if err := a.RefreshStatus(); err == nil {
|
||||
if p, err := GetProcess(a.Name); err == nil && p.DataDir != "" {
|
||||
process = p
|
||||
a.DataDir = p.DataDir
|
||||
log.Info().Msgf("数据目录已就绪: %s", p.DataDir)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if process.DataDir == "" {
|
||||
return "", fmt.Errorf("数据目录未就绪,无法进行图片密钥扫描,请确保微信已登录")
|
||||
}
|
||||
|
||||
// 准备验证器
|
||||
validator, err := decrypt.NewValidator(process.Platform, process.Version, process.DataDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建验证器失败(请确保已浏览过图片以生成缓存): %v", err)
|
||||
}
|
||||
|
||||
// 直接调用原生V4扫描器
|
||||
v4 := windows.NewV4Extractor()
|
||||
v4.SetValidate(validator)
|
||||
|
||||
log.Info().Msg("正在启动内存扫描以获取图片密钥...")
|
||||
_, imgKey, err := v4.Extract(ctx, process)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if imgKey != "" {
|
||||
a.ImgKey = imgKey
|
||||
return imgKey, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("未能获取到图片密钥")
|
||||
}
|
||||
|
||||
// DecryptDatabase 解密数据库
|
||||
func (a *Account) DecryptDatabase(ctx context.Context, dbPath, outputPath string) error {
|
||||
// 获取密钥
|
||||
|
||||
Reference in New Issue
Block a user