💾 Archived View for thrig.me › tech › openbsd › pledge.gmi captured on 2023-06-14 at 15:09:32. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-04-19)
-=-=-=-=-=-=-
The OpenBSD pledge system call is a way to restrict operations that a program can take. The process of adding pledge to a C program is relatively simple. Add a pledge call at the top, maybe after initialization is complete,
if (pledge("stdio", NULL) == -1) err(1, "pledge failed");
then iterate building and running the program, which may be automated with entr(1) so you need only save the file in your editor to trigger a build and test attempt. Recall that unix is the IDE.
$ echo canary-dns.c | entr sh -c 'make canary-dns && ./canary-dns'
Then, inspect /var/log/messages to see why it blows up
Jul 15 23:38:34 leno /bsd: canary-dns[69674]: pledge "rpath", syscall 5
and then modify the pledge list,
if (pledge("rpath stdio", NULL) == -1) err(1, "pledge failed");
and continue until the program mostly stops blowing up.
A more advanced method is to rework the code with multiple pledge calls, as appropriate to what the code is doing, and to move or "hoist" the initialization code to the beginning of the script. But that may take more time than the simple first-pass setup described above.
Sometimes the messages file will not be useful; in this case use ktrace and kdump to determine exactly what the program is failing on. A developer with more experience may be able to help here. A grep for NAMI can help, though it could be that a particular system call is a problem.
$ ktrace ./canary-dns $ kdump | grep NAMI | less ... $ kdump | tail ...
Complicated programs will take more work, and may require multiple sets of pledge and unveil calls, or none at all. Another solution here is to simplify the program; for instance, I removed the "escape to the shell" feature of rogue 3.6.3. This removed the need for "proc" and "exec". That's less attack surface for someone to play with, and escaping to the shell is not really necessary these days given tmux as opposed to your one login session back in the early 1980s.
What other software can be slimmed down, to do less with less?
$ ktrace ./canary-dns ld.so: canary-dns: can't load library 'libcares.so.7.1' Killed
This could be solved by setting LD_LIBRARY_PATH, but that slows down everything that searches for library files (as a ktrace will help show; grep the output for NAMI). A quick fix is to adjust the c-ares pkg-config file as follows:
$ grep Libs: libcares.pc Libs: -Wl,-R ${libdir} -L${libdir} -lcares
Another way to avoid this is to install custom libraries into a directory the build system already knows about, but I prefer to keep the base operating system /usr/{lib,include}, ports system /usr/local/*, and my own custom software depot under ~/usr/OpenBSD7.3-amd64 (or as appropriate to the OS, version, and arch) all strictly separate. The stow port may be of interest.
Some have pondered automation to help build the pledge line. This idea has various problems; for one, you might spend quite some time getting the code parser working okay, and even then it might miss something. Or the automation could allow much more than necessary. How about for a "good enough" start? This still requires time spent writing a parser, versus the method above where you run the program until it mostly stops failing.
Little beats actually looking at and understanding the code, and from that to craft a suitable pledge configuration. One might even rewrite the program to "hoist" all the initialization to the beginning, and then to apply multiple pledge calls thereafter to reduce the program to the minimum necessary permissions. The study of the code may reveal that the program is poorly designed and must be rewritten to better suit pledge—maybe you want an IRC client that cannot make new TCP connections once it is up and running, or one that does not need to make prot_exec calls to load dynamic libraries. These may be difficult to implement in an existing IRC client.
This is not to say that a code scanner of some sort would not be useful, perhaps to help understand the code, maybe to learn how the function calls are laid out in a big codebase. However, this misses questions such as: why is that codebase so large? Could it be shrunk, or abandoned?
But wait, there's more! With unveil allowed
if (pledge("inet rpath stdio unveil", NULL) == -1) err(1, "pledge failed");
a series of unveil calls can restrict filesystem access. Here ktrace really is necessary, as who knows what files the program and libraries involved actually touch, or whether the program will give any sort of error message should a critical file be unreadable.
$ kdump -f ktrace.out | grep NAMI 89250 ktrace NAMI "./canary-dns" 89250 canary-dns NAMI "/etc/resolv.conf" 89250 canary-dns NAMI "/dev/urandom" 89250 canary-dns NAMI "/etc/hosts"
One might deny access to /etc/hosts if one is only interested in letting the program make DNS queries, but otherwise the above might result in:
if (unveil("/dev/urandom", "r") == -1) err(1, "unveil failed"); if (unveil("/etc/hosts", "r") == -1) err(1, "unveil failed"); if (unveil("/etc/resolv.conf", "r") == -1) err(1, "unveil failed"); if (unveil(NULL, NULL) == -1) err(1, "unveil failed");
This results in a program that can make inet calls and interact with stdio and can read three files. Not much effort was required to implement these restrictions.
A program such as w3m may require significant patching due to the default use of the system(3) interface, which runs through sh, which can run most anything an attacker might want to. The downside here is you must manually pull, merge, and compile the program. On the plus side, you end up with a w3m that has a slightly better security stance than the default, one that is closer to the features you really need.
A different method is to wrap a program with a pledge and unveil policy. This may help if the program is difficult to compile. There will only be one policy, which may not be ideal, but some pledge on code that talks to the internet is maybe better than none. Of course you need to ensure that you launch the wrapper, and not the unprotected binary.
An X11 program may rarely want to update the font cache. This will crash a program that does not allow wpath. One workaround is to allow wpath but to deny filesystem writes with unveil. This will cause the font cache update to fail, but will not cause the program to crash. Periodic fc-cache updates especially after messing around with your ~/.fonts directory may help—maybe have a Makefile rule for that?
Another fun issue is when unveil prevents access to /tmp/.X11-unix/* and then the code falls back to making a TCP connection to port 6000 which is then shot down by pledge because inet is not allowed. So unveil can change how the program behaves, which is probably why the programmer really needs to know what the code is doing and how to best manage it.
Various languages have packages (the one for Perl is included by default) that allow access to the pledge and unveil system calls. Such an interface is not difficult to write, though languages that make a lot of system calls will require a long allow list by default. This may reduce the benefit of pledge. Embedded TCL for example requires at least the following, and SBCL is even worse.
if (pledge("getpw rpath stdio tty unix", NULL) == -1) err(1, "pledge failed");
Load both pledge and unveil, and then apply any pledge restrictions; this helps avoid a need to allow prot_exec. If the script must be portable to other operating systems, optionally load the modules.
my $pledged = eval { require OpenBSD::Pledge; require OpenBSD::Unveil; OpenBSD::Unveil->import; 1; }; if ($pledged) { OpenBSD::Pledge::pledge(qw{dns inet rpath unveil}); unveil(qw{/etc/ssl r}); ... unveil(); };
On the other hand, one may learn that modules such as DateTime lazy-load various things, such as the zoneinfo files not from the /usr/share/zoneinfo location but instead from an @INC directory. This may need to be accounted for. ktrace(1) may again be of service. (Lots of perl modules turn out to lazy-load things. This is something of a problem for the pledge model; either you need to force a load to happen before the pledge call, or to loosen up the pledge list which may allow an attacker more access to the system.
Also unveil favors directory permissions; for one, allowing a whole directory keeps the unveil list short. Another, databases such as sqlite may want to write various *.db-wal and *.db-journal files. So, it is easier to place the database file into a unique directory and allow full permissions on that.
tags #openbsd #security #perl