Pulsar preventing vulnerabilities #1 - polkit (CVE-2021-4034)
Pulsar is a security framework. Its primal goal is to prevent actual attacks in your IoT environment. One part of achieving that are generic rules which might help with detecting suspicious activity in your system, but that’s only one part of what we are focusing on. The other important part is studying known vulnerabilities in order to prevent them.
In this series of articles we are going to show Pulsar on the battlefield - how it can be used to prevent actual known vulnerabilities.
Understanding CVE-2021-4034
Introducing PwnKit
CVE-2021-4034, also known as "PwnKit," is a security vulnerability discovered in polkit, a system service installed by default on many Linux distributions. Polkit (formerly PolicyKit) is used to manage system-wide privileges in Unix-like operating systems.
To be precise, the vulnerability specifically affects pkexec, a program that is part of the polkit system. pkexec allows an authorized user to execute commands as another user, including the superuser (root). It’s responsible for communicating with polkit and making a verdict whether the user executing the command should be able to do it, even though that user normally cannot execute the wrapped command.
You can think of pkexec of being somehow similar to sudo, although the difference is that sudo simply allows some groups of users (or some specific users) to authenticate as root just by using their passwords, while polkit has more complex rules which define what and under what circumstance can be done.
A common rule used in most graphical Linux installations is mounting external drives. Normally, mounting new filesystems with the mount syscall, requires root privileges. But when you are using Linux with a desktop environment, you are not being asked for a password after plugging in an USB stick. That’s because of udisks which has this polkit rule! Without it, using Linux on a desktop would be quite annoying, wouldn’t it?
That’s why the security of pkexec itself is extremely important. If there is any way of tricking pkexec into making a wrong decision, the game is over. And unfortunately, PwnKit won the game.
How PwnKit works
In Linux, all processes have an array of arguments, called argv[]
, which is followed by an array of environment variables, called envp[]
. Both arrays are NULL
terminated. The length of argv[]
is being provided in another variable, called argc
. For a simple pkctl my-command
call, the arrays and their memory layout would look like:
There are no requirements with regards to the size of these - they can be empty. In that case, argc
would have value 0.
The problem is that the vulnerable version of pkctl doesn’t check argc
in a safe manner. Instead, it assumes that argv[1]
is either NULL
or a command to execute, no matter what. But then if argv[]
is empty, according to pointer arithmetics in C, reading argv[1]
results in reading envp[0]
. C doesn’t check the bounds of the pointers, the language simply computes the memory location by treating the index (1 in argv[1]
) as an offset from the starting point (argv[0]
). This calculation leads directly to the memory location, without any validation of the pointer's limits.
If an attacker manages to make the argument array empty, pkexec will mistakenly use the content from the adjacent environment array as the application to execute. By adjusting these environment variables to include certain values and payloads, the attacker can exploit this to run commands as a privileged user. This execution occurs without needing any authentication.
If you are curious about details, you can look at one of the exploit examples.
Polkit upstream fix
Polkit fixed this issue by introducing explicit checks for argc.
To prevent such issues in general, Linux kernel 5.18 introduced a change which:
- For userspace processes, when
argv[]
is empty, it adds an empty string and setsargc
to 1. - For kernel threads, it rejects ones with empty
argv[]
.
Preventing polkit exploitation in vulnerable environments
To come up with an answer, let’s think about what’s unique about the pwnkit exploit in comparison to all the other processes in the system.
The answer is simple - it’s being scheduled with an empty array of arguments - argc of value 0 and with empty argv. We can already consider this simple fact as a suspicious property of any process. Why would we even have processes launching without argv? Every command which is being executed correctly, has at least its own name as an argument. Can you think of any legitimate use case of stripping the argv when executing a binary? Me neither!
Could we then just disallow any processes with empty argc and argv? Yes!
When running Linux with kernel older than 5.18, one can mitigate the issue by this simple Pulsar rule:
- name: CVE-2021-4034, possible envp manipulation with empty argv
type: Exec
condition: payload.argc == 0
This rule monitors all Exec
events (a new process launched through execution a binary) and is triggered when argc
(number of arguments) is 0.
Pulsar with the rule above is going to issue the following alert whenever we are trying to either use a PwnKit exploit or, in general, launch processes with empty argv[], trying our luck in finding out whether some software can be vulnerable to manipulation with argv[] and envp[].
Final remarks
As we mentioned at the beginning, with this post, we are starting a whole series about using Pulsar on the battlefield against CVEs and known attack vectors. Stay tuned for more rules and examples in the coming weeks.
If you like it, don't forget to try out Pulsar and give it a star on github!
Michal Rostecki - Software Engineer