A DSL For Seccomp Rules

rkeene

Roy Keene

Posted on December 17, 2019

A DSL For Seccomp Rules

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
Enter fullscreen mode Exit fullscreen mode

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 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) 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
}
Enter fullscreen mode Exit fullscreen mode

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),
Enter fullscreen mode Exit fullscreen mode

The updated build process looks something like:

...

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

download && dropNetwork && extract && build
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
rkeene
Roy Keene

Posted on December 17, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related