#BSDBOY

Books | Mailing List (marc.info) | Twitter | Github | StackExchange | LinkedIn | About Me

pledge(2): OpenBSD's defensive approach to OS Security

Hi there,

This is the same blog post that I have posted on my medium blog

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:

What is pledge(2)?

The meaning of pledge is same as in the real world, that is, "a solemn promise or undertaking".

So, in OpenBSD:

Calling pledge(2) 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:

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(2) is not a system call filter. So, it is not used to restrict system calls.

For example,

pledge(2) works on stdio, dns, inet, etc. promises but not directly on system calls like read, write, etc.

And, unique functionality of pledge(2) 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 
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(2), 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(2) 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'.


# cat sampe.c
#include <unistd.h>
#include <stdio.h>
int
main() {
    if(pledge("stdio",NULL) == -1) {
        err(1,"pledge");
    }
    printf("Pledged\n");
    return 0;
}
# ./testing
Pledged
#
# vim sampe.c
# gcc -o testing_reduced sampe.c
# cat sampe.c
#include <unistd.h>
#include <stdio.h>
int
main() {
    if(pledge("",NULL) == -1) {
        err(1,"pledge");
    }
    printf("Pledged\n");
    return 0;
}
# ./testing_reduced
Abort trap (core dumped)
#

Little Introduction about the working of pledge(2) 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(2) system call.

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

Steps:

Below is the pledgereq[] array:


static const struct {
	char *name;
	uint64_t flags;
} 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 },
	{ "error",		PLEDGE_ERROR },
	{ "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 },
	{ "unveil",		PLEDGE_UNVEIL },
	{ "vminfo",		PLEDGE_VMINFO },
	{ "vmm",		PLEDGE_VMM },
	{ "wpath",		PLEDGE_WPATH },
	{ "wroute",		PLEDGE_WROUTE },
};

#include <sys/cdefs.h;>

/*
 * pledge(2) requests
 */
#define PLEDGE_ALWAYS	0xffffffffffffffffULL
#define PLEDGE_RPATH	0x0000000000000001ULL	/* allow open for read */
#define PLEDGE_WPATH	0x0000000000000002ULL	/* allow open for write */
#define PLEDGE_CPATH	0x0000000000000004ULL	/* allow creat, mkdir, unlink etc */
#define PLEDGE_STDIO	0x0000000000000008ULL	/* operate on own pid */
#define PLEDGE_TMPPATH	0x0000000000000010ULL	/* for mk*temp() */
#define PLEDGE_DNS	0x0000000000000020ULL	/* DNS services */
#define PLEDGE_INET	0x0000000000000040ULL	/* AF_INET/AF_INET6 sockets */
#define PLEDGE_FLOCK	0x0000000000000080ULL	/* file locking */
#define PLEDGE_UNIX	0x0000000000000100ULL	/* AF_UNIX sockets */
#define PLEDGE_ID	0x0000000000000200ULL	/* allow setuid, setgid, etc */
#define PLEDGE_TAPE	0x0000000000000400ULL	/* Tape ioctl */
#define PLEDGE_GETPW	0x0000000000000800ULL	/* YP enables if ypbind.lock */
#define PLEDGE_PROC	0x0000000000001000ULL	/* fork, waitpid, etc */
#define PLEDGE_SETTIME	0x0000000000002000ULL	/* able to set/adj time/freq */
#define PLEDGE_FATTR	0x0000000000004000ULL	/* allow explicit file st_* mods */
#define PLEDGE_PROTEXEC	0x0000000000008000ULL	/* allow use of PROT_EXEC */
#define PLEDGE_TTY	0x0000000000010000ULL	/* tty setting */
#define PLEDGE_SENDFD	0x0000000000020000ULL	/* AF_UNIX CMSG fd sending */
#define PLEDGE_RECVFD	0x0000000000040000ULL	/* AF_UNIX CMSG fd receiving */
#define PLEDGE_EXEC	0x0000000000080000ULL	/* execve, child is free of pledge */
#define PLEDGE_ROUTE	0x0000000000100000ULL	/* routing lookups */
#define PLEDGE_MCAST	0x0000000000200000ULL	/* multicast joins */
#define PLEDGE_VMINFO	0x0000000000400000ULL	/* vminfo listings */
#define PLEDGE_PS	0x0000000000800000ULL	/* ps listings */
#define PLEDGE_DISKLABEL 0x0000000002000000ULL	/* disklabels */
#define PLEDGE_PF	0x0000000004000000ULL	/* pf ioctls */
#define PLEDGE_AUDIO	0x0000000008000000ULL	/* audio ioctls */
#define PLEDGE_DPATH	0x0000000010000000ULL	/* mknod & mkfifo */
#define PLEDGE_DRM	0x0000000020000000ULL	/* drm ioctls */
#define PLEDGE_VMM	0x0000000040000000ULL	/* vmm ioctls */
#define PLEDGE_CHOWN	0x0000000080000000ULL	/* chown(2) family */
#define PLEDGE_CHOWNUID	0x0000000100000000ULL	/* allow owner/group changes */
#define PLEDGE_BPF	0x0000000200000000ULL	/* bpf ioctl */
#define PLEDGE_ERROR	0x0000000400000000ULL	/* ENOSYS instead of kill */
#define PLEDGE_WROUTE	0x0000000800000000ULL	/* interface address ioctls */
#define PLEDGE_UNVEIL	0x0000001000000000ULL	/* allow unveil() */

/*
 * Bits outside PLEDGE_USERSET are used by the kernel itself
 * to track program behaviours which have been observed.
 */
#define PLEDGE_USERSET	0x0fffffffffffffffULL
#define PLEDGE_STATLIE	0x4000000000000000ULL
#define PLEDGE_YPACTIVE	0x8000000000000000ULL	/* YP use detected and allowed */

#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(2) system call.

There are also some other little things left, like about the working of the aborting/killing mechanism of a process when a process/program tries to use forbidden system calls in pledge(2). 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:

Share on Facebook | Share on Twitter | Share on LinkedIn