| // Program captrace traces processes and notices when they attempt |
| // kernel actions that require Effective capabilities. |
| // |
| // The reference material for developing this tool was the the book |
| // "Linux Observabililty with BPF" by David Calavera and Lorenzo |
| // Fontana. |
| package main |
| |
| import ( |
| "bufio" |
| "flag" |
| "fmt" |
| "io" |
| "log" |
| "os" |
| "os/exec" |
| "strconv" |
| "strings" |
| "sync" |
| "syscall" |
| "time" |
| |
| "kernel.org/pub/linux/libs/security/libcap/cap" |
| ) |
| |
| var ( |
| bpftrace = flag.String("bpftrace", "bpftrace", "command to launch bpftrace") |
| debug = flag.Bool("debug", false, "more output") |
| pid = flag.Int("pid", -1, "PID of target process to trace (-1 = trace all)") |
| ) |
| |
| type thread struct { |
| PPID, Datum int |
| Value cap.Value |
| Token string |
| } |
| |
| // mu protects these two maps. |
| var mu sync.Mutex |
| |
| // tids tracks which PIDs we are following. |
| var tids = make(map[int]int) |
| |
| // cache tracks in-flight cap_capable invocations. |
| var cache = make(map[int]*thread) |
| |
| // event adds or resolves a capability event. |
| func event(add bool, tid int, th *thread) { |
| mu.Lock() |
| defer mu.Unlock() |
| |
| if len(tids) != 0 { |
| if _, ok := tids[th.PPID]; !ok { |
| if *debug { |
| log.Printf("dropped %d %d %v event", th.PPID, tid, *th) |
| } |
| return |
| } |
| tids[tid] = th.PPID |
| tids[th.PPID] = th.PPID |
| } |
| |
| if add { |
| cache[tid] = th |
| } else { |
| if b, ok := cache[tid]; ok { |
| detail := "" |
| if th.Datum < 0 { |
| detail = fmt.Sprintf(" (%v)", syscall.Errno(-th.Datum)) |
| } |
| task := "" |
| if th.PPID != tid { |
| task = fmt.Sprintf("+{%d}", tid) |
| } |
| log.Printf("%-16s %d%s opt=%d %q -> %d%s", b.Token, b.PPID, task, b.Datum, b.Value, th.Datum, detail) |
| } |
| delete(cache, tid) |
| } |
| } |
| |
| // tailTrace tails the bpftrace command output recognizing lines of |
| // interest. |
| func tailTrace(cmd *exec.Cmd, out io.Reader) { |
| launched := false |
| sc := bufio.NewScanner(out) |
| for sc.Scan() { |
| fields := strings.Split(sc.Text(), " ") |
| if len(fields) < 4 { |
| continue // ignore |
| } |
| if !launched { |
| launched = true |
| mu.Unlock() |
| } |
| switch fields[0] { |
| case "CB": |
| if len(fields) < 6 { |
| continue |
| } |
| pid, err := strconv.Atoi(fields[1]) |
| if err != nil { |
| continue |
| } |
| th := &thread{ |
| PPID: pid, |
| } |
| tid, err := strconv.Atoi(fields[2]) |
| if err != nil { |
| continue |
| } |
| c, err := strconv.Atoi(fields[3]) |
| if err != nil { |
| continue |
| } |
| th.Value = cap.Value(c) |
| aud, err := strconv.Atoi(fields[4]) |
| if err != nil { |
| continue |
| } |
| th.Datum = aud |
| th.Token = strings.Join(fields[5:], " ") |
| event(true, tid, th) |
| case "CE": |
| if len(fields) < 4 { |
| continue |
| } |
| pid, err := strconv.Atoi(fields[1]) |
| if err != nil { |
| continue |
| } |
| th := &thread{ |
| PPID: pid, |
| } |
| tid, err := strconv.Atoi(fields[2]) |
| if err != nil { |
| continue |
| } |
| aud, err := strconv.Atoi(fields[3]) |
| if err != nil { |
| continue |
| } |
| th.Datum = aud |
| event(false, tid, th) |
| default: |
| if *debug { |
| fmt.Println("unparsable:", fields) |
| } |
| } |
| } |
| if err := sc.Err(); err != nil { |
| log.Fatalf("scanning failed: %v", err) |
| } |
| } |
| |
| // tracer invokes bpftool it returns an error if the invocation fails. |
| func tracer() (*exec.Cmd, error) { |
| cmd := exec.Command(*bpftrace, "-e", `kprobe:cap_capable { |
| printf("CB %d %d %d %d %s\n", pid, tid, arg2, arg3, comm); |
| } |
| kretprobe:cap_capable { |
| printf("CE %d %d %d\n", pid, tid, retval); |
| }`) |
| out, err := cmd.StdoutPipe() |
| cmd.Stderr = os.Stderr |
| if err != nil { |
| return nil, fmt.Errorf("unable to create stdout for %q: %v", *bpftrace, err) |
| } |
| mu.Lock() // Unlocked on first ouput from tracer. |
| if err := cmd.Start(); err != nil { |
| return nil, fmt.Errorf("failed to start %q: %v", *bpftrace, err) |
| } |
| go tailTrace(cmd, out) |
| return cmd, nil |
| } |
| |
| func main() { |
| flag.Usage = func() { |
| fmt.Fprintf(flag.CommandLine.Output(), `Usage: %s [options] [command ...] |
| |
| This tool monitors cap_capable() kernel execution to summarize when |
| Effective Flag capabilities are checked in a running process{thread}. |
| The monitoring is performed indirectly using the bpftrace tool. |
| |
| Each line logged has a timestamp at which the tracing program is able to |
| summarize the return value of the check. A return value of " -> 0" implies |
| the check succeeded and confirms the process{thread} does have the |
| specified Effective capability. |
| |
| The listed "opt=" value indicates some auditing context for why the |
| kernel needed to check the capability was Effective. |
| |
| Options: |
| `, os.Args[0]) |
| flag.PrintDefaults() |
| } |
| flag.Parse() |
| |
| tr, err := tracer() |
| if err != nil { |
| log.Fatalf("failed to start tracer: %v", err) |
| } |
| |
| mu.Lock() |
| |
| if *pid != -1 { |
| tids[*pid] = *pid |
| } else if len(flag.Args()) != 0 { |
| args := flag.Args() |
| cmd := exec.Command(args[0], args[1:]...) |
| cmd.Stdin = os.Stdin |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Start(); err != nil { |
| log.Fatalf("failed to start %v: %v", flag.Args(), err) |
| } |
| tids[cmd.Process.Pid] = cmd.Process.Pid |
| |
| // waiting for the trace to complete is racy, so we sleep |
| // to obtain the last events then kill the tracer and wait |
| // for it to exit. Defers are in reverse order. |
| defer tr.Wait() |
| defer tr.Process.Kill() |
| defer time.Sleep(1 * time.Second) |
| |
| tr = cmd |
| } |
| |
| mu.Unlock() |
| tr.Wait() |
| } |