Go back to main menu

Shadow Stack

Basic Shadow stack implementation in C language using GCC compiler.
It prevents rewriting stack addresses by creating a new stack called shadow stack.
This can help to mitigate stack-based buffer overflow exploitation techniques such as ROP (Return-Oriented Programming).

How does it work ?

Some GCC built-in functions have been used :

At the beginning of main function, the shadow stack is allocated in data segment. Then, the return address of the next callstack level is pushed to this shadow stack.

When a function will exit, the previous return address is popped and compared with the one from the next callstack level. If the two addresses are not the same then it means that you have data corruption on the stack and potentially a vulnerability. The program exits.

PROS CONS
Because of mprotect call, the shadow stack is not accessible when not in __cyg_profile_func_XXX function. So, no leak (data reading) is possible outside these functions. The shadow stack size is static (the more the callstack grows, the more the shadow stack grows but is limited by its size, which is 1024 addresses by default).
Automatic protection just by linking files and adding -finstrument-functions during GCC compilation. Loss of performance during execution because some code is added at enter and exit of each functions, using syscalls.
It only protects main function and inside it, but it doesn’t protect outside it.

Proof of concept

Here is the vulnerable piece of code I used :

#include <stdio.h>

void vulnerable_function(void)
{
    /* This function is vulnerable to stack-based buffer overflow because of gets() function */
    char buffer[5];
    gets(buffer); // gets function is vulnerable. Never use it.
}

int main(void)
{
    vulnerable_function();
    return 1;
}

Without shadow stack protection, a SIGSEGV occurs and the program crashes :

gdb without prot

But with shadow stack protection, the program exists normally after printing an error message :

gdb with prot

Source code

Here is shadow_stack.c source file :

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include "shadow_stack.h"

void *shadow_stack = NULL;
int shadow_stack_sp = 0;

#define SHADOW_STACK_SIZE 1024
#if defined(__x86_64__) || defined(_M_X64) // 64 bits
    #define BYTES_ARCH 8
#else // 32 bits
    #define BYTES_ARCH 4
#endif

__attribute__((no_instrument_function)) void activate_shadow_stack_protection(void *unaligned_address, int prot)
{
    /* This function does a mprotect call to a memory area. 
        Before reading the shadow stack, PROT_READ and PROT_WRITE protections are ON (activate_protection == 1).
        When finished, these previous protections are OFF (activate_protection == 0). 
        The unaligned address is automatically aligned to a page size.
    */
    int page_size = getpagesize();
    void *aligned_address = (void*)((size_t)unaligned_address & ((size_t)(-1) & ~(page_size - 1)));
    mprotect(aligned_address, SHADOW_STACK_SIZE * BYTES_ARCH, prot);
}

void __cyg_profile_func_enter(void *this_fn, void *call_site) {
    /* Shadow stack push */

    if(shadow_stack_sp >= SHADOW_STACK_SIZE)
    {
        printf("Shadow stack limit reached ! Exiting program to avoid BSS overflow...\n");
        printf("Current stack limit is at %d. To increase this limit, you can modify SHADOW_STACK_SIZE constant.\n",SHADOW_STACK_SIZE);
        exit(1);
    }

    void *ret_address = __builtin_return_address(1);
    if(shadow_stack == NULL)
    { 
        shadow_stack = sbrk(SHADOW_STACK_SIZE * BYTES_ARCH);
        if(shadow_stack == NULL)
        {
            perror("Error allocating memory.");
            exit(EXIT_FAILURE);
        }
    }

    activate_shadow_stack_protection(shadow_stack,PROT_READ | PROT_WRITE);
    ((void **)shadow_stack)[shadow_stack_sp++] = ret_address; // copy return_address in shadow_stack
    activate_shadow_stack_protection(shadow_stack,PROT_NONE);  
}

void __cyg_profile_func_exit(void *this_fn, void *call_site) {
    /* Shadow stack pop + compare */
    void *ret_addr = __builtin_return_address(1);
    activate_shadow_stack_protection(shadow_stack,PROT_READ);
    if (shadow_stack_sp <= 0 || ((void **)shadow_stack)[--shadow_stack_sp] != ret_addr) {
        printf("Ayo ! Potential security breach detected... Leaving the program !\n");
        exit(1);
    }
    activate_shadow_stack_protection(shadow_stack,PROT_NONE);
}

and his shadow-stack.h header file :

#ifndef SHADOW_STACK
#define SHADOW_STACK

/* Activate or desactivate shadow stack protection (read / write / none) */
__attribute__((no_instrument_function)) void activate_shadow_stack_protection(void *unaligned_address, int prot);

/* Shadow stack push */
void __cyg_profile_func_enter(void *this_fn, void *call_site) __attribute__((no_instrument_function));

/* Shadow stack pop + compare */
void __cyg_profile_func_exit(void *this_fn, void *call_site) __attribute__((no_instrument_function));

#endif