Pledge: OpenBSD’s defensive approach to OS Security

pledge() — mitigation mechanism

Hello readers,

Today, I would like to introduce you all to the pledge system call which is used to restrict system operations, and is supported only on OpenBSD currently.

I am learning about OpenBSD kernel development now, and wish to share a few tips about how to start OpenBSD kernel development.

Let me first introduce you all to our new friends, who will be helping us in learning of OpenBSD kernel internals:

  • First, the book → “The design and implementation of the *BSD operating system” by Kirk McKusick.
  • Second, OpenBSD source code.
  • Third, man pages and few presentations and papers on OpenBSD.
  • Fourth, goto step2(Second). If you have some doubt or confusion then ask in mailing lists and OpenBSD Facebook group.
What is pledge?

The meaning of Pledge is same as in the real world, that is, “a solemn promise or undertaking”.

So, in OpenBSD:

Calling pledge in a program means to promise that the program will only use certain resources.

How does it make a program more secure?

It limits the operation of a program. Example:

You wrote a program named ‘abc’ that only needed the stdio to just print something to stdout.

  • You added pledge to use only stdio and nothing else.
  • Then, a malicious user found out that there is a vulnerability in your program which one can exploit and get into shell (or root shell).
  • Exploiting your program to open a shell (or root shell) will result in the kernel killing the process with SIGABRT (which cannot be caught/ignored) and will generate a log (which you can find with dmesg).

This happens because before executing other codes of your program, the code first pledges not to use anything other than stdio promise/operations. But, opening a shell or root shell will call several other system-calls which are distributed in lots of other promises like “stdio”, “proc”, “exec” etc. They are all forbidden because the program has already promised not to use any promises other than stdio.

Pledge is not a system call filter. So, it is not used to restrict system calls.
For example,

  • pledge(“read”,NULL)wrong syntax of the pledge()
  • pledge(“stdio inet”,NULL)correct syntax of the pledge()

Pledge works on stdio, dns, inet, etc. promises but not directly on system calls like read, write, etc. And, unique functionality of pledge() is that it works on behavioral approach not just like 1:1 approach with the system calls.

On 11 December 2017, Theo de Raadt said:

List: openbsd-tech
Subject:
pledge execpromises
From:
Theo de Raadt <deraadt () openbsd ! org>
Date:
2017–12–11 21:20:51
Message-ID:
6735.1513027251 () cvs ! openbsd ! org
This will probably be committed in the next day or so.
The 2nd argument of pledge() becomes execpromises, which is what
will gets activated after execve.
There is also a small new feature called “error”, which causes
violating system calls to return -1 with ENOSYS rather than killing
the process. This must be used with EXTREME CAUTION because libraries
and programs are full of unchecked system calls. If you carry on past
one of these failures, your program is in uncharted territory and
risks of exploitation become high.
“error” is being introduced for a different reason: The pre-exec
process’s expectation of what the post-exec process will do might
mismatch, so “error” allows things like starting an editor which has
no network access or maybe other restrictions in the future…

First it was:

#include <unistd.h>
int pledge(const char *promises, const char *paths[]);

Now,

#include <unistd.h>
int pledge(
const char *promises, const char *execpromises);

But, as per OpenBSD 6.2-stable, developers are still using pledge(const char *promises, const char *paths[]) system call. I hope you will see changes in upcoming OpenBSD versions. So, for now, I will focus only on the current pledge system call, which is in OpenBSD 6.2-stable.

How to use pledge() in a program?

Let’s take a simple hello world example:

#include <unistd.h>
#include <stdio.h>
int
main() {
if(pledge("stdio",NULL) == -1) {
err(1,”pledge”);
}
printf(“Pledged\n”);
return 0;
}

In the above example, the program takes a pledge that it will only use stdio operations.
Now, suppose, if the above program tries to open network socket(), then the kernel will kill this program with SIGABRT signal.

Let’s take another example:

#include <unistd.h>
#include <stdio.h>
int
main() {
if(pledge("",NULL) == -1) {
err(1,”pledge”);
}
printf(“Pledged\n”);
return 0;
}

Now, in this case, there is nothing in the first parameter of pledge() system call, like in the above code. According to OpenBSD pledge man page, “A promises value of “” restricts the process to the _exit(2) system call.”

