How not to execve()
There is a local privilege escalation in Polkit (formerly PolicyKit, and used in plenty of places where privilege escalation is needed). It was found by Qualys, and carefully documented on the oss-sec mailing list. It has hit the mainstream media fairly hard, too – probably because it follows closely on unrelated log4j and faker.js issues in Open-Source-land. I’m not a security specialist by a long shot (not by at least 3 light-seconds, even), but let’s take a brief look at execve()
in FreeBSD.
The description below applies to FreeBSD’s
execve()
as it is today. This morning. Right now when I write it, because there is a review to fix the root cause of the issue which will surely roll out quickly. In the meantime I landed updates to the polkit package last night, although navigating-the-security-notifications parts have not finished yet.
In UNIX talk, when one process wants to start another, there is a “fork-exec dance”. The one process makes a copy of itself, proceeding as two separate processes, and then exec is called, which replaces a process by a different one from another executable. The most common program (process) that starts other processes is the shell. When you do this in a terminal:
$ ls
Aside from all the other things going on, the shell is going to fork itself and then one of the two will replace itself by the executable for ls
. The ls
executable then runs, does its thing, and exits. Afterwards, there is one shell process left.
The current API for exec – for replacing the running process by different one from another executable – is execve()
. Here is a snippet from the manpage:
int
execve(const char *path, char *const argv[], char *const envp[]);
The execve() system call transforms the calling process into a new
process. The new process is constructed from an ordinary file, whose
name is pointed to by path, called the new process file.
The argument argv is a pointer to a null-terminated array of character
pointers to null-terminated character strings. These strings construct
the argument list to be made available to the new process. At least one
argument must be present in the array; by custom, the first element
should be the name of the executed program (for example, the last
component of path).
Here is a complete program that uses execve()
. The intention is that this program just replaces itself with ls
. Running ls
with no arguments usually lists the files in the current directory. With arguments, there’s command-line option processing and then the named files or directories are listed.
#include <unistd.h>
int main(int argc, char **argv)
{
char * const bogus_argv[2] = {NULL, NULL};
char * const bogus_envp[2] = {"PS1=$", NULL};
int r = execve("/bin/ls", bogus_argv, bogus_envp);
return 0;
}
Let’s take a look at some of the lines of this program.
char * const bogus_argv[2] = {NULL, NULL};
Here we have an array of pointers, ready to act as the argv for the program we’re going to
run. Note that both pointers are set to NULL
, so we’re violating the constraint mentioned
in the manpage: here there are zero arguments in the null-terminated array. But there’s
two NULL
s, so we can at least pretend that we’re compliant: if there were a
non-null pointer which would customarily point to the name of the program,
the array would look like {"ls", NULL}
. So we have a suitable null-terminator in place.
char * const bogus_envp[2] = {"PS1=$", NULL};
This is a very minimal environment. PS1
is the name of the variable – it’s the shell prompt.
The value $
is what you traditionally have in the POSIX shell.
int r = execve("/bin/ls", bogus_argv, bogus_envp);
The current process (this program whose source I show above) is replaced by the executable
which is in the file /bin/ls
– that’s the ls
program we know and love.
We pass in the bogus argv, which violates the constraints described for execve()
,
and the very simple environment.
Personally I expected ls
to run and list the current directory. After all,
even without the name-of-the-program as the first argument,
the argv
array is null-terminated. Nope.
$ cc t.c && ./a.out
: PS1=$: No such file or directory
Somehow we’re reaching the environment pointers, which are interpreted as arguments
to ls
. Replace the first NULL
in the assignment to bogus_argv
by anything else,
including ""
, and the program behaves as I expected. Not with a bunch of NULL
s,
though. Making the bogus_argv
longer, e.g. with 16 NULL
s, doesn’t help:
all of them are ignored and the environment leaks into the arguments of ls
.
Takeaway
OpenBSD did it right in 2015 (quoting a Twitter message from Bryan Steele, @canadianbryan):
date: 2015/02/07 08:47:49; author: tedu; state: Exp; lines: +7 -1;
forbid execve() with argc == 0. prompted by a millert email.
ok deraadt miod
FreeBSD is going to do it right (in review), and maybe even Linux is going to do it right (via Ariadne Conill, @ariadneconill on Twitter, this patch).