Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Guided Example: Compartmentalising a HeartBleed-style Vulnerability

Understanding the Code Base

Consider this program:

// Includes omitted

void privileged_function() {
    printf("Privileged code running!\n");
    // ... do some privilege stuff here e.g. update the server's configuration
    return;
}

int main(int argc, char **argv) {
    char admin_pw[64] = "supersecret"; // admin password
    unsigned char buf[32];
    int opt = 1;

    if(argc == 2 && !strcmp(argv[1], "login")) {
        // Attempt at admin login

        // Get the password attempt
        printf("Please enter the admin password: ");
        char attempt[128];
        fgets(attempt, 128, stdin);

        // remove carriage return
        attempt[strlen(attempt)-1] = '\0';

        // check if the password is correct
        if(!strcmp(attempt, admin_pw))
            privileged_function();
        else {
            printf("Admin authentication failed!\n");
            return -1;
        }
    }

    // Setup the server to listen to port 12345
    int server = socket(AF_INET, SOCK_STREAM, 0);
    setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(12345),
        .sin_addr.s_addr = INADDR_ANY
    };

    bind(server, (struct sockaddr*)&addr, sizeof(addr));
    listen(server, 1);

    // Wait for a client's request
    int client = accept(server, NULL, NULL);

    // read the request into buf
    recv(client, buf, sizeof(buf), 0);

    // Heartbleed-style vulnerability:
    // client sends: [type][len][data] -> respond with `len` bytes
    int len = buf[1];  // vulnerable: no bounds check

    // Send response
    send(client, buf + 2, len, 0);

    // Cleanup
    close(client);
    close(server);

    return 0;
}

This is a variation of the HeartBleed-style vulnerability we covered in the memory safety part of the unit. The program has an administrator mode which can be enabled by having a local user enter the proper password from the standard input. When authenticated, the administrator can run security-critical code (e.g. to update the server's configuration), here represented by privileged_function. As you can see the password is in clear: this is obviously a terrible security practice, we use it here for the sake of the example.

The server listens on the port 12345 for a request from a client, reads it, and sends a response to the client. The client sends the request to the server with the following format:

  • The first byte of the request indicates the request type (here it's not relevant because it's not processed by the server).
  • The second byte of the request indicates the size of server response expected by the client.
  • After that the next bytes indicate what the server should respond: that content should have a size in bytes equal to the value present in the second byte.

Building and Running the Server

Download the server's source file, then in a terminal compile and run the server:

gcc server-monolithic-v1.c -o server
./server

In a separate terminal we can connect to the server as a client. Under normal operation:

printf '\x01\x02hi' | nc localhost 12345
hi

Here we send a request with request type 0x01, expected response size 0x02 (2 bytes), and expected response hi (which size is indeed 2 bytes). The server replies hi, it's all good.

Exploiting Our Server

The vulnerability in the server code is that, when a client sends a request with a value in the second byte that is larger than the size of what is contained in bytes 3+, the server sending the response with send will overflow buf and leak some of its data to the client. So here our threat model is a remote attacker (a client) leveraging that vulnerability to leak the admin password.

Restart the server. Leaking the password is quite easy: the client can indicate a very large expected response size, and a small expected response, for example:

printf '\x01\x90hi' | nc localhost 12345
hiasupersecret��\k����Q�"��:V@% 

As you can see we have overflown buf which is located on the stack. password is nearby, and gets leaked to the remote attacker.

Compartmentalisation Policy

The fix for this bug is easy: the server can put a cap on the size of what the client expects as response, in such a way that buf will never be overflown. However, here our goal is to study Compartmentalisation. To that aim we are not going to fix the bug. We'll rather leave it there and see how compartmentalisation can prevent the password from leaking. This illustrates the fact that compartmentalisation protects program against vulnerabilities that are unknown, or that do not even exist yet.

We want to break down our server and sandbox the request processing code containing the bug as follows:

We want to create two compartments:

  • Compartment 1 contains the program entry point, the authentication code (including the password we don't want to leak), and the privileged code that runs when an administrator successfully logs in.
  • Compartment 2 contains the network code that receives, processes, and sends requests.

With the isolation enforced by compartmentalisation, our goal is that an attacker exploiting the vulnerability in compartment 2 won't be able to leak the password because it lives in the other compartment.

Our compartmentalised server will have 2 binaries, one for each compartment. Its execution flow is numbered on the picture above:

  1. The server is launched by running compartment 1's binary. If needed, compartment 1 authenticates the administrator against the password and run the privilege code.
  2. Compartment 1 invokes compartment 2's binary with fork() + execve(). Compartment 2 sets up networking and start to listen on port 12345.
  3. Compartment 2 receives a request from a client.
  4. Compartment 2 responds to the client.

Compartmentalising Our Server

Split the monolithic server's code into two source files: comp1.c for compartment 1, and comp2.c for compartment 2:

  • Place the authentication and privileged code in comp1.c. After the authentication code has run, compartment 2 should be invoked: for that use fork() and execve() as seen in the lecture. Make sure to remove any code and data (variables) that is no longer needed in compartment 1. Have compartment 1 wait for the termination of compartment 2 before exiting.
  • In compartment 2 create a main function and place the networking code in there. Make sure to delete any code or data that is no longer needed in compartment 2.

Validating Functionality & Protection

The compartmentalised server should behave similarly to the monolithic version under normal operation:

gcc comp1.c -o comp1
gcc comp2.c -o comp2

./comp1

In a separate terminal:

printf '\x01\x02hi' | nc localhost 12345
hi

When an attacker tries to trigger the bug, the password should not be present in the bytes that leak:

./comp1

In a separate terminal:

hi��C�H��L#�H���T��q��>V@`�����T����T�}��@hJ_���T�`E�H؝��>V}�

Submission Instructions

Submit the source code of each compartment as two separate files grouped in a folder in the git repository:

  • heartbleed-guided/comp1.c
  • heartbleed-guided/comp2.c