providers/windows/process_windows.go (292 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package windows
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"unsafe"
syswin "golang.org/x/sys/windows"
windows "github.com/elastic/go-windows"
"github.com/elastic/go-sysinfo/types"
)
var (
selfPID = os.Getpid()
devMapper = newDeviceMapper()
)
func (s windowsSystem) Processes() (procs []types.Process, err error) {
pids, err := windows.EnumProcesses()
if err != nil {
return nil, fmt.Errorf("EnumProcesses: %w", err)
}
procs = make([]types.Process, 0, len(pids))
var proc types.Process
for _, pid := range pids {
if pid == 0 || pid == 4 {
// The Idle and System processes (PIDs 0 and 4) can never be
// opened by user-level code (see documentation for OpenProcess).
continue
}
if proc, err = s.Process(int(pid)); err == nil {
procs = append(procs, proc)
}
}
if len(procs) == 0 {
return nil, err
}
return procs, nil
}
func (s windowsSystem) Process(pid int) (types.Process, error) {
return newProcess(pid)
}
func (s windowsSystem) Self() (types.Process, error) {
return newProcess(selfPID)
}
type process struct {
pid int
info types.ProcessInfo
}
func (p *process) PID() int {
return p.pid
}
func (p *process) Parent() (types.Process, error) {
info, err := p.Info()
if err != nil {
return nil, err
}
return newProcess(info.PPID)
}
func newProcess(pid int) (*process, error) {
p := &process{pid: pid}
if err := p.init(); err != nil {
return nil, err
}
return p, nil
}
func (p *process) init() error {
handle, err := p.open()
if err != nil {
return err
}
defer syscall.CloseHandle(handle)
var path string
if imgf, err := windows.GetProcessImageFileName(handle); err == nil {
path, err = devMapper.DevicePathToDrivePath(imgf)
if err != nil {
path = imgf
}
}
var creationTime, exitTime, kernelTime, userTime syscall.Filetime
if err := syscall.GetProcessTimes(handle, &creationTime, &exitTime, &kernelTime, &userTime); err != nil {
return err
}
// Try to read the RTL_USER_PROCESS_PARAMETERS struct from the target process
// memory. This can fail due to missing access rights or when we are running
// as a 32bit process in a 64bit system (WOW64).
// Don't make this a fatal error: If it fails, `args` and `cwd` fields will
// be missing.
var args []string
var cwd string
var ppid int
pbi, err := getProcessBasicInformation(syswin.Handle(handle))
if err == nil {
ppid = int(pbi.InheritedFromUniqueProcessID)
userProcParams, err := getUserProcessParams(syswin.Handle(handle), pbi)
if err == nil {
if argsW, err := readProcessUnicodeString(handle, &userProcParams.CommandLine); err == nil {
args, err = splitCommandline(argsW)
if err != nil {
args = nil
}
}
if cwdW, err := readProcessUnicodeString(handle, &userProcParams.CurrentDirectoryPath); err == nil {
cwd, _, err = windows.UTF16BytesToString(cwdW)
if err != nil {
cwd = ""
}
// Remove trailing separator
cwd = strings.TrimRight(cwd, "\\")
}
}
}
p.info = types.ProcessInfo{
Name: filepath.Base(path),
PID: p.pid,
PPID: ppid,
Exe: path,
Args: args,
CWD: cwd,
StartTime: time.Unix(0, creationTime.Nanoseconds()),
}
return nil
}
func getProcessBasicInformation(handle syswin.Handle) (pbi windows.ProcessBasicInformationStruct, err error) {
var actualSize uint32
err = syswin.NtQueryInformationProcess(handle, syswin.ProcessBasicInformation, unsafe.Pointer(&pbi), uint32(windows.SizeOfProcessBasicInformationStruct), &actualSize)
if actualSize < uint32(windows.SizeOfProcessBasicInformationStruct) {
return pbi, errors.New("bad size for PROCESS_BASIC_INFORMATION")
}
return pbi, err
}
func getUserProcessParams(handle syswin.Handle, pbi windows.ProcessBasicInformationStruct) (params windows.RtlUserProcessParameters, err error) {
const is32bitProc = unsafe.Sizeof(uintptr(0)) == 4
// Offset of params field within PEB structure.
// This structure is different in 32 and 64 bit.
paramsOffset := 0x20
if is32bitProc {
paramsOffset = 0x10
}
// Read the PEB from the target process memory
pebSize := paramsOffset + 8
peb := make([]byte, pebSize)
var nRead uintptr
err = syswin.ReadProcessMemory(handle, pbi.PebBaseAddress, &peb[0], uintptr(pebSize), &nRead)
if err != nil {
return params, err
}
if nRead != uintptr(pebSize) {
return params, fmt.Errorf("PEB: short read (%d/%d)", nRead, pebSize)
}
// Get the RTL_USER_PROCESS_PARAMETERS struct pointer from the PEB
paramsAddr := *(*uintptr)(unsafe.Pointer(&peb[paramsOffset]))
// Read the RTL_USER_PROCESS_PARAMETERS from the target process memory
paramsBuf := make([]byte, windows.SizeOfRtlUserProcessParameters)
err = syswin.ReadProcessMemory(handle, paramsAddr, ¶msBuf[0], uintptr(windows.SizeOfRtlUserProcessParameters), &nRead)
if err != nil {
return params, err
}
if nRead != uintptr(windows.SizeOfRtlUserProcessParameters) {
return params, fmt.Errorf("RTL_USER_PROCESS_PARAMETERS: short read (%d/%d)", nRead, windows.SizeOfRtlUserProcessParameters)
}
params = *(*windows.RtlUserProcessParameters)(unsafe.Pointer(¶msBuf[0]))
return params, nil
}
// read an UTF-16 string from another process memory. Result is an []byte
// with the UTF-16 data.
func readProcessUnicodeString(handle syscall.Handle, s *windows.UnicodeString) ([]byte, error) {
// Allocate an extra UTF-16 null character at the end in case the read string
// is not terminated.
extra := 2
if s.Size&1 != 0 {
extra = 3 // If size is odd, need 3 nulls to terminate.
}
buf := make([]byte, int(s.Size)+extra)
nRead, err := windows.ReadProcessMemory(handle, s.Buffer, buf[:s.Size])
if err != nil {
return nil, err
}
if nRead != uintptr(s.Size) {
return nil, fmt.Errorf("unicode string: short read: (%d/%d)", nRead, s.Size)
}
return buf, nil
}
// Use Windows' CommandLineToArgv API to split an UTF-16 command line string
// into a list of parameters.
func splitCommandline(utf16 []byte) ([]string, error) {
n := len(utf16)
// Discard odd byte
if n&1 != 0 {
n--
utf16 = utf16[:n]
}
if n == 0 {
return nil, nil
}
terminated := false
for i := 0; i < n && !terminated; i += 2 {
terminated = utf16[i] == 0 && utf16[i+1] == 0
}
if !terminated {
// Append a null uint16 at the end if terminator is missing
utf16 = append(utf16, 0, 0)
}
var numArgs int32
argsWide, err := syscall.CommandLineToArgv((*uint16)(unsafe.Pointer(&utf16[0])), &numArgs)
if err != nil {
return nil, err
}
// Free memory allocated for CommandLineToArgvW arguments.
defer syscall.LocalFree((syscall.Handle)(unsafe.Pointer(argsWide)))
args := make([]string, numArgs)
for idx := range args {
args[idx] = syscall.UTF16ToString(argsWide[idx][:])
}
return args, nil
}
func (p *process) open() (handle syscall.Handle, err error) {
if p.pid == selfPID {
return syscall.GetCurrentProcess()
}
// Try different access rights, from broader to more limited.
// PROCESS_VM_READ is needed to get command-line and working directory
// PROCESS_QUERY_LIMITED_INFORMATION is only available in Vista+
for _, permissions := range [4]uint32{
syscall.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_READ,
windows.PROCESS_QUERY_LIMITED_INFORMATION | windows.PROCESS_VM_READ,
syscall.PROCESS_QUERY_INFORMATION,
windows.PROCESS_QUERY_LIMITED_INFORMATION,
} {
if handle, err = syscall.OpenProcess(permissions, false, uint32(p.pid)); err == nil {
break
}
}
return handle, err
}
func (p *process) Info() (types.ProcessInfo, error) {
return p.info, nil
}
func (p *process) User() (types.UserInfo, error) {
handle, err := p.open()
if err != nil {
return types.UserInfo{}, fmt.Errorf("OpenProcess failed: %w", err)
}
defer syscall.CloseHandle(handle)
var accessToken syswin.Token
err = syswin.OpenProcessToken(syswin.Handle(handle), syscall.TOKEN_QUERY, &accessToken)
if err != nil {
return types.UserInfo{}, fmt.Errorf("OpenProcessToken failed: %w", err)
}
defer accessToken.Close()
tokenUser, err := accessToken.GetTokenUser()
if err != nil {
return types.UserInfo{}, fmt.Errorf("GetTokenUser failed: %w", err)
}
sid, err := sidToString(tokenUser.User.Sid)
if sid == "" || err != nil {
if err != nil {
return types.UserInfo{}, fmt.Errorf("failed to look up user SID: %w", err)
}
return types.UserInfo{}, errors.New("failed to look up user SID")
}
tokenGroup, err := accessToken.GetTokenPrimaryGroup()
if err != nil {
return types.UserInfo{}, fmt.Errorf("GetTokenPrimaryGroup failed: %w", err)
}
gsid, err := sidToString(tokenGroup.PrimaryGroup)
if gsid == "" || err != nil {
if err != nil {
return types.UserInfo{}, fmt.Errorf("failed to look up primary group SID: %w", err)
}
return types.UserInfo{}, errors.New("failed to look up primary group SID")
}
return types.UserInfo{
UID: sid,
GID: gsid,
}, nil
}
func (p *process) Memory() (types.MemoryInfo, error) {
handle, err := p.open()
if err != nil {
return types.MemoryInfo{}, err
}
defer syscall.CloseHandle(handle)
counters, err := windows.GetProcessMemoryInfo(handle)
if err != nil {
return types.MemoryInfo{}, err
}
return types.MemoryInfo{
Resident: uint64(counters.WorkingSetSize),
Virtual: uint64(counters.PrivateUsage),
}, nil
}
func (p *process) CPUTime() (types.CPUTimes, error) {
handle, err := p.open()
if err != nil {
return types.CPUTimes{}, err
}
defer syscall.CloseHandle(handle)
var creationTime, exitTime, kernelTime, userTime syscall.Filetime
if err := syscall.GetProcessTimes(handle, &creationTime, &exitTime, &kernelTime, &userTime); err != nil {
return types.CPUTimes{}, err
}
return types.CPUTimes{
User: windows.FiletimeToDuration(&userTime),
System: windows.FiletimeToDuration(&kernelTime),
}, nil
}
// OpenHandles returns the number of open handles of the process.
func (p *process) OpenHandleCount() (int, error) {
handle, err := p.open()
if err != nil {
return 0, err
}
defer syscall.CloseHandle(handle)
count, err := windows.GetProcessHandleCount(handle)
return int(count), err
}