Skip to content

Introduction To GNU Heap Exploitation

Dynamic Memory - An Overview

Why Use It?

Reasons and Advantage of allocating memory dynamically:

  • When we do not know how much amount of memory would be needed for the program beforehand.
  • We can allocate dynamic local variable but its scope limited to the function call. Memory allocated with Dynamic Allocation is global memory and is not limited to the function call.
  • For dynamically growing data structure.

Using Dynamic Memory

In C, stdlib.h provides with standard library functions to access, modify and manage dynamic memory. Commonly used functions include malloc, free and realloc.

  1. Malloc

malloc(size_t n) returns a pointer to a newly allocated chunk of at least nbytes, or null if no space is available.

If n is zero, malloc returns a minimum-sized chunk. (The minimum size is 16 bytes on most 32bit systems, and 24 or 32 bytes on 64bit systems.).

There's a safer version of malloc which is calloc(size_t nmemb, size_t size), the only major difference between malloc and calloc is that calloc sets the memory zero before returning the allocated chunk.

  1. Free

free(void* p) Releases the chunk of memory pointed to by p, that had been previously allocated using malloc or a related routine such as realloc. It has no effect if p is null. It can have devastating effects if p has already been freed [Double Free].

  1. Realloc

realloc(void *ptr, size_t size) The realloc() function changes the size of the memory block pointed to by ptr to size bytes. Contents will be unchanged in the range from the start of the region up to the minimum of the old and new sizes.

  • If the new size is larger than the old size,the added memory will not be initialized.
  • If ptr is NULL, then the call is equivalent to malloc(size).
  • For all values of size; if size is equal to zero, and ptr is not NULL, then the call is equivalent to free(ptr).

Note - Internally, these functions(malloc,realloc and calloc) use two system calls sbrk and mmap to request and release heap memory from the operating system.

Behaviour of Free Chunks

When we free a chunk of size less than 0x80, glibc uses a singly linked list to organize each of the freed chunks, and each bin adopts the LIFO policy, and the recently released chunk will be allocated earlier.

Why The LIFO Policy

The LIFO policy is applied due to the fact that Access latency can be avoided by reusing the data fetched previously. This reduction in access latency is what is termed as Locality Of Reference which is a term for the phenomenon in which the same values, or related storage locations, are frequently, depending on the memory access pattern.

In case of small size chunks , the concept of Locality Of Reference is better re-enforced due to the fact that generally, programmers may repeated allocated and free fairly small amount of memory to work with, and when small size memory chunks are freed, they can cause External fragmentation in case they are not handled properly.

Classes Of Heap Memory Bugs

Used And Abused: The Infamous Use After Free

Basically, UAF happens when the program tries to access a portion of memory after it has been freed that may force the program to crash and based on the flow of the program you might get arbitrary code execution.

Consider the following situations:

  • Memory block released -> pointer=NULL (safe) -> used again -> program crashes (why?).

  • Memory block released -> pointer!=NULL (unsafe) -> no code to modify the free block -> program is likely to work properly.

And now comes the important scenario.

  • Memory block is released -> pointer!=NULL, but before it is used next time, there is code to modify the memory, then when the program tries to use the freed memory again, it can lead to serious Security breaches(how?).

Let us try to understand with a simple program how UAF can happen due to simple programming abberations and how it can be leveraged to attacker's needs.

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

typedef struct name {
  char *myname;
  void (*func)(char *str);
} NAME;

void myprint(char *str) 
{
    printf("%s\n", str); 
}
void Malicious(char *str) 
{ 
    system(str); 
}

int main() {
  NAME *a;
  a = (NAME *)malloc(sizeof(struct name));
  a->func = myprint;
  a->myname = "I can also use it";
  a->func("this is my function");

  // Free without Nulling out a
  free(a);
  a->func("I can also use it");

  // free with modify
  a->func = Malicious;
  a->func("/bin/sh");
}

As you can see, in the above program, with the help of Use After Free, we could do 2 major things.

  1. We overwrote the function argument and thus we could leverage memory leaks.
  2. We overwrote a function pointer and made it point to system('/bin/sh') landing us to a bash shell which is devasting if we can get shell on server.

So, with UAF, you can leverage a lot of control over the functionality of a program -

  • Pointer dereference leading to arbitrary write-what-where primitive.
  • Leaking memory to defeat memory address randomisation (ASLR).
  • Overwrite pointer dereferences to functions as we saw above.

And... a lot more, only limited by creativity and imagination.

A Little More You Can Do

To get your hands dirty with exploiting UAF, you can play around with this code,

//test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    char *name = 0;
    char *pass = 0;
    while(1)
    {
        if(name) printf("name adress: %x\nname: %s\n",name,name);
        if(pass) printf("pass address: %x\npass: %s\n",pass,pass);
        printf("1: Username\n");
        printf("2: Password\n");
        printf("3: Reset\n");
        printf("4: Login\n");
        printf("5: Exit\n");
        printf("Selection? ");
        int num = 0;
        scanf("%d", &num);
        switch(num)
        {
            case 1:
                name = malloc(255*sizeof(char));
                printf("Insert Username: ");
                scanf("%254s", name);
                if(strcmp(name,"root") == 0)
                {
                    printf("root not allowed.\n");
                    strcpy(name,"");
                }
                break;
            case 2:
                pass = malloc(255*sizeof(char));
                printf("Insert Password: ");
                scanf("%254s", pass);
                break;
            case 3:
                free(name);
                free(pass);
                break;
            case 4:
                if(strcmp(name,"root") == 0)
                {
                    printf("You just used after free!\n");
                    exit(0);
                }
                break;
            case 5:
                exit(0);
        }
    }
}

Compile it with gcc -no-pie -o test test.c.

Apart from this, you can also try out the challenge HackNote from Pwnable.tw.