A DSL For Seccomp Rules
Tags: security, linux
Published: 2019-12-17 22:45:23

Recently I started to improve my package building system.  It's a pretty simple build system that downloads source files from the Internet, verifies their SHA-2 against known values for that version of the package, extracts, and compiles them.  Something like:

```
url='...'
sha256='...'
destdir='...'

function verify() { ... }

function download() {
    curl -sSL "${url}" > src.new && \
        verify src.new "${sha256}" && \
        mv src.new src && \
        return 0
}

function extract() {
    gzip -dc src | tar -xf - && \
        mv foo/* . && \
        return 0
}

function build() {
    ./configure && make install DESTDIR="${destdir}"
}

download && extract && build
```

But recently I discovered that some packages would try to go out to
the Internet and attempt to download software without the SHA-2 being
verified, which could lead to unpredictable results.

I wanted to address this but keep the basic process intact.

What I decided to do was create a bash extension, so that when I loaded
the shared object into bash no more network access was permitted
by the shell or any of its child processes.

This can easily be accomplished using [`seccomp`](https://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt) so I started about writing the rules.

`seccomp` works by attaching a fragment of code using the instruction set specified by [Berkley Packet Filter (BPF)](https://en.wikipedia.org/wiki/Berkeley_Packet_Filter) to the path between a process and the Linux system call interface.  When a new system call is made by a process, the Linux kernel starts a BPF virtual machine and runs the code to determine the result (e.g., allow the system call, kill the process, return an error).

Writing code for that instruction set by manually typing in each op code is tedious, but there are some C macros defined to make it a bit higher-level.  However, even with those high-level interfaces it's still not that great of an experience, still very close to writing in an assembly language.

Initially I hard-coded the rules using these macros, but I wanted to make something a bit more flexible so I created a domain specific language for creating the rules.

The language looks a bit like Tcl (and is indeed parsed by a Tcl script).  The ruleset I came up for this is:

```
i386 {
	if {$nr eq "socketcall"} {
		return errno ENOSYS
	}

	if {$nr eq "socket"} {
		if {$args(0) != PF_LOCAL} {
			return errno EINVAL
		}
	}

	return allow
}

x86_64 {
	if {$nr eq "socket"} {
		if {$args(0) != PF_LOCAL} {
			return errno EINVAL
		}
	}

	return allow
}
```

Which basically says on the `i386` platform return `ENOSYS` for the system call `socketcall()` (since there is no good way to deal with
its arguments via `seccomp`), otherwise for both `i386` and `x86_64` platforms reject the `socket()` system call with `EINVAL` unless the first argument is `PF_LOCAL`.  All other system calls are permitted.

The resulting rules look like:
```
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, arch))), /* Load architecture */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_I386, 2, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 10, 0),
BPF_STMT(BPF_RET, SECCOMP_RET_TRAP),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))), /* if ($nr eq "socketcall") ... */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 102, 0, 1),
BPF_STMT(BPF_RET, SECCOMP_RET_ERRNO | ENOSYS),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))), /* if ($nr eq "socket") ... */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 359, 0, 3),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, args[0]))), /* if ($args(0) != PF_LOCAL) ... */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, PF_LOCAL, 1, 0),
BPF_STMT(BPF_RET, SECCOMP_RET_ERRNO | EINVAL),
BPF_STMT(BPF_RET, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))), /* if ($nr eq "socket") ... */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 41, 0, 3),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, args[0]))), /* if ($args(0) != PF_LOCAL) ... */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, PF_LOCAL, 1, 0),
BPF_STMT(BPF_RET, SECCOMP_RET_ERRNO | EINVAL),
BPF_STMT(BPF_RET, SECCOMP_RET_ALLOW),
```

The updated build process looks something like:

```
...

function dropNetwork() {
    enable -f /path/to/dropnet.so dropnet
}

download && dropNetwork && extract && build
```

The rest of the `dropnet` code is pretty trivial, it really just
loads the `seccomp` filter as part of initialization of the
shared object, but it's very effective in keeping build processes
from reaching out to the Internet and we are one step closer to ensuring
reproducible builds.

Full source for [dropnet can be found here](https://chiselapp.com/user/rkeene/repository/bash-drop-network-access/dir?ci=trunk&type=tree).