blob: 1ef1ace9df9a88a14ecd0ca7d12060fd3ca484c0 [file] [log] [blame]
// 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()
}