Unveiling OpenBSD pflogd(8)

So I recently committed my unveil(2) work on OpenBSD pflogd(8), in the process I also learned a bit more about tcpdump/pcap filters, mostly thanks to florian@ for finding a case I had missed related to hostnames/DNS.

As a short summary, pflogd(8) is the pf(4) logging daemon. Whenever a pf.conf(5) rule contains the log parameter it is sent to a pflog(4) interface (default: pflog0). It is possible to use tcpdump(8) on this interface directly, but normally it is pflogd(8)'s job to run in the background and write the log to disk for easier review later.

Now that unveil has landed, it's interesting to discuss the benefits of using it in this case. pflogd(8) is one of OpenBSD's early privsep daemons, it takes advantage of several existing security mechanisms, including chroot(2), dropping to a separate user, and BIOCLOCK bpf descriptor locking, which facilitates privilege separating bpf(4) between processes using descriptor passing.

I previously worked on the pflogd(8) codebase last September, along with OpenBSD tcpdump, converting them to the fork+exec model, to benefit more from PIE/ASLR. It didn't take too long to re-familiarize myself with the code.

As a privsep daemon, pflogd(8) consists of two parts:

• The privileged process, which opens a socketpair/pipe for communication between the two processes, forks/execs the unpriv child, does privileged actions on its behalf like opening files and then passing it back those descriptors.

• The unprivileged process only acts on file descriptors it receives, reading in packets from bpf, parsing pcap hdrs, and writing to the output log. This process runs in a chroot(2), as user "_pflogd", and is pledged tightly as stdio recvfd.

Now to unveil(2), as can be seen from above, the unprivileged part of pflogd(8) is already looking pretty decent, it cannot access the filesystem, or much of anything for that matter. It does not need to unveil.

The privileged part is different, it cannot pledge(2) due to some runtime bpf ioctls not currently permitted. It can, however, unveil(2) a limited view of the filesystem and continue to work, as hinted at earlier it needs to be able to read some files for DNS resolution to succeed: /etc/{resolv.conf,hosts,services}, which would normally be permitted by pledge dns, and finally it needs to be able to open the bpf descriptor read-only, and the log file as "rwc" (read, write, create). After that point, unveil(2) can be locked down with unveil(NULL, NULL) preventing further changes.

Now, let's think about that for a moment. This is a root process, but now it has a restricted view of the filesystem, how much protection is this actually providing? It can still call execv(2), for example. But on what? It can't read or execute /bin/sh. You might be thinking, what about that juicy log file? Let's say we write out an ELF binary there, and execv.. EACCES. We never initially unveiled that path as "x" (execute), so it cannot be executed. :-)

So it can't access /, or /bin, or /etc, /dev, not even /tmp! How about creating a device node? That normally won't work either, /var (and hence /var/log) is mounted nodev nosuid by default and cannot contain device nodes or setuid binaries. With that said, make no mistake though, root can still do Bad Things ™, but now at least any potential attacker has more to contend with, meaning they need potentially larger, more sophisticated attacks.

"More Seatbelts" :-)

It's worth noting that this is a fairly special case, where unveil(2) can be used but pledge(2) cannot, however care should be taken when evaluating the needs of a program. Theo de Raadt explains this better here.

In conclusion, I'm continually fascinated by all the new and exciting mitigations and features developed on OpenBSD, like Todd Mortimer's RETGUARD, or MAP_STACK. Whether or not I'm just following along or particpating myself. I hope that others feel encouraged to as well.

Copyright © 2020 Bryan Steele.