6

At the man page I read the following line:

cause an attempt to use setuid(2) to set the caller's user IDs to nonzero values to instead return 0 without actually making the system call

I can't understand what they are trying to say. Can anyone explain this to me?

Thanks.

6
  • 1
    The example you are quoting decribes the sequence like the following. Some user program sets the seccomp filter up and calls, say, sudo su someuser, which escalates (temporarily) the thread privileges to the full capabilities set due to SUID bit on its binary. After this, sudo tries to change the UID to someuser (of course, assuming that the caller is really allowed to sudo su someuser in sudoers). If at this point the seccomp filter catches the setuid(2) call from sudo and immediately returns 0 instead of proceeding to the real call, the thread will stay privileged. Jan 16, 2020 at 7:42
  • 2
    I.e. the unprivileged process sets up the filter which will then discard any attempts to "downgrade" the privileges of the process. After this, the process calls something that temporarily escalates the privileges (sudo) and happily stays with these privileges forever. Thus, to avoid such tricks, the thread which sets the filter has to have no_new_privs, which will forbid permissions escalation while calling things like SUID binaries. Jan 16, 2020 at 7:45
  • Fix to the first post: "su tries to change the UID to someuser", of course, and "the setuid(2) call from su". Jan 16, 2020 at 7:49
  • @DanilaKiver nice explanation. please post this as an answer :) Jan 16, 2020 at 9:14
  • I will be able to post the detailed answer a bit later. Jan 16, 2020 at 10:03

1 Answer 1

7

First of all, notice the following paragraph at the seccomp(2) manpage:

SECCOMP_RET_ERRNO

This value results in the SECCOMP_RET_DATA portion of the filter's return value being passed to user space as the errno value without executing the system call.

So, the seccomp filter may make the kernel skip the real system call execution and instead return some value to pretend that the system call was executed and produced the specified result. This also covers the successful result, if the return value is set to zero.

Here is the sample libseccomp–based program (sec.c) which shows how this works (all error checks are omitted for brevity):

#include <stdio.h>
#include <seccomp.h>

int main() {
    scmp_filter_ctx seccomp;

    seccomp = seccomp_init(SCMP_ACT_ALLOW);

    // Make the `openat(2)` syscall always "succeed".
    seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(0), SCMP_SYS(openat), 0);

    // Install the filter.
    seccomp_load(seccomp);

    FILE *file = fopen("/non-existent-file", "r");

    // Do something with the file and then perform the cleanup.
    // <...>

    return 0;
}

Compiling and running this program while tracing the execution shows that openat(2) system call returns zero (i.e. "executed" successfully) though the file /non-existent-file does not exist in the file system:

$ gcc sec.c -lseccomp -o sec

# Run the program showing the openat(2) invocations and their results.
$ strace -e trace=openat ./sec
...
openat(AT_FDCWD, "/non-existent-file", O_RDONLY) = 0
...

# Ensure that the file does not exist.
$ stat /non-existent-file
stat: cannot stat '/non-existent-file': No such file or directory

Well, now we understand the capabilities of seccomp filters. Let's go closer to the point of the paragraph you've quoted in your question. Think about two things:

  • there are some kernel mechanisms which may be employed by the unprivileged process to escalate its privileges;
  • there are some tools which employ such mechanisms to grant the privileges temporarily and only for the restricted amount of operations.

The typical example is sudo(8) utility, which is the root–owned SUID binary. Executing it in the context of the unprivileged process grants the calling process the full superuser power (except some special cases like sandboxes and containers), after which sudo(8) checks the /etc/sudoers file to learn whether the calling user is allowed to perform the requested operation or not. If he is, sudo(8) proceeds with the execution, and if not, the execution is denied. Also, sudo(8) allows the execution on behalf of arbitrary user — in this case, it will set the appropriate credentials before executing the requested operation.

For example, assuming that the /etc/sudoers file contains the following record

testuser ALL=(anotheruser) NOPASSWD: /bin/bash

the user testuser will be able to run the shell on behalf of anotheruser with the following command:

sudo -u anotheruser -i /bin/bash

Now, we get closer to the point: what if testuser–owned process installs the seccomp filter, which makes the kernel return from setuid(2) without actually executing it, and then runs sudo -u anotheruser -i /bin/bash? Well, let's see:

  • the kernel sees the SUID bit on the sudo(8) binary and escalates the privileges of the calling process appropriately;
  • sudo(8) checks the /etc/sudoers and ensures that testuser is allowed to run /bin/bash as anotheruser;
  • then sudo(8) calls setuid(2) to change the process' credentials appropriately and "downgrade" its privileges;
  • the user-installed seccomp filter silently reports the success, pretending that the "downgrade" is finished;
  • sudo(8) believes that the process now runs on behalf of anotheruser and proceeds with the execution of /bin/bash...
  • actually running it with superuser privileges, because setuid(2) was skipped by the filter and was not really executed!

So, allowing the unprivileged user to install seccomp filters also allows this user to "hijack" the privileged credentials by catching the calls to setuid(2) while temporarily running with escalated privileges, hence there are the restrictions specified at the manpage:

either the calling thread must have the CAP_SYS_ADMIN capability in its user namespace, or the thread must already have the no_new_privs bit set.

It is clear what does the first part of this sentence mean: CAP_SYS_ADMIN is a capability which grants the process lots of privileges, so it is safe to assume that the process possessing it is already powerful enough to wreak the havoc in the system. What about the second part?

The no_new_privs bit is a property of the process which, if set, tells the kernel to not employ privileges escalation mechanisms like SUID bit (so, invoking things like sudo(8) will not work at all), so it is safe to allow the unprivileged process with this bit set to use seccomp filters: this process will not have any possibility to escalate privileges even temporarily, thus, will not be able to "hijack" these privileges.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .