| 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...
|
|
# 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.
|
|
## 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
[...]
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
#include
[...]
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
+#include
#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.
|
|
Here is an example in Common LISP to create a new function
`c-kiosk-pledge`.
```common-lisp
#+ecl
(progn
(ffi:clines "
#include
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. |