Created
January 17, 2020 23:42
-
-
Save bored-engineer/ff428f9e41a8ff4743435e8ba30bc7fb to your computer and use it in GitHub Desktop.
Watch new process spawns from userspace (without becoming root) via polling /proc (`GOOS=linux GOARCH=amd64 go build watch.go`)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bufio" | |
"bytes" | |
"io/ioutil" | |
"log" | |
"os" | |
"path/filepath" | |
"strconv" | |
"strings" | |
"syscall" | |
"time" | |
) | |
// Entry point | |
func main() { | |
// Read in /etc/passwd so we know which uid is which (os/user cannot x-compile easily) | |
passwdBytes, err := ioutil.ReadFile("/etc/passwd") | |
if err != nil { | |
log.Fatalf("failed to read /etc/passwd: %v", err) | |
} | |
scanner := bufio.NewScanner(bytes.NewReader(passwdBytes)) | |
users := make(map[uint32]string) | |
for scanner.Scan() { | |
parts := strings.SplitN(scanner.Text(), ":", 4) | |
if uid, err := strconv.Atoi(parts[2]); err == nil { | |
users[uint32(uid)] = parts[0] | |
} | |
} | |
if err := scanner.Err(); err != nil { | |
log.Fatalf("scanner failed: %v", err) | |
} | |
// Determine the max PID that can occur | |
maxPIDBytes, err := ioutil.ReadFile("/proc/sys/kernel/pid_max") | |
if err != nil { | |
log.Fatalf("failed to determine max PID: %v", err) | |
} | |
maxPID, err := strconv.Atoi(strings.TrimSpace(string(maxPIDBytes))) | |
if err != nil { | |
log.Fatalf("failed to parse max PID: %v", err) | |
} | |
// Slices are _fast_ compared to map[int]time.Time | |
processes := make([]time.Time, maxPID) | |
// Using time.Tick will skip ticks if we're still processing the last tick | |
for tick := range time.Tick(time.Millisecond * 100) { | |
// List out /proc to get a list of every running process | |
files, err := ioutil.ReadDir("/proc") | |
if err != nil { | |
log.Fatalf("failed to list proc: %v", err) | |
} | |
for _, file := range files { | |
// Skip anything that's not a PID/number | |
id, err := strconv.Atoi(file.Name()) | |
if err != nil { | |
continue | |
} | |
// If this is the first time we've seen the process, emit it | |
if processes[id].IsZero() { | |
cmdline, err := ioutil.ReadFile(filepath.Join("/proc", strconv.Itoa(id), "cmdline")) | |
if err != nil { | |
if os.IsNotExist(err) { | |
log.Printf("Process %d went away while we were reading it", id) | |
continue | |
} | |
log.Fatalf("failed to read PID: %v", err) | |
} | |
args := strings.Split(strings.TrimSpace(string(cmdline)), "\000") | |
username := "unknown" | |
if stat, ok := file.Sys().(*syscall.Stat_t); ok { | |
if name, ok := users[stat.Uid]; ok { | |
username = name | |
} | |
} | |
log.Printf("Process %d spawned by %s with arguments %v", id, username, args) | |
} | |
processes[id] = tick | |
} | |
// Expire any process we haven't seen in a second | |
for id, lastSeen := range processes { | |
if !lastSeen.IsZero() && lastSeen.Before(tick) { | |
log.Printf("Process %d seems to have stopped", id) | |
processes[id] = time.Time{} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment