Pages: Welcome | Projects

Got Cap

2015/5/24
Tags: [ GNU/Linux ] [ security ]

I always wondered how Linux Capabilities work, so today I gave it a quick read. Summing up, classic permissions work like booleans: either you're root or you are not. Capabilities simply give you a more fine-granted control on what an user can do and what not.

How is this good? Well, say we've got a daemon which should serve through a TCP port below 1025. As you might know, this usually requires root permissions, thus one might be tempted to run the program as root. Unfortunately the program is bugged, offers an exploitation, and allows $BadGuy to run a shell on your beloved, non-sandboxed system. Do you want $BadGuy to have a root shell? Abort, Retry, Fail?.

Or we might set the CAP_NET_BIND_SERVICE as permitted and effective to the executable which will run your server. Nice pointers about what that means: the capabilities manpage and the cap_from_text manpage.

Quick example here, i.e. how I tried it.

First a refresh on Linux TCP sockets, which reminds me the sequence: socket, bind, listen, accept. Then a bit of lazy code for implementing an echo service:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#include <assert.h>

int main(int argc, char *argv[])
{
    int sd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in baddr, raddr;

    if (sd == -1) {
        perror("Cannot socket");
        exit(EXIT_FAILURE);
    }

    baddr.sin_family = AF_INET;
    baddr.sin_port = htons(1023);    // hence error, for permission
    inet_aton("127.0.0.1", &baddr.sin_addr);

    if (bind(sd, (struct sockaddr *)&baddr, sizeof(baddr)) == -1) {
        close(sd);
        perror("Cannot bind");
        exit(EXIT_FAILURE);
    }

    if (listen(sd, 1) == -1) {
        close(sd);
        perror("Cannot listen");
        exit(EXIT_FAILURE);
    }

    socklen_t alen;
    int rsd = accept(sd, (struct sockaddr *)&raddr, &alen);
    close(sd);
    if (rsd == -1) {
        perror("Cannot listen");
        exit(EXIT_FAILURE);
    }

    size_t n;
    char buffer[5];
    buffer[4] = 0;
    while ((n = read(rsd, buffer, sizeof(buffer) - 1)) > 0) {
        printf("Echoing: %s", buffer);
        write(rsd, buffer, n);  // yeah, ignoring errors. I said lazy
    }
    if (n < 0) {
        perror("Oh shit! Dinosaurs");
    }

    close(rsd);
    exit(EXIT_SUCCESS);
}

One gcc -Wall later, we've got our a.out, which will fail as expected if launced from a regular user:

$ ./a.out
Cannot bind: Permission denied

After we stop crying, we can try with the aforementioned CAP_NET_BIND_SERVICE capability:

$ su -c 'setcap cap_net_bind_service+pe ./a.out'
Password:
$ 

...and then simply run ./a.out again with your regular user. Since the executable has now CAP_NET_BIND_SERVICE in the Permitted and Effective sets, the process which runs it through execve gains the capability, and only that one.

getcap <filename> lists the capability associated to an executable. For instance you can try it with ./a.out

$ getcap a.out                                                           
a.out = cap_net_bind_service+ep

Or with the ping executable, since it needs some additional permission, but everyone wants to use ping without being root...

$ getcap `which ping`                                                        
/usr/bin/ping = cap_net_admin,cap_net_raw+ep

Uh, this post is not intended as a guide, it's just my way of sharing info with friends. It's nowhere a complete documentation on the subject, so RTFM. And happy hacking.