1// Program captrace traces processes and notices when they attempt 2// kernel actions that require Effective capabilities. 3// 4// The reference material for developing this tool was the the book 5// "Linux Observabililty with BPF" by David Calavera and Lorenzo 6// Fontana. 7package main 8 9import ( 10 "bufio" 11 "flag" 12 "fmt" 13 "io" 14 "log" 15 "os" 16 "os/exec" 17 "strconv" 18 "strings" 19 "sync" 20 "syscall" 21 "time" 22 23 "kernel.org/pub/linux/libs/security/libcap/cap" 24) 25 26var ( 27 bpftrace = flag.String("bpftrace", "bpftrace", "command to launch bpftrace") 28 debug = flag.Bool("debug", false, "more output") 29 pid = flag.Int("pid", -1, "PID of target process to trace (-1 = trace all)") 30) 31 32type thread struct { 33 PPID, Datum int 34 Value cap.Value 35 Token string 36} 37 38// mu protects these two maps. 39var mu sync.Mutex 40 41// tids tracks which PIDs we are following. 42var tids = make(map[int]int) 43 44// cache tracks in-flight cap_capable invocations. 45var cache = make(map[int]*thread) 46 47// event adds or resolves a capability event. 48func event(add bool, tid int, th *thread) { 49 mu.Lock() 50 defer mu.Unlock() 51 52 if len(tids) != 0 { 53 if _, ok := tids[th.PPID]; !ok { 54 if *debug { 55 log.Printf("dropped %d %d %v event", th.PPID, tid, *th) 56 } 57 return 58 } 59 tids[tid] = th.PPID 60 tids[th.PPID] = th.PPID 61 } 62 63 if add { 64 cache[tid] = th 65 } else { 66 if b, ok := cache[tid]; ok { 67 detail := "" 68 if th.Datum < 0 { 69 detail = fmt.Sprintf(" (%v)", syscall.Errno(-th.Datum)) 70 } 71 task := "" 72 if th.PPID != tid { 73 task = fmt.Sprintf("+{%d}", tid) 74 } 75 log.Printf("%-16s %d%s opt=%d %q -> %d%s", b.Token, b.PPID, task, b.Datum, b.Value, th.Datum, detail) 76 } 77 delete(cache, tid) 78 } 79} 80 81// tailTrace tails the bpftrace command output recognizing lines of 82// interest. 83func tailTrace(cmd *exec.Cmd, out io.Reader) { 84 launched := false 85 sc := bufio.NewScanner(out) 86 for sc.Scan() { 87 fields := strings.Split(sc.Text(), " ") 88 if len(fields) < 4 { 89 continue // ignore 90 } 91 if !launched { 92 launched = true 93 mu.Unlock() 94 } 95 switch fields[0] { 96 case "CB": 97 if len(fields) < 6 { 98 continue 99 } 100 pid, err := strconv.Atoi(fields[1]) 101 if err != nil { 102 continue 103 } 104 th := &thread{ 105 PPID: pid, 106 } 107 tid, err := strconv.Atoi(fields[2]) 108 if err != nil { 109 continue 110 } 111 c, err := strconv.Atoi(fields[3]) 112 if err != nil { 113 continue 114 } 115 th.Value = cap.Value(c) 116 aud, err := strconv.Atoi(fields[4]) 117 if err != nil { 118 continue 119 } 120 th.Datum = aud 121 th.Token = strings.Join(fields[5:], " ") 122 event(true, tid, th) 123 case "CE": 124 if len(fields) < 4 { 125 continue 126 } 127 pid, err := strconv.Atoi(fields[1]) 128 if err != nil { 129 continue 130 } 131 th := &thread{ 132 PPID: pid, 133 } 134 tid, err := strconv.Atoi(fields[2]) 135 if err != nil { 136 continue 137 } 138 aud, err := strconv.Atoi(fields[3]) 139 if err != nil { 140 continue 141 } 142 th.Datum = aud 143 event(false, tid, th) 144 default: 145 if *debug { 146 fmt.Println("unparsable:", fields) 147 } 148 } 149 } 150 if err := sc.Err(); err != nil { 151 log.Fatalf("scanning failed: %v", err) 152 } 153} 154 155// tracer invokes bpftool it returns an error if the invocation fails. 156func tracer() (*exec.Cmd, error) { 157 cmd := exec.Command(*bpftrace, "-e", `kprobe:cap_capable { 158 printf("CB %d %d %d %d %s\n", pid, tid, arg2, arg3, comm); 159} 160kretprobe:cap_capable { 161 printf("CE %d %d %d\n", pid, tid, retval); 162}`) 163 out, err := cmd.StdoutPipe() 164 cmd.Stderr = os.Stderr 165 if err != nil { 166 return nil, fmt.Errorf("unable to create stdout for %q: %v", *bpftrace, err) 167 } 168 mu.Lock() // Unlocked on first ouput from tracer. 169 if err := cmd.Start(); err != nil { 170 return nil, fmt.Errorf("failed to start %q: %v", *bpftrace, err) 171 } 172 go tailTrace(cmd, out) 173 return cmd, nil 174} 175 176func main() { 177 flag.Usage = func() { 178 fmt.Fprintf(flag.CommandLine.Output(), `Usage: %s [options] [command ...] 179 180This tool monitors cap_capable() kernel execution to summarize when 181Effective Flag capabilities are checked in a running process{thread}. 182The monitoring is performed indirectly using the bpftrace tool. 183 184Each line logged has a timestamp at which the tracing program is able to 185summarize the return value of the check. A return value of " -> 0" implies 186the check succeeded and confirms the process{thread} does have the 187specified Effective capability. 188 189The listed "opt=" value indicates some auditing context for why the 190kernel needed to check the capability was Effective. 191 192Options: 193`, os.Args[0]) 194 flag.PrintDefaults() 195 } 196 flag.Parse() 197 198 tr, err := tracer() 199 if err != nil { 200 log.Fatalf("failed to start tracer: %v", err) 201 } 202 203 mu.Lock() 204 205 if *pid != -1 { 206 tids[*pid] = *pid 207 } else if len(flag.Args()) != 0 { 208 args := flag.Args() 209 cmd := exec.Command(args[0], args[1:]...) 210 cmd.Stdin = os.Stdin 211 cmd.Stdout = os.Stdout 212 cmd.Stderr = os.Stderr 213 if err := cmd.Start(); err != nil { 214 log.Fatalf("failed to start %v: %v", flag.Args(), err) 215 } 216 tids[cmd.Process.Pid] = cmd.Process.Pid 217 218 // waiting for the trace to complete is racy, so we sleep 219 // to obtain the last events then kill the tracer and wait 220 // for it to exit. Defers are in reverse order. 221 defer tr.Wait() 222 defer tr.Process.Kill() 223 defer time.Sleep(1 * time.Second) 224 225 tr = cmd 226 } 227 228 mu.Unlock() 229 tr.Wait() 230} 231