BreizhCTF 2k25: Pwn Otis_10 writeup

BreizhCTF 2k25: Pwn Otis_10 writeup

I had the pleasure to go to BreizhCTF 2k25 with Formind, and i thought i would write some writeups about some chall i have done. So this will be a mini serie, depending on the time i have.

This time the challenge is Otis_10 and is basically a Use-After-Free simple challenge. I decided to make a writeup for this because its very original as its built around the cowsay linux executable.

So, if we look at the zip we have with this challenge, we have the following:

If we look at the Otis_10.c file we have the code of the executable. The code is built around a few functions, namely moo, roaaar, new_cow and new_creature.

Most of them are fairly straight forward and play with the structure creature_t which looks like this:

typedef struct {
    char msg[32];
    char name[64];
} creature_t;

The structure is also simple, as it contains only two members and both of them are arrays of char.

The new_cow function is about creating a new instance of the said struct, and puting "default" as the name of their structure:

creature_t *new_cow() {
    creature_t *cow = malloc(sizeof(*cow));
    strlcpy(cow->name, "default", sizeof(cow->name));
    return cow;
}

The new_creature function gets a random asciiart from the cowsay share directory, create a new creature_t instance and put that asciiart file as a name:

creature_t *new_creature() {
    creature_t *creature = malloc(sizeof(*creature));

    // you may need to install cowsay for this to work
    FILE *p = popen("ls /usr/share/cowsay/cows/ | shuf -n1", "r");
    fgets(creature->name, sizeof(creature->name), p);
    pclose(p);

    return creature;
}

The roaaar function is interesting to us, because what its doing is basically using the name of the creature_t (which is supposed to be the asciiart filename) as the arg of the command cowsay (using strlcat). This means that if we control the name of a creature_t then we are going to get a command execution!

void roaaar(creature_t *creature) {
    // you may need to install cowsay for this to work
    char cmd[256] = "echo 'Roarrr !' | /usr/games/cowsay -f ";
    strlcat(cmd, creature->name, sizeof(cmd));
    system(cmd);
}

The moo function is also interesting. It allocates a buffer of 96 bytes (which is exactly the size of the structure creature_t ), and puts the message we write to that buffer before executing the cowsay command with that message.

void moo(creature_t *cow) {
    char *msg = malloc(96);

    printf("Message : ");
    fflush(stdout);
    fgets(msg, 96, stdin);


    FILE *p = popen("/usr/games/cowsay", "w");
    fwrite(msg, 1, strlen(msg), p);
    pclose(p);
}

Now, to the CLI menu:

switch (choice) {
            case 'n':
                creature = new_creature();
                printf("Vous vous transformez en %s\n", creature->name);
                break;
            case 'v':
                free(creature);
                break;
            case 'r':
                if (creature != NULL) {
                    roaaar(creature);
                } else {
                    puts("Vous êtes une vache");
                }
                break;
            case 'm':
                moo(cow);
                break;
            case 'q':
                quit = 1;
                break;
            default:
                break;
        }

We can see a few interesting things over there. First, we have a double free because the case 'v' does not check if creature has been freed before freeing it. We can see however than the case 'r' is however checking if creature is null or not before running the moofunction. And we can see that case 'v' doesnt set creature to null, which creates a Use-After-Free condition, if we execute `roaaar` after freeing creature.

Ok now that we now all that, what do we do. Because of the roaaar function, we want to control the name of a creature_t. We can't directly control that given the tools we have here however we can free the creature pointer at will. Now the creature_t struct size (96 bytes) collides with the size of the message buffer in the moofunction, which means that their chunk size are the same. Please, keep that in mind as its very important.

Now lets talk a little bit about how heap management works in linux using GlibC. The heap is originally a big chunk that is fairly big (usually over 0x20000 bytes) that we call the top_chunk. Now every malloc we will do, will take parts of this top_chunk by making smaller chunks. These chunk (basically small parts of long-term memory), once freed will be sorted into lists of free chunks, with different lists for different size of chunk. Once a new buffer is allocated, malloc checks if there is any free chunk in those list, that would be of the exact same size. This is where the size of creature_t and msg come into place. Their size are the same, so they will have the same chunk size.

Ok so lets sum this up. We can allocate a chunk by creating a creature_t ( case 'n'), then create a free chunk by freeing the creature ( case 'v'), and then call moo ( case 'm'), which will do a malloc, which should take the same spot as the chunk we just freed, and in turn will put our message into that spot. And we also have a Use-After-Free if we call roaaar after freeing creature. Now if we do that, the roaaar function will use strlcat to concatenate the command with whatever is at the spot at the time of the execution of the function. Here we go, we have it.

So the plan is:

  • Create a new creature (n)
  • Free it (v)
  • Moo a message (m) that is longer than 32 bytes (because name is after msg in creature_t and msg is 32 bytes long) and that should contain a command injection, so probably something like this: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA;id
  • Roaaar (r) and watch our command execute

Overall it was a fun little challenge for beginners, thanks for reading !