| 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. |