diff --git a/process/process.go b/process/process.go index d2a043d..fc1c5f4 100644 --- a/process/process.go +++ b/process/process.go @@ -542,3 +542,8 @@ func (p *Process) Kill() error { func (p *Process) Username() (string, error) { return p.UsernameWithContext(context.Background()) } + +// Environ returns the environment variables of the process. +func (p *Process) Environ() ([]string, error) { + return p.EnvironWithContext(context.Background()) +} diff --git a/process/process_bsd.go b/process/process_bsd.go index ffb7481..545e44f 100644 --- a/process/process_bsd.go +++ b/process/process_bsd.go @@ -72,6 +72,10 @@ func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesS return nil, common.ErrNotImplementedError } +func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { + return nil, common.ErrNotImplementedError +} + func parseKinfoProc(buf []byte) (KinfoProc, error) { var k KinfoProc br := bytes.NewReader(buf) diff --git a/process/process_darwin.go b/process/process_darwin.go index 383e099..ea7a7b1 100644 --- a/process/process_darwin.go +++ b/process/process_darwin.go @@ -399,6 +399,10 @@ func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net return net.ConnectionsPidMaxWithContext(ctx, "all", p.Pid, max) } +func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { + return nil, common.ErrNotImplementedError +} + func ProcessesWithContext(ctx context.Context) ([]*Process, error) { out := []*Process{} diff --git a/process/process_fallback.go b/process/process_fallback.go index dad8547..0d54900 100644 --- a/process/process_fallback.go +++ b/process/process_fallback.go @@ -203,3 +203,7 @@ func (p *Process) KillWithContext(ctx context.Context) error { func (p *Process) UsernameWithContext(ctx context.Context) (string, error) { return "", common.ErrNotImplementedError } + +func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { + return nil, common.ErrNotImplementedError +} diff --git a/process/process_freebsd.go b/process/process_freebsd.go index 0666e7f..9091d6e 100644 --- a/process/process_freebsd.go +++ b/process/process_freebsd.go @@ -10,9 +10,9 @@ import ( "strconv" "strings" - cpu "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/internal/common" - net "github.com/shirou/gopsutil/net" + "github.com/shirou/gopsutil/net" "golang.org/x/sys/unix" ) @@ -289,6 +289,10 @@ func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net return nil, common.ErrNotImplementedError } +func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { + return nil, common.ErrNotImplementedError +} + func ProcessesWithContext(ctx context.Context) ([]*Process, error) { results := []*Process{} diff --git a/process/process_linux.go b/process/process_linux.go index f52b700..4360eff 100644 --- a/process/process_linux.go +++ b/process/process_linux.go @@ -490,6 +490,17 @@ func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]M return &ret, nil } +func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { + environPath := common.HostProc(strconv.Itoa(int(p.Pid)), "environ") + + environContent, err := ioutil.ReadFile(environPath) + if err != nil { + return nil, err + } + + return strings.Split(string(environContent), "\000"), nil +} + /** ** Internal functions **/ diff --git a/process/process_openbsd.go b/process/process_openbsd.go index 902664b..b642137 100644 --- a/process/process_openbsd.go +++ b/process/process_openbsd.go @@ -14,10 +14,10 @@ import ( "strings" "unsafe" - cpu "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/internal/common" - mem "github.com/shirou/gopsutil/mem" - net "github.com/shirou/gopsutil/net" + "github.com/shirou/gopsutil/mem" + "github.com/shirou/gopsutil/net" "golang.org/x/sys/unix" ) @@ -310,6 +310,10 @@ func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net return nil, common.ErrNotImplementedError } +func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { + return nil, common.ErrNotImplementedError +} + func ProcessesWithContext(ctx context.Context) ([]*Process, error) { results := []*Process{} diff --git a/process/process_solaris.go b/process/process_solaris.go index eb6af5a..f24a327 100644 --- a/process/process_solaris.go +++ b/process/process_solaris.go @@ -198,6 +198,10 @@ func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]M return nil, common.ErrNotImplementedError } +func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { + return nil, common.ErrNotImplementedError +} + /** ** Internal functions **/ diff --git a/process/process_test.go b/process/process_test.go index 0ea85ea..b32b4ca 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -709,6 +709,21 @@ func Test_AllProcesses_cmdLine(t *testing.T) { } } +func Test_AllProcesses_environ(t *testing.T) { + procs, err := Processes() + if err == nil { + for _, proc := range procs { + exeName, _ := proc.Exe() + environ, err := proc.Environ() + if err != nil { + environ = []string{"Error: " + err.Error() } + } + + t.Logf("Process #%v: Name: %v / Environment Variables: %v\n", proc.Pid, exeName, environ) + } + } +} + func BenchmarkNewProcess(b *testing.B) { checkPid := os.Getpid() for i := 0; i < b.N; i++ { diff --git a/process/process_windows.go b/process/process_windows.go index 5475f6a..8efece1 100644 --- a/process/process_windows.go +++ b/process/process_windows.go @@ -3,17 +3,19 @@ package process import ( + "bufio" "context" "errors" "fmt" + "io" "os" "strings" "syscall" "unsafe" - cpu "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/internal/common" - net "github.com/shirou/gopsutil/net" + "github.com/shirou/gopsutil/net" "golang.org/x/sys/windows" ) @@ -113,6 +115,53 @@ type processBasicInformation64 struct { Reserved4 uint64 } +type processEnvironmentBlock32 struct { + Reserved1 [2]uint8 + BeingDebugged uint8 + Reserved2 uint8 + Reserved3 [2]uint32 + Ldr uint32 + ProcessParameters uint32 + // More fields which we don't use so far +} + +type processEnvironmentBlock64 struct { + Reserved1 [2]uint8 + BeingDebugged uint8 + Reserved2 uint8 + _ [4]uint8 // padding, since we are 64 bit, the next pointer is 64 bit aligned (when compiling for 32 bit, this is not the case without manual padding) + Reserved3 [2]uint64 + Ldr uint64 + ProcessParameters uint64 + // More fields which we don't use so far +} + +type rtlUserProcessParameters32 struct { + Reserved1 [16]uint8 + Reserved2 [10]uint32 + ImagePathNameLength uint16 + _ uint16 + ImagePathAddress uint32 + CommandLineLength uint16 + _ uint16 + CommandLineAddress uint32 + EnvironmentAddress uint32 + // More fields which we don't use so far +} + +type rtlUserProcessParameters64 struct { + Reserved1 [16]uint8 + Reserved2 [10]uint64 + ImagePathNameLength uint32 + _ uint32 + ImagePathAddress uint64 + CommandLineLength uint32 + _ uint32 + CommandLineAddress uint64 + EnvironmentAddress uint64 + // More fields which we don't use so far +} + type winLUID struct { LowPart winDWord HighPart winLong @@ -622,6 +671,14 @@ func (p *Process) KillWithContext(ctx context.Context) error { return process.Kill() } +func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { + envVars, err := getProcessEnvironmentVariables(p.Pid, ctx) + if err != nil { + return nil, fmt.Errorf("could not get environment variables: %s", err) + } + return envVars, nil +} + // retrieve Ppid in a thread-safe manner func (p *Process) getPpid() int32 { p.parentMutex.RLock() @@ -748,39 +805,45 @@ func getProcessCPUTimes(pid int32) (SYSTEM_TIMES, error) { return times, err } -func is32BitProcess(procHandle syscall.Handle) bool { - var wow64 uint +func getUserProcessParams32(handle windows.Handle) (rtlUserProcessParameters32, error) { + pebAddress, err := queryPebAddress(syscall.Handle(handle), true) + if err != nil { + return rtlUserProcessParameters32{}, fmt.Errorf("cannot locate process PEB: %w", err) + } - ret, _, _ := common.ProcNtQueryInformationProcess.Call( - uintptr(procHandle), - uintptr(common.ProcessWow64Information), - uintptr(unsafe.Pointer(&wow64)), - uintptr(unsafe.Sizeof(wow64)), - uintptr(0), - ) - if int(ret) >= 0 { - if wow64 != 0 { - return true - } - } else { - //if the OS does not support the call, we fallback into the bitness of the app - if unsafe.Sizeof(wow64) == 4 { - return true - } + buf := readProcessMemory(syscall.Handle(handle), true, pebAddress, uint(unsafe.Sizeof(processEnvironmentBlock32{}))) + if len(buf) != int(unsafe.Sizeof(processEnvironmentBlock32{})) { + return rtlUserProcessParameters32{}, fmt.Errorf("cannot read process PEB") + } + peb := (*processEnvironmentBlock32)(unsafe.Pointer(&buf[0])) + userProcessAddress := uint64(peb.ProcessParameters) + buf = readProcessMemory(syscall.Handle(handle), true, userProcessAddress, uint(unsafe.Sizeof(rtlUserProcessParameters32{}))) + if len(buf) != int(unsafe.Sizeof(rtlUserProcessParameters32{})) { + return rtlUserProcessParameters32{}, fmt.Errorf("cannot read user process parameters") } - return false + return *(*rtlUserProcessParameters32)(unsafe.Pointer(&buf[0])), nil } -func getProcessCommandLine(pid int32) (string, error) { - h, err := windows.OpenProcess(processQueryInformation|windows.PROCESS_VM_READ, false, uint32(pid)) - if err == windows.ERROR_ACCESS_DENIED || err == windows.ERROR_INVALID_PARAMETER { - return "", nil - } +func getUserProcessParams64(handle windows.Handle) (rtlUserProcessParameters64, error) { + pebAddress, err := queryPebAddress(syscall.Handle(handle), false) if err != nil { - return "", err + return rtlUserProcessParameters64{}, fmt.Errorf("cannot locate process PEB: %w", err) } - defer syscall.CloseHandle(syscall.Handle(h)) + buf := readProcessMemory(syscall.Handle(handle), false, pebAddress, uint(unsafe.Sizeof(processEnvironmentBlock64{}))) + if len(buf) != int(unsafe.Sizeof(processEnvironmentBlock64{})) { + return rtlUserProcessParameters64{}, fmt.Errorf("cannot read process PEB") + } + peb := (*processEnvironmentBlock64)(unsafe.Pointer(&buf[0])) + userProcessAddress := peb.ProcessParameters + buf = readProcessMemory(syscall.Handle(handle), false, userProcessAddress, uint(unsafe.Sizeof(rtlUserProcessParameters64{}))) + if len(buf) != int(unsafe.Sizeof(rtlUserProcessParameters64{})) { + return rtlUserProcessParameters64{}, fmt.Errorf("cannot read user process parameters") + } + return *(*rtlUserProcessParameters64)(unsafe.Pointer(&buf[0])), nil +} + +func is32BitProcess(h windows.Handle) bool { const ( PROCESSOR_ARCHITECTURE_INTEL = 0 PROCESSOR_ARCHITECTURE_ARM = 5 @@ -789,86 +852,165 @@ func getProcessCommandLine(pid int32) (string, error) { PROCESSOR_ARCHITECTURE_AMD64 = 9 ) - procIs32Bits := true + var procIs32Bits bool switch processorArchitecture { case PROCESSOR_ARCHITECTURE_INTEL: fallthrough case PROCESSOR_ARCHITECTURE_ARM: procIs32Bits = true - case PROCESSOR_ARCHITECTURE_ARM64: fallthrough case PROCESSOR_ARCHITECTURE_IA64: fallthrough case PROCESSOR_ARCHITECTURE_AMD64: - procIs32Bits = is32BitProcess(syscall.Handle(h)) + var wow64 uint + + ret, _, _ := common.ProcNtQueryInformationProcess.Call( + uintptr(h), + uintptr(common.ProcessWow64Information), + uintptr(unsafe.Pointer(&wow64)), + uintptr(unsafe.Sizeof(wow64)), + uintptr(0), + ) + if int(ret) >= 0 { + if wow64 != 0 { + procIs32Bits = true + } + } else { + //if the OS does not support the call, we fallback into the bitness of the app + if unsafe.Sizeof(wow64) == 4 { + procIs32Bits = true + } + } default: //for other unknown platforms, we rely on process platform if unsafe.Sizeof(processorArchitecture) == 8 { procIs32Bits = false + } else { + procIs32Bits = true } } + return procIs32Bits +} - pebAddress := queryPebAddress(syscall.Handle(h), procIs32Bits) - if pebAddress == 0 { - return "", errors.New("cannot locate process PEB") +func getProcessEnvironmentVariables(pid int32, ctx context.Context) ([]string, error) { + h, err := windows.OpenProcess(processQueryInformation|windows.PROCESS_VM_READ, false, uint32(pid)) + if err == windows.ERROR_ACCESS_DENIED || err == windows.ERROR_INVALID_PARAMETER { + return nil, nil + } + if err != nil { + return nil, err } + defer syscall.CloseHandle(syscall.Handle(h)) + + procIs32Bits := is32BitProcess(h) + + var processParameterBlockAddress uint64 if procIs32Bits { - buf := readProcessMemory(syscall.Handle(h), procIs32Bits, pebAddress+uint64(16), 4) - if len(buf) != 4 { - return "", errors.New("cannot locate process user parameters") + peb, err := getUserProcessParams32(h) + if err != nil { + return nil, err } - userProcParams := uint64(buf[0]) | (uint64(buf[1]) << 8) | (uint64(buf[2]) << 16) | (uint64(buf[3]) << 24) - - //read CommandLine field from PRTL_USER_PROCESS_PARAMETERS - remoteCmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, userProcParams+uint64(64), 8) - if len(remoteCmdLine) != 8 { - return "", errors.New("cannot read cmdline field") + processParameterBlockAddress = uint64(peb.EnvironmentAddress) + } else { + peb, err := getUserProcessParams64(h) + if err != nil { + return nil, err + } + processParameterBlockAddress = peb.EnvironmentAddress + } + envvarScanner := bufio.NewScanner(&processReader{ + processHandle: h, + is32BitProcess: procIs32Bits, + offset: processParameterBlockAddress, + }) + envvarScanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error){ + if atEOF && len(data) == 0 { + return 0, nil, nil + } + // Check for UTF-16 zero character + for i := 0; i < len(data) - 1; i+=2 { + if data[i] == 0 && data[i+1] == 0 { + return i+2, data[0:i], nil + } + } + if atEOF { + return len(data), data, nil + } + // Request more data + return 0, nil, nil + }) + var envVars []string + for envvarScanner.Scan() { + entry := envvarScanner.Bytes() + if len(entry) == 0 { + break // Block is finished } + envVars = append(envVars, convertUTF16ToString(entry)) + select { + case <-ctx.Done(): + break + default: + continue + } + } + if err := envvarScanner.Err(); err != nil { + return nil, err + } + return envVars, nil +} + +type processReader struct { + processHandle windows.Handle + is32BitProcess bool + offset uint64 +} + +func (p *processReader) Read(buf []byte) (int, error) { + processMemory := readProcessMemory(syscall.Handle(p.processHandle), p.is32BitProcess, p.offset, uint(len(buf))) + if len(processMemory) == 0 { + return 0, io.EOF + } + copy(buf, processMemory) + p.offset += uint64(len(processMemory)) + return len(processMemory), nil +} - //remoteCmdLine is actually a UNICODE_STRING32 - //the first two bytes has the length - cmdLineLength := uint(remoteCmdLine[0]) | (uint(remoteCmdLine[1]) << 8) - if cmdLineLength > 0 { - //and, at offset 4, is the pointer to the buffer - bufferAddress := uint32(remoteCmdLine[4]) | (uint32(remoteCmdLine[5]) << 8) | - (uint32(remoteCmdLine[6]) << 16) | (uint32(remoteCmdLine[7]) << 24) +func getProcessCommandLine(pid int32) (string, error) { + h, err := windows.OpenProcess(processQueryInformation|windows.PROCESS_VM_READ, false, uint32(pid)) + if err == windows.ERROR_ACCESS_DENIED || err == windows.ERROR_INVALID_PARAMETER { + return "", nil + } + if err != nil { + return "", err + } + defer syscall.CloseHandle(syscall.Handle(h)) - cmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, uint64(bufferAddress), cmdLineLength) - if len(cmdLine) != int(cmdLineLength) { + procIs32Bits := is32BitProcess(h) + + if procIs32Bits { + userProcParams, err := getUserProcessParams32(h) + if err != nil { + return "", err + } + if userProcParams.CommandLineLength > 0 { + cmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, uint64(userProcParams.CommandLineAddress), uint(userProcParams.CommandLineLength)) + if len(cmdLine) != int(userProcParams.CommandLineLength) { return "", errors.New("cannot read cmdline") } return convertUTF16ToString(cmdLine), nil } } else { - buf := readProcessMemory(syscall.Handle(h), procIs32Bits, pebAddress+uint64(32), 8) - if len(buf) != 8 { - return "", errors.New("cannot locate process user parameters") - } - userProcParams := uint64(buf[0]) | (uint64(buf[1]) << 8) | (uint64(buf[2]) << 16) | (uint64(buf[3]) << 24) | - (uint64(buf[4]) << 32) | (uint64(buf[5]) << 40) | (uint64(buf[6]) << 48) | (uint64(buf[7]) << 56) - - //read CommandLine field from PRTL_USER_PROCESS_PARAMETERS - remoteCmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, userProcParams+uint64(112), 16) - if len(remoteCmdLine) != 16 { - return "", errors.New("cannot read cmdline field") + userProcParams, err := getUserProcessParams64(h) + if err != nil { + return "", err } - - //remoteCmdLine is actually a UNICODE_STRING64 - //the first two bytes has the length - cmdLineLength := uint(remoteCmdLine[0]) | (uint(remoteCmdLine[1]) << 8) - if cmdLineLength > 0 { - //and, at offset 8, is the pointer to the buffer - bufferAddress := uint64(remoteCmdLine[8]) | (uint64(remoteCmdLine[9]) << 8) | - (uint64(remoteCmdLine[10]) << 16) | (uint64(remoteCmdLine[11]) << 24) | - (uint64(remoteCmdLine[12]) << 32) | (uint64(remoteCmdLine[13]) << 40) | - (uint64(remoteCmdLine[14]) << 48) | (uint64(remoteCmdLine[15]) << 56) - - cmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, bufferAddress, cmdLineLength) - if len(cmdLine) != int(cmdLineLength) { + if userProcParams.CommandLineLength > 0 { + cmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, userProcParams.CommandLineAddress, uint(userProcParams.CommandLineLength)) + if len(cmdLine) != int(userProcParams.CommandLineLength) { return "", errors.New("cannot read cmdline") } diff --git a/process/process_windows_386.go b/process/process_windows_386.go index cd88496..71d4b2f 100644 --- a/process/process_windows_386.go +++ b/process/process_windows_386.go @@ -3,10 +3,12 @@ package process import ( + "errors" "syscall" "unsafe" "github.com/shirou/gopsutil/internal/common" + "golang.org/x/sys/windows" ) type PROCESS_MEMORY_COUNTERS struct { @@ -22,7 +24,7 @@ type PROCESS_MEMORY_COUNTERS struct { PeakPagefileUsage uint32 } -func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) uint64 { +func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) (uint64, error) { if is32BitProcess { //we are on a 32-bit process reading an external 32-bit process var info processBasicInformation32 @@ -34,8 +36,10 @@ func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) uint64 { uintptr(unsafe.Sizeof(info)), uintptr(0), ) - if int(ret) >= 0 { - return uint64(info.PebBaseAddress) + if status := windows.NTStatus(ret); status == windows.STATUS_SUCCESS { + return uint64(info.PebBaseAddress), nil + } else { + return 0, windows.NTStatus(ret) } } else { //we are on a 32-bit process reading an external 64-bit process @@ -49,14 +53,15 @@ func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) uint64 { uintptr(unsafe.Sizeof(info)), uintptr(0), ) - if int(ret) >= 0 { - return info.PebBaseAddress + if status := windows.NTStatus(ret); status == windows.STATUS_SUCCESS { + return info.PebBaseAddress, nil + } else { + return 0, windows.NTStatus(ret) } + } else { + return 0, errors.New("can't find API to query 64 bit process from 32 bit") } } - - //return 0 on error - return 0 } func readProcessMemory(h syscall.Handle, is32BitProcess bool, address uint64, size uint) []byte { diff --git a/process/process_windows_amd64.go b/process/process_windows_amd64.go index 3ee5be4..14308e4 100644 --- a/process/process_windows_amd64.go +++ b/process/process_windows_amd64.go @@ -7,6 +7,7 @@ import ( "unsafe" "github.com/shirou/gopsutil/internal/common" + "golang.org/x/sys/windows" ) type PROCESS_MEMORY_COUNTERS struct { @@ -22,7 +23,7 @@ type PROCESS_MEMORY_COUNTERS struct { PeakPagefileUsage uint64 } -func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) uint64 { +func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) (uint64, error) { if is32BitProcess { //we are on a 64-bit process reading an external 32-bit process var wow64 uint @@ -34,8 +35,10 @@ func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) uint64 { uintptr(unsafe.Sizeof(wow64)), uintptr(0), ) - if int(ret) >= 0 { - return uint64(wow64) + if status := windows.NTStatus(ret); status == windows.STATUS_SUCCESS { + return uint64(wow64), nil + } else { + return 0, windows.NTStatus(ret) } } else { //we are on a 64-bit process reading an external 64-bit process @@ -48,13 +51,12 @@ func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) uint64 { uintptr(unsafe.Sizeof(info)), uintptr(0), ) - if int(ret) >= 0 { - return info.PebBaseAddress + if status := windows.NTStatus(ret); status == windows.STATUS_SUCCESS { + return info.PebBaseAddress, nil + } else { + return 0, windows.NTStatus(ret) } } - - //return 0 on error - return 0 } func readProcessMemory(procHandle syscall.Handle, _ bool, address uint64, size uint) []byte {