98 lines
2.8 KiB
Go
98 lines
2.8 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// Windows facts for the origin walk: exe and parent from one Toolhelp
|
|
// snapshot, the session from ProcessIdToSessionId. Session 0 is where
|
|
// services and scheduled tasks live; a non-zero session is a logged-in
|
|
// desktop. Scheduled tasks are children of the svchost.exe that hosts the
|
|
// Schedule service, whose pid is looked up once per cache window, so a
|
|
// generic svchost parent is recognised as THE scheduler rather than guessed.
|
|
//
|
|
// Not done yet: telling an RDP session from a console one. Both read
|
|
// as `interactive` here; it is later work.
|
|
|
|
func procFactsReader() func(int) (procFacts, bool) {
|
|
var facts map[int]procFacts
|
|
return func(pid int) (procFacts, bool) {
|
|
if facts == nil {
|
|
facts = winProcFacts()
|
|
}
|
|
f, ok := facts[pid]
|
|
return f, ok
|
|
}
|
|
}
|
|
|
|
func winProcFacts() map[int]procFacts {
|
|
out := map[int]procFacts{}
|
|
snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
|
|
if err != nil {
|
|
return out
|
|
}
|
|
defer windows.CloseHandle(snap)
|
|
var e windows.ProcessEntry32
|
|
e.Size = uint32(unsafe.Sizeof(e))
|
|
for err = windows.Process32First(snap, &e); err == nil; err = windows.Process32Next(snap, &e) {
|
|
pid := int(e.ProcessID)
|
|
if pid <= 0 {
|
|
continue
|
|
}
|
|
exe := windows.UTF16ToString(e.ExeFile[:])
|
|
f := procFacts{PID: pid, PPID: int(e.ParentProcessID), Comm: binBase(exe), Exe: exe}
|
|
var sess uint32
|
|
if windows.ProcessIdToSessionId(uint32(pid), &sess) == nil && sess > 0 {
|
|
f.Interactive, f.Detail = true, "session "+strconv.Itoa(int(sess))
|
|
}
|
|
out[pid] = f
|
|
}
|
|
return out
|
|
}
|
|
|
|
// The Schedule service's host pid. One PowerShell CIM query as a fixed
|
|
// -EncodedCommand (no quoting through the Win32 command line), cached; an
|
|
// empty answer (no PowerShell, service stopped) means no scheduler pid and
|
|
// tasks fall through to `service` (session 0 under services.exe), which is
|
|
// still true.
|
|
var schedSvcCache = struct {
|
|
sync.Mutex
|
|
at time.Time
|
|
pid int
|
|
}{}
|
|
|
|
func scheduleServicePID() int {
|
|
schedSvcCache.Lock()
|
|
defer schedSvcCache.Unlock()
|
|
if !schedSvcCache.at.IsZero() && time.Since(schedSvcCache.at) < schedCacheFor {
|
|
return schedSvcCache.pid
|
|
}
|
|
pid := 0
|
|
if out, err := runTimed("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
"-EncodedCommand", windowsScheduleSvcEncoded); err == nil {
|
|
pid, _ = strconv.Atoi(strings.TrimSpace(string(out)))
|
|
}
|
|
schedSvcCache.at, schedSvcCache.pid = time.Now(), pid
|
|
return pid
|
|
}
|
|
|
|
func originExtras() originExtra {
|
|
ex := originExtra{SchedulerPIDs: map[int]bool{}}
|
|
if pid := scheduleServicePID(); pid > 0 {
|
|
ex.SchedulerPIDs[pid] = true
|
|
}
|
|
return ex
|
|
}
|
|
|
|
const windowsScheduleSvcScript = `(Get-CimInstance Win32_Service -Filter "Name='Schedule'").ProcessId`
|
|
|
|
var windowsScheduleSvcEncoded = encodePowerShell(windowsScheduleSvcScript)
|