cmd/seccomp-profiler/main.go (365 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"debug/elf"
"encoding/hex"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"sort"
"strings"
"text/template"
"unicode"
yaml "gopkg.in/yaml.v2"
seccomp "github.com/elastic/go-seccomp-bpf"
"github.com/elastic/go-seccomp-bpf/arch"
"github.com/elastic/go-seccomp-bpf/cmd/seccomp-profiler/disasm"
)
type stringSlice []string
func (s *stringSlice) String() string {
return strings.Join(*s, ", ")
}
func (s *stringSlice) Set(value string) error {
list := strings.FieldsFunc(value, func(r rune) bool {
return unicode.IsSpace(r) || r == ',' || r == ';'
})
*s = append(*s, list...)
return nil
}
// Flags.
var (
debug bool
format string
templateFile string
packageName string
blacklist stringSlice
allowList stringSlice
outFile string
)
func init() {
flag.StringVar(&format, "format", "code", "output format (code or config)")
flag.StringVar(&templateFile, "t", "", "custom code template file")
flag.StringVar(&packageName, "pkg", "main", "package name to use in source code")
flag.BoolVar(&debug, "d", false, "add debug to the config output")
flag.Var(&blacklist, "b", "blacklist syscalls by name")
flag.Var(&allowList, "allow", "allow syscalls by name (always include them in the profile)")
flag.StringVar(&outFile, "out", "-", "output filename")
}
func main() {
flag.Parse()
binary := flag.Arg(0)
if binary == "" {
log.Fatal("no binary specified")
}
log.Println("Binary file:", binary)
archInfo, goarch, err := getBinaryArch(binary)
if err != nil {
log.Fatal(err)
}
log.Println("Detected architecture:", archInfo.Name)
hash, err := hashBinary(binary)
if err != nil {
log.Fatal(err)
}
log.Println("SHA256:", hash)
objDump, err := doObjdump(binary, hash)
if err != nil {
log.Fatal(err)
}
log.Println("Objdump File:", objDump)
syscalls, err := disasm.ExtractSyscalls(archInfo, objDump)
if err != nil {
log.Fatal(err)
}
// Deduplicate syscalls.
m := make(map[int]disasm.Syscall, len(syscalls))
for _, s := range syscalls {
m[s.Num] = s
}
var names []string
for _, s := range m {
names = append(names, s.Name)
}
log.Printf("Found %d total syscalls", len(syscalls))
log.Printf("Found %d unique syscalls", len(m))
if len(blacklist) > 0 {
var filtered []string
names, filtered = filterBlacklist(names)
log.Printf("Filtered %d blacklisted syscalls (%v)", len(m)-len(names), strings.Join(filtered, ", "))
}
if len(allowList) > 0 {
size := len(names)
var added []string
names, added = addWhitelist(archInfo, names)
log.Printf("Added %d allowed syscalls (%v)", len(names)-size, strings.Join(added, ", "))
}
sort.Strings(names)
// Open the output.
f, err := openOutput(goarch)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// Write output.
switch format {
case "code":
if err = writeGoTemplate(f, goarch, names); err != nil {
log.Fatal(err)
}
case "config":
if debug {
if err = writeDebugYAML(f, syscalls); err != nil {
log.Fatal(err)
}
}
if err = writeProfileConfig(f, names); err != nil {
log.Fatal(err)
}
default:
log.Fatalf("invalid format=%v", format)
}
}
func getBinaryArch(binary string) (*arch.Info, string, error) {
f, err := os.Open(binary)
if err != nil {
return nil, "", err
}
defer f.Close()
bin, err := elf.NewFile(f)
if err != nil {
return nil, "", err
}
if section := bin.Section(".note.go.buildid"); section == nil {
return nil, "", fmt.Errorf("%v is not a Go binary", binary)
}
libs, err := bin.DynString(elf.DT_NEEDED)
if err != nil {
return nil, "", err
}
if len(libs) > 0 {
log.Println("Binary is dynamically linked with", strings.Join(libs, ", "))
log.Println("WARN: The profiler cannot detect syscalls used in linked libraries.")
}
switch bin.Machine {
case elf.EM_386:
return arch.I386, "386", nil
case elf.EM_ARM:
return arch.ARM, "arm", nil
case elf.EM_X86_64:
return arch.X86_64, "amd64", nil
default:
return nil, "", fmt.Errorf("%v architecture is not supported by go-seccomp-bpf", bin.Machine)
}
}
func hashBinary(binary string) (string, error) {
f, err := os.Open(binary)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, bufio.NewReader(f)); err != nil {
return "", nil
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func cachedDumpFile(binary string) (string, error) {
abs, err := filepath.Abs(binary)
if err != nil {
return "", err
}
h := sha256.New()
if _, err := h.Write([]byte(abs)); err != nil {
return "", err
}
hash := hex.EncodeToString(h.Sum(nil))
hash = hash[:10]
usr, err := user.Current()
if err != nil {
return "", err
}
dumpDir := filepath.Join(usr.HomeDir, ".seccomp-profiler")
if err := os.MkdirAll(dumpDir, 0o700); err != nil {
return "", err
}
return filepath.Join(dumpDir, filepath.Base(binary)+"-"+hash), nil
}
func doObjdump(binary, hash string) (string, error) {
dumpFile, err := cachedDumpFile(binary)
if err != nil {
return "", err
}
f, err := os.Open(dumpFile)
if err == nil {
buf := make([]byte, 256/4)
n, err := f.Read(buf)
f.Close()
if err == nil && n == len(buf) && hash == string(buf) {
log.Println("Using cached objdump.")
return dumpFile, nil
}
}
f, err = os.Create(dumpFile)
if err != nil {
return "", err
}
defer f.Close()
out := bufio.NewWriter(f)
defer out.Flush()
if _, err = out.WriteString(hash + "\n"); err != nil {
return "", err
}
cmd := exec.Command("go", "tool", "objdump", binary)
cmd.Stdout = out
if err = cmd.Run(); err != nil {
return "", err
}
log.Println("objdump written to", dumpFile)
return dumpFile, nil
}
func filterBlacklist(syscalls []string) ([]string, []string) {
filter := make(map[string]struct{}, len(blacklist))
for _, s := range blacklist {
filter[s] = struct{}{}
}
var out []string
var filtered []string
for _, s := range syscalls {
if _, found := filter[s]; !found {
out = append(out, s)
} else {
filtered = append(filtered, s)
}
}
return out, filtered
}
func addWhitelist(archInfo *arch.Info, syscalls []string) ([]string, []string) {
m := make(map[string]struct{}, len(syscalls))
for _, s := range syscalls {
m[s] = struct{}{}
}
var added []string
for _, s := range allowList {
if _, found := archInfo.SyscallNames[s]; found {
_, found := m[s]
if !found {
m[s] = struct{}{}
added = append(added, s)
}
}
}
out := make([]string, 0, len(m))
for s := range m {
out = append(out, s)
}
return out, added
}
func openOutput(goarch string) (io.WriteCloser, error) {
if outFile == "-" {
return os.Stdout, nil
}
t, err := template.New("outFile").Parse(outFile)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
err = t.Execute(buf, map[string]string{
"GOOS": "linux",
"GOARCH": goarch,
})
if err != nil {
return nil, err
}
outFile = buf.String()
log.Println("Output File:", outFile)
dir := filepath.Dir(outFile)
if dir != "" {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
}
return os.Create(outFile)
}
func writeDebugYAML(w io.Writer, syscalls []disasm.Syscall) error {
sort.Slice(syscalls, func(i, j int) bool {
return syscalls[i].Name < syscalls[j].Name
})
debug := struct {
AllSyscalls []disasm.Syscall `yaml:"all_syscalls"`
}{
AllSyscalls: syscalls,
}
data, err := yaml.Marshal(debug)
if err != nil {
return err
}
fmt.Fprintln(w, string(data))
return nil
}
func writeProfileConfig(w io.Writer, syscalls []string) error {
type Config struct {
Seccomp seccomp.Policy `yaml:"seccomp"`
}
config := Config{
Seccomp: seccomp.Policy{
DefaultAction: seccomp.ActionErrno,
Syscalls: []seccomp.SyscallGroup{
{
Action: seccomp.ActionAllow,
Names: syscalls,
},
},
},
}
data, err := yaml.Marshal(config)
if err != nil {
return err
}
fmt.Fprintln(w, string(data))
return nil
}
const defaultTemplate = `// Code generated by seccomp-profiler - DO NOT EDIT.
// {{ printf "+build linux,"}}{{.GOARCH}}
package {{.Package}}
import (
"github.com/elastic/go-seccomp-bpf"
)
var SeccompProfile = seccomp.Policy{
DefaultAction: seccomp.ActionErrno,
Syscalls: []seccomp.SyscallGroup{
{
Action: seccomp.ActionAllow,
Names: []string{
{{- range $syscall := .SyscallNames}}
"{{ $syscall}}",
{{- end}}
},
},
},
}
`
var codeTemplate = template.Must(template.New("profile").Parse(defaultTemplate))
func writeGoTemplate(w io.Writer, goarch string, syscalls []string) error {
t := codeTemplate
if templateFile != "" {
var err error
t, err = template.ParseFiles(templateFile)
if err != nil {
return err
}
}
type Params struct {
Package string
GOARCH string
SyscallNames []string
}
p := Params{
Package: packageName,
GOARCH: goarch,
SyscallNames: syscalls,
}
return t.Execute(w, p)
}