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 :
__cyg_profile_func_enter
: Allows for automatic profiling by inserting__cyg_profile_func_enter
at the entry of each function in the code.__cyg_profile_func_exit
: Allows for automatic profiling by inserting__cyg_profile_func_exit
at the exit of each function in the code.__builtin_return_address(N)
: Returns the return address of a function from specified callstack level (defined by N).
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 :
But with shadow stack protection, the program exists normally after printing an error message :
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