Output of above two codes
Little Introduction about the working of pledge() system call (under the hood — Kernel Level)

This part is a little difficult to understand at first.

I am very thankful to OpenBSD developers like Marc Espie, Benny Löfgren, Bob Beck, Stuart Henderson and Otto Moerbeek for giving their precious time to clear my confusion on kernel level working of pledge system call.

pledge(“stdio”, NULL); or pledge(“stdio inet proc route dns”, NULL);

Steps:

  • First, this full string in pedge() system call is divided into words like “stdio” or “ ‘stdio’, ‘inet’, ‘proc’, ‘route’, ‘dns’ ”.
  • Second, these words will be checked in the pledgereq[] array and then if found, their specified flags will return.
    Below is the pledgereq[] array:
pledgereq[] array
  • Third, pledgereq array contains macro for every word; for example, 
    “stdio” → PLEDGE_STDIO. Now, these macros expand into their specific hex pledge value, like PLEDGE_STDIO → 0x0000000000000008ULL.
  • For other macros and their expansions, you may have a look at the screenshot:
PLEDGE_* macros
  • Fourth, now, all PLEDGE_* macros content will “or” or “|” with each other, according to the pledge() string in user-space code.
    Like,

#Below pseudo algorithm of logic

uint64_t flags=0
for content_of_PLEDGE_macro from [“stdio”, “inet”, “proc”, “dns”, “proc”, “route”]
        flags |= content_of_PLEDGE_macro
ps_pledge = flags

I have also implemented the logic behind the calculation of pledge_bit or pledge value in kernel code using python for demonstration:

ubroot@DESKTOP-2AB4AM0:~$ cat pledge_python.py
import sys
PLEDGE_ALWAYS    =  0xffffffffffffffff  #/* pledge always */
PLEDGE_RPATH = 0x0000000000000001 #/* allow open for read */
PLEDGE_WPATH = 0x0000000000000002 #/* allow open for write */
PLEDGE_CPATH = 0x0000000000000004 #/* allow creat, mkdir, unlink etc */
PLEDGE_STDIO = 0x0000000000000008 #/* operate on own pid */
PLEDGE_TMPPATH = 0x0000000000000010 #/* for mk*temp() */
PLEDGE_DNS = 0x0000000000000020 # /* DNS services */
PLEDGE_INET = 0x0000000000000040 # /* AF_INET/AF_INET6 sockets */
PLEDGE_FLOCK = 0x0000000000000080 # /* file locking */
PLEDGE_UNIX = 0x0000000000000100 # /* AF_UNIX sockets */
PLEDGE_ID = 0x0000000000000200 # /* allow setuid, setgid, etc */
PLEDGE_TAPE = 0x0000000000000400 # /* Tape ioctl */
PLEDGE_GETPW = 0x0000000000000800 # /* YP enables if ypbind.lock */
PLEDGE_PROC = 0x0000000000001000 # /* fork, waitpid, etc */
PLEDGE_SETTIME = 0x0000000000002000 # /* able to set/adj time/freq */
PLEDGE_FATTR = 0x0000000000004000 # /* allow explicit file st_* mods */
PLEDGE_PROTEXEC = 0x0000000000008000 # /* allow use of PROT_EXEC */
PLEDGE_TTY = 0x0000000000010000 # /* tty setting */
PLEDGE_SENDFD = 0x0000000000020000 # /* AF_UNIX CMSG fd sending */
PLEDGE_RECVFD = 0x0000000000040000 # /* AF_UNIX CMSG fd receiving */
PLEDGE_EXEC = 0x0000000000080000 # /* execve, child is free of pledge */
PLEDGE_ROUTE = 0x0000000000100000 # /* routing lookups */
PLEDGE_MCAST = 0x0000000000200000 # /* multicast joins */
PLEDGE_VMINFO = 0x0000000000400000 # /* vminfo listings */
PLEDGE_PS = 0x0000000000800000 # /* ps listings */
PLEDGE_DISKLABEL = 0x0000000002000000 #/* disklabels */
PLEDGE_PF = 0x0000000004000000 # /* pf ioctls */
PLEDGE_AUDIO = 0x0000000008000000 # /* audio ioctls */
PLEDGE_DPATH = 0x0000000010000000 # /* mknod & mkfifo */
PLEDGE_DRM = 0x0000000020000000 # /* drm ioctls */
PLEDGE_VMM = 0x0000000040000000 # /* vmm ioctls */
PLEDGE_CHOWN = 0x0000000080000000 # /* chown(2) family */
PLEDGE_CHOWNUID = 0x0000000100000000 # /* allow owner/group changes */
PLEDGE_BPF = 0x0000000200000000 # /* bpf ioctl */
PLEDGE_ERROR = 0x0000000400000000 # /* ENOSYS instead of kill */
pledgereq = {   "audio"     :  PLEDGE_AUDIO,
"bpf" : PLEDGE_BPF,
"chown" : PLEDGE_CHOWN | PLEDGE_CHOWNUID,
"cpath" : PLEDGE_CPATH,
"disklabel" : PLEDGE_DISKLABEL,
"dns" : PLEDGE_DNS,
"dpath" : PLEDGE_DPATH,
"drm" : PLEDGE_DRM,
"exec" : PLEDGE_EXEC,
"fattr" : PLEDGE_FATTR | PLEDGE_CHOWN,
"flock" : PLEDGE_FLOCK,
"getpw" : PLEDGE_GETPW,
"id" : PLEDGE_ID,
"inet" : PLEDGE_INET,
"mcast" : PLEDGE_MCAST,
"pf" : PLEDGE_PF,
"proc" : PLEDGE_PROC,
"prot_exec" : PLEDGE_PROTEXEC,
"ps" : PLEDGE_PS,
"recvfd" : PLEDGE_RECVFD,
"route" : PLEDGE_ROUTE,
"rpath" : PLEDGE_RPATH,
"sendfd" : PLEDGE_SENDFD,
"settime" : PLEDGE_SETTIME,
"stdio" : PLEDGE_STDIO,
"tape" : PLEDGE_TAPE,
"tmppath" : PLEDGE_TMPPATH,
"tty" : PLEDGE_TTY,
"unix" : PLEDGE_UNIX,
"vminfo" : PLEDGE_VMINFO,
"vmm" : PLEDGE_VMM,
"wpath" : PLEDGE_WPATH,
}
def sys_pledge(promises,path):
flags = 0
if len(promises) == 0:
print "ABRT (SIGABRT)"
sys.exit(1)
promises_list = promises.split()
for perm in promises_list:
try:
perms = pledgereq[perm]
except Exception as e:
print(str(e) + ": Undefined promise(s) you made")
sys.exit(1)
        flags = flags | pledgereq[perm]
return flags
if __name__ == '__main__':

pledge_bits = sys_pledge(sys.argv[1],"NULL");
    print "pledge_bits :" + str(hex(pledge_bits))

Output of above python code (for demonstration purpose only):

ubroot@DESKTOP-2AB4AM0:~$ python pledge_python.py "stdio"
pledge_bits :0x8
ubroot@DESKTOP-2AB4AM0:~$
ubroot@DESKTOP-2AB4AM0:~$ python pledge_python.py "stdio inet proc route dns"
pledge_bits :0x101068
ubroot@DESKTOP-2AB4AM0:~$
ubroot@DESKTOP-2AB4AM0:~$ python pledge_python.py "stdio abcd"
'abcd': Undefined promise(s) you made
ubroot@DESKTOP-2AB4AM0:~$
ubroot@DESKTOP-2AB4AM0:~$ python pledge_python.py ""
ABRT (SIGABRT)

So, the above four steps give you some under the hood working of pledge system call.

There are also some other little things left, like about working of the killing mechanism of a process when a process/program tries to use forbidden system calls in pledge(). I will try to cover them later.
If you are able to understand this content, then I think you can easily understand the remaining part of the pledge kernel code, that is,

sys/kern/kern_pledge.c
Note:
 The only interesting part, security-wise, is that pledge does check that you never go increasing the pledge flags once a process gets pledged.
I have tried to cover as most of what I have learned, I case I have forgotten or missed something, please feel free to update me.
Like what you read? Give Neeraj Pal a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.