Go Seccomp Filters – Part 2

Estimated reading time: 7 Minutes

In the previous post you walked through an explanation on what syscalls are, what is seccomp and specifically what is a seccomp filter.

If you are unfamiliar with seccomp filters or syscalls, I suggest you read the first part and then come back here.

In this post, you’ll implement a seccomp filter on a Go binary.

All of the code used below is available under this GitHub repository.

Verifying seccomp support

In order to verify seccomp is supported on your kernel, run the following command:

$ sudo grep CONFIG_SECCOMP= /boot/config-$(uname -r)

If the output is CONFIG_SECCOMP=y then your kernel supports seccomp and you’re good to go.

If you encounter an error such as this one when running one of the examples below:

Package libseccomp was not found in the pkg-config search path.
Perhaps you should add the directory containing `libseccomp.pc'
to the PKG_CONFIG_PATH environment variable
No package 'libseccomp' found

Run the following command to install the seccomp package on your machine:

$ apt-get install libseccomp-dev

Keeping it simple

In order to keep this post focused on protecting your application, and not the application itself, we would want it to be rather simple.

Take a look at the following function:

const dirPath = "/tmp/info"
// Running the whitelisted code
if err := syscall.Mkdir(dirPath, 0600); err != nil {
	fmt.Printf("Failed creating directory: %v\n", err)
	return
}
fmt.Printf("Directory %q created successfully\n", dirPath)

The function does the following:

  1. Creates a directory using the syscall package – this is done in order to have the smallest amount of syscalls generated by our function.
  2. The directory is created under the tmp directory which is a temporary folder.
  3. Next we either print an error or a success message, that’s it.

Let’s start by building a binary that runs just this function:

$ go build main.go
$ ./main
Directory "/tmp/info" created successfully

As you would imagine, running this program again without deleting the folder created will yield an error:

Failed creating directory: file exists

We do not remove the folder as part of our code for simplicity sake, so just remember to delete it manually prior to every run.

Down to business

Let’s get a list of the syscalls generated by our simple binary:

$ strace -cfq ./main 
Folder created successfully
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 46.79    0.000073          18         4           futex
 19.87    0.000031          31         1           mkdirat
 15.38    0.000024          24         1           nanosleep
  7.05    0.000011          11         1           readlinkat
  3.85    0.000006           6         1           write
  1.92    0.000003           0        19           mmap
  1.92    0.000003           1         3           fcntl
  1.28    0.000002           0         8           sigaltstack
  0.64    0.000001           0        11           rt_sigprocmask
  0.64    0.000001           0         4           arch_prctl
  0.64    0.000001           0         7           gettid
  0.00    0.000000           0         1           read
  0.00    0.000000           0         1           close
  0.00    0.000000           0       114           rt_sigaction
  0.00    0.000000           0         3           clone
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           uname
  0.00    0.000000           0         4           mlock
  0.00    0.000000           0         1           sched_getaffinity
  0.00    0.000000           0         1           openat
------ ----------- ----------- --------- --------- ----------------
100.00    0.000156                   187           total

The next thing we will need, is a way to filter out all syscalls that do not appear in this list. That means we’re going to whitelist the above syscalls.

In order to do that, we will use libseccomp-golang, which is a library that provides a Go based interface to the libseccomp library.

Install the library using the following command:

$ go get github.com/seccomp/libseccomp-golang

Let’s go over the function that creates the filter, line by line:

func loadSeccompFilter() error {
	whitelist := []string{
		"futex", "mkdirat", "nanosleep", "readlinkat",
		"write", "mmap", "fcntl", "sigaltstack",
		"rt_sigprocmask", "arch_prctl", "gettid",
		"read", "close", "rt_sigaction", "clone",
		"execve", "uname", "mlock", "sched_getaffinity", "openat",
	}

	// The filter defaults to fail all syscalls
	filter, err := seccomp.NewFilter(seccomp.ActErrno.SetReturnCode(int16(syscall.EPERM)))
	if err != nil {
		return err
	}
	// Whitelist relevant syscalls and load the filter
	for _, name := range whitelist {
		syscallID, err := seccomp.GetSyscallFromName(name)
		if err != nil {
			return err
		}
		err = filter.AddRule(syscallID, seccomp.ActAllow)
		if err != nil {
			return err
		}
	}
	return filter.Load()
}

  • Lines #2-#7 is the list of the syscalls composed by the list you just printed out above. These are the syscalls to be whitelisted.
  • Line #11 is the “block all syscalls” filter. Meaning, that if this filter is applied without any whitelisting – after loading it, all executed syscall(s) would fail.
  • Line #17 translates a syscall name to an ID. We didn’t go over it in the previous post since it has some complexity which is not relevant at this point. The only thing you need to know now is that such mapping exists.
  • Line #21 is the actual filter rule. Here we apply the “allow” option to the syscall we want to whitelist.
  • Last thing left for you to do is load the filter (line #26), and that’s it.

Now you can try and run your program again, with the seccomp filter, and verify everything works as expected.

Someone call security!

Now that we have everything in place, let’s see what happens if we try to execute a syscall which wasn’t whitelisted.

Let’s say that now our code also runs:

// Trying to run non whitelisted syscall
fmt.Println("Get current working directory")
wd, err := syscall.Getwd()
if err != nil {
	fmt.Printf("Failed getting current working directory: %v\n", err)
	return
}
fmt.Printf("Current working directory is: %s\n", wd)

Try running the code above and you will get the following output:

Directory "/tmp/info" created successfully
Trying to get current working directory
Failed getting current working directory: operation not permitted

Great! as expected, our program cannot run any syscalls other than the ones listed above.

As an exercise for you, try to understand which syscall(s) caused your program to fail, and whitelist them as well.

Key notes

Adding a seccomp filter to your Go binaries would definitely improve your applications security posture, and, as you can see, it’s very simple.

Note some of the key pitfalls:

  1. You need to be able to list all of the syscalls which your application is producing. For complex applications, This could be a very long list. For multi-threaded applications it could be very hard to obtain.
  2. Seccomp filter mode is set per-thread, requiring each thread in a process to configure seccomp filtering independently. Which, on your end, requires you to be able to identify which thread generates which syscalls.

Do keep in mind that having a seccomp filter does not rid you of all other security pro cautions, this is simply another tool for you to use.

As always, feel free to ask me anything. Comment and share at will.

See you on the next one!

Leave a Reply