Title: How to add pledge to a program in OpenBSD | |
Author: Solène | |
Date: 08 September 2023 | |
Tags: security openbsd | |
Description: In this article you will learn how to use OpenBSD specific | |
feature pledge to prevent a program to misbehave. | |
# Introduction | |
This article is meant to be a simple guide explaining how to make use | |
of the OpenBSD specific feature pledge in order to restrict a software | |
capabilities for more security. | |
While pledge falls in the sandboxing features, it's different than the | |
traditional sandboxing we are used to see because it happens within the | |
source code itself, and can be really tightened. Actually, many | |
programs requires lot of privileges like reading files, doing DNS | |
etc... when initializing, then those privileges could be removed, this | |
is possible with pledge but not for traditional sandboxing wrappers. | |
In OpenBSD, most of the base userland have support for pledge, and more | |
and more packaged software (including Chromium and Firefox) received | |
some code to add pledge. If a program tries to use a system call that | |
isn't in pledge promises list, it dies and the violation is reported in | |
the system logs. | |
What makes pledge pretty cool is how it's easy to implement it in your | |
software, it has a simple mechanism of system call families so you | |
don't have to worry about listing every system calls, but only their | |
categories (named promises), like reading a file, writing a file, | |
executing binaries etc... | |
OpenBSD manual page for pledge(2) | |
# Let's pledge a program | |
I found a small utility that I will use to illustrate how to add pledge | |
to a program. The program is qprint, a C quoted printable | |
encoder/decoder. This kind of converter is quite easy to pledge | |
because most of the time, they only take an input, do some computation | |
and make an output, they don't run forever and don't do network. | |
qprint official project page | |
## Digging in the sources | |
When extracting the sources, we can find a bunch of files, we will | |
focus at reading the `*.c` files, the first thing we want to find is | |
the function `main()`. | |
It happens the main function is in the file `qprint.c`. It's important | |
to call pledge as soon as possible in the program, most of the time | |
after variable initialization. | |
## Modifying the code | |
Adding pledge to a program requires to understand how it works, because | |
some feature that aren't often used may be broken by pledge, and some | |
programs having live reloading or being able to change behavior during | |
runtime are complicated to pledge. | |
Within the function `main` below variables declaration, We will add a | |
call to pledge for `stdio` because the program can display the result | |
on the output, `rpath` because it can read files and `wpath` as it can | |
also write files. | |
```c | |
#include <unistd.h> | |
[...] | |
pledge("stdio rpath wpath", NULL); | |
``` | |
It's ok, we imported the library providing pledge, and called it from | |
within. But what if the pledge call fails for some reasons? We need | |
to ensure it worked or abort the program. Let's add some checks. | |
``` | |
#include <unistd.h> | |
#include <err.h> | |
[...] | |
if (pledge("stdio rpath wpath", NULL) == -1) { | |
err(1, "pledge call didn't work"); | |
} | |
``` | |
This is a lot better now, if pledge call failed, the program will stop | |
and we will be warned about it. I don't know exactly under which | |
circumstance it could fail, but maybe if promise name changes or | |
doesn't exist anymore in a program, that would be bad if pledge | |
silently failed. | |
## Testing | |
Now we made some changes to the program, we need to verify it's still | |
working as expected. | |
Fortunately, qprint comes with a test suite which can be used with | |
`make wringer`, if the test suite pass and the tests have a good | |
coverage, this mean we may have not break anything. If the test suite | |
fails, we should have an error in the output of `dmesg` telling us why | |
it failed. | |
And, it failed! | |
``` | |
qprint[98802]: pledge "cpath", syscall 5 | |
``` | |
This error (which killed the PID instantly) indicates that the pledge | |
list is missing `cpath`, this makes sense because it has to create new | |
files if you specify an output file. | |
Adding `cpath` to the list, and running the test suite again, all tests | |
pass! Now, we exactly know that the software can't do anything except | |
using the system calls we whitelisted. | |
We could tighten pledge more by dropping `rpath` if the file is read | |
from stdin, and `cpath wpath` if the output is sent to stdout. I left | |
this exercise to the reader :-) | |
## The diff | |
Here is my diff to add pledge support to qprint. | |
``` | |
Index: qprint.c | |
--- qprint.c.orig | |
+++ qprint.c | |
@@ -2,6 +2,8 @@ | |
#line 70 "./qprint.w" | |
#include "config.h" | |
+#include <unistd.h> | |
+#include <err.h> | |
#define REVDATE "16th December 2014" \ | |
@@ -747,6 +749,9 @@ char*cp; | |
+if (pledge("stdio cpath rpath wpath", NULL) == -1) { | |
+ err(1, "pledge error"); | |
+} | |
fi= stdin; | |
fo= stdout; | |
``` | |
# Using pledge in non-C programs | |
It's actually possible to call pledge() in other programming languages, | |
Perl has a library provided in OpenBSD base system that will work out | |
of the box. For some other, such library may be packaged already (for | |
python and Golang at least). If you use something less common, you can | |
define an interface to call the library. | |
OpenBSD manual page for the Perl pledge library | |
Here is an example in Common LISP to create a new function | |
`c-kiosk-pledge`. | |
```common-lisp | |
#+ecl | |
(progn | |
(ffi:clines " | |
#include <unistd.h> | |
void kioskPledge() { | |
pledge(\"dns inet stdio tty rpath\",NULL); | |
} | |
#endif") | |
#+openbsd | |
(ffi:def-function | |
("kioskPledge" c-kiosk-pledge) | |
() :returning :void)) | |
``` | |
# Extra | |
It's possible to find which running programs are currently using | |
pledge() by using `ps auxww | awk '$8 ~ "p" { print }'`, any PID with a | |
state containing `p` indicates it's pledged. | |
If you want to add pledge to a packaged program on OpenBSD, make sure | |
it still fully work. | |
Adding pledge to a program that contain most promises won't be doing | |
much... | |
# Exercise reader | |
Now, if you want to practice, you can tighten the pledge calls to only | |
allow qprint to use the pledge `stdio` only in the case it's used in a | |
pipe for input and output like this: `./qprint < input.txt > | |
output.txt`. | |
Ideally, it should add the pledge `cpath wpath` only when it writes | |
into a file, and `rpath` only when it has to read a file, so in the | |
case of using stdin and stdout, only `stdio` would have been added at | |
the beginning. | |
Good luck, Have fun! Thanks to Brynet@ for the suggestion! | |
# Conclusion | |
The system call pledge() is a wonderful security feature that is | |
reliable, and as it must be done in the source code, the program isn't | |
run from within a sandboxed environment that may be possible to escape. | |
I can't say pledge can't be escaped, but I think it's a lot less | |
likely to be escaped than any other sandbox mechanism (especially since | |
the program immediately dies if it tries to escape). | |
Next time, I'll present its companion system called unveil which is | |
used to restrict access to the filesystem, except some developer | |
defined files. |