💾 Archived View for thrig.me › tech › openbsd › pledge.gmi captured on 2023-04-26 at 13:49:49. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-04-19)
-=-=-=-=-=-=-
The OpenBSD pledge system call is a simple way to restrict operations that a program can take. The process of adding pledge to a C program is relatively simple, provided that the program is simple; simply add a pledge call
if (pledge("stdio", NULL) == -1) err(1, "pledge failed");
then iterate building and running the program, which may be automated with something like the following, so you need only save the file in your editor to trigger a build and test attempt in another terminal (recall that unix is the IDE):
$ echo canary-dns.c | entr sh -c 'make canary-dns && ./canary-dns'
and watching /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");
wash rinse repeat.
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.
$ ktrace ./canary-dns $ kdump | less
Complicated programs will take more work, and may require multiple sets of pledge and unveil calls (or none at all), depending. Another solution here is to simplify the program; for instance, I removed the "escape to shell" feature of rogue 3.6.3 which allowed for a shorter list of allowed operations (no proc and no exec).
$ 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 file 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 OS /usr/{lib,include}, ports system /usr/local/*, and my own custom software depot under ~/usr/OpenBSD7.1-amd64 (or as appropriate to the OS, version, and arch) all strictly separate. The stow port may also be of interest, or something more complicated like the nix package manager, if you want.
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. More could be done via firewall rules to restrict network access, though that would likely get in the way of being able to query arbitrary DNS servers at arbitrary ports, and would not for this program improve the security; security systems that get in my way are not a way I like to go: selinux would helpfully and quite often block SSH access unless you made periodic offerings to some command or the other. Eventually selinux got itself disabled.
Programs like irssi or w3m may be difficult to apply a generic security policy to; one option here is to compile a custom version with a custom set of pledge and unveil calls. For instance one could specify that irssi should have no permission to make any filesystem writes, which is good for security, but bad for configuration file updates. An attacker may want to write something to your configuration file, for instance. Others may want irssi to be able to write to ~/.irssi and to a logfile directory, but certainly nowhere else. Another may want the prior, and also a downloads directory for files sent via DCC.
A program like w3m may require significant patching to restrict the exec calls, due to the default use of the system(3) interface for all external programs, which runs through sh, and can then run most anything an attacker might want to. TODO work more on this part.
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 here.
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 which does not like the inet call involved.
Various languages have packages (the one for Perl is included in the base OS) that allow access to the pledge and unveil system calls. Such an interface is generally not difficult to write (I've written such for Common LISP and TCL), though languages that make a lot of system calls as part of their basic environment will require a long allow list by default. This may reduce the benefit of pledge. Embedded TCL for example requires at least:
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 $pledge = eval { require OpenBSD::Pledge; require OpenBSD::Unveil; OpenBSD::Unveil->import; 1; }; if ($pledge) { 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.
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