FwordCTF2020 - xo challange (RE)
30 / 08 / 2020

Sometimes the simpler the better !

nc xo.fword.wtf 5554

flag format : FwordCTF{}

Author: Semah BA

          

The challange contains single binary called `task`. Running file over it prints:

task: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=2688f7dbf0489de08c22d55eea6040adc7b242bc, stripped

So we have 64 ELF binary for amd64, stripped, statically linked - nothing special here. So, let's run the binary and poke a little.

$ ./task

results in

Error while opening the file. Contact an admin!
: No such file or directory

Ok that's weird, but instead of contacting the admin , Let's load binary into disassembler and see what's happeing.
I wanted to try some tool that would run on native Linux host since It's the machine I was solving the task on.
I decided to go with radare2. It also has a friendly front-end (written in Qt I believe) called cutter.
New version comes with the ghidra decompiler.

cutter's project loading window

default auto analysis (aaa) from r2, write mode (for possibly patching binary) and virtual addressing for easier debugging. Let's jump stright to main.

cutter's project loading window

The key in RE task is to NOT reverse engineer the code ;]. We have to try to extract as much as possible from the binary with the least possible effort. Simply - we are looking for low hanging fruits first and then try to guess as much as possible.

So let's get to It! In the first block of code we can see 4 calls to some functions. No idea what they are, but the last one takes "flag.txt" string and some int64_t from mysterious 0x004ac8e8 address. Hexdump view reveals that under this address is a "r" string.

cutter's project loading window

Seems like a fopen call to me. We can change the name in the disassembler for now and procceed with analysis. We could ofc wrongly assume that this is fopen, but we can fix this later if we see some contradictions.

cutter's project loading window

amd64 ABI leaves return value in $rax register. At the end of the call contents of $rax are written to stack variable and compared to 0. If the returned value is 0 we read address to the "Error while opening file..." error. This confirms our previous assumptions. fopen returns FILE*, if it fails, the NULL is returned and then we print the error. Mentioned store on the stack is saving FILE* for later to use. We can change the name of the variable and the function that takes the string "Error while opening a file..." as print (it might be printf or puts perhaps). For now we dont really know. The fail branch is short so lets analyze it to the end, It might help us later if we guess the name of the called function, because it might be used later and we will have the name of the functions already present in the disassembler. It passes 1 as argument and simply calls. Call after the error printing to a function with a single argument seems like a exit call, but radare says that after a call we simply return to normal execution. Let's run the binary again and check the exit status.

$ ./task

Error while opening the file. Contact an admin!
: No such file or directory
$ echo $?
1

This is the 1 we saw in the disassembler. That's great news. It really seems to be exit call then. Radare must have no information that this call never returns, hence inaccurate control flow. We know from the argument that program tries to open flag.txt. Let's create it and see what happens. We again run the binary and . . .

touch flag.txt
$ ./task
input :

and It seems to be waiting for the user input. Let's try to poke around a little.

input :
asdf
4
input :
ASDF
4
input :
asdasd
6
input :
AAAAAAA
7
input :
A
1
input :



BD
2
input :

Seems like it prints the length of the input string and doesnt do anything when empty input (only newline) is written. Nothing to do more I guess. Let's head back to the disassembler. We have confirmed that the call with the argument 1 is exit, so fill this in the disassembler and continue the analysis. Next we see some argument preparation and call. One of the arguments is our flag_file pointer. Again mysterious address, but quick look at hexview reveals its "%s" string.

FILE*, "%s" and some pointer to the stack. Looks like fscanf / fprintf. I would bet on fscanf since it's flag file and It was opened only for reading (remember "r" string passed to fopen?).

cutter's project loading window

This means that the unknown argument is pointer to some buffer that will now contain the flag. Its var_30 (called by radare), It's also referenced at the beggining of the main function. It was used to store return value from some function that took a single parameter 0x32. This probably was allocating memory and the 0x32 is the size. Let's rename var_30 to flag_buffer and the function to malloc (although we are not 100% sure It's malloc). In main there is a second call to the malloc with 0x32 argument aswell. It's probably another buffer, let's call it buffer_other for now and continue. Next we see, yet again call to malloc with 0x32 argument and store on the stack variable. Let's call it buffer_2 and rename buffer_other to buffer_1. Next we see call to function with address of "input : " as argument. It's probably puts, so let's call it like that for now.

Next call takes allocated buffer1 and "%s" format string. It's called after the puts with the "input : " message, so probably It wait for the user input. Let's call it scanf for now - since the %s format string. Also let's rename buffer1 to user_input, since It seems to take user input from stdin.

cutter's project loading window

Next we see two calls to the same function. First with the flag_buffer as argument and second with the user_input. The result of the call is moved from $rax to $rbx and compared later with the return value from the second call with cmp rbx, rax instruction. If the flag_buffer result is bigger (ja - jump above instuction) we do something with the user_input buffer, otherwise we do the same, but with the flag_buffer. My first guess was, that It's strlen and then we select the shorter string, but than the call is made again to the same function, so for now, let's call it strlen? with the question mark and continue with the analysis.

Next we fill some 2 stack variables - var_34 got filled with return value of strlen? and var_38 set to 0. Next they are compared and if the var_38 is smaller we got to the branch on the right. This seems to confirm the strlen assumption. So lets call the var_34 smaller_len.

cutter's project loading window

The code in the right branch is bit more complex the the earlier one, but It's still very simple. Let's analyze it instruction by instruction. We load var_38, in which (with mov eax, dword [var_38]) we just stored 0. Then we move with sign extend to rdx. Then we load pointer to the flag_buffer and offset it with rdx which now holds var_38 value and load value from this address to the esi. The very same thing we do with the user_input buffer - fetch and offset and store, but this time to ecx. And again with the buffer2, but instead of load we take stored values from ecx and esi, xor them and store the result in the buffer2. It seems that the buffer2 will contain some kind of result of operation on flag_buffer and user_input. Let's rename it to output_buffer then. At the end we increment var_38 and jump back to the conditional which makes a perfect sense for a loop. var_38 has to be index variable - let's call it "i". The loop seems to work until we reach smaller length of the two xor'ing them in the process. Left block shows the path when we reach the end of the xoring. We take the output_buffer and pass it to strlen, take the result and together with the %ld format string pass it to some new function. After the user entered the input there was some number printed in the terminal, remember? That must be it and this must be printf. Ok, the last call to some function with output_buffer and return to some top block, which again seems like a loop. The block begins with malloc. Wait, what? malloc again? Isn't it a leak? It probably is not, bacause the just mentioned unknown call is probably free. Lets rename to free? and sum up all the info we have.

Program reads file flag. txt into buffer, allocates some more buffers, copies user input to one of them and then XOR it with the flag buffer and then prints strlen of the xor result. strlen reads until it reaches '\0' and the xor results in 0, only if we xor two, exactly the same arguments. This means that to leak a flag we have to know the flag?! Well yes, but actually no. We do, but we can guess one character at the time. Simply loop over possible characters at given position and if they match we will observe that the result changed. Remember that the result is printed every time to the user. Ok so if the result change, we just save the guessed character and try another one on the next position, in result leaking all the flag. That sounds like a plan. Let's write the code. I will be using Python2, becuase I hate utf-8 by default on CTF's and don't know pwn tools (yes I know they exists, I promise I will do the homework and learn It until next CTF ;])

Here is the source (github):

#!/usr/bin/python2

import socket
import string

challange_ip = 'xo.fword.wtf'
challange_port = 5554

# All chars for bruteforcing
chars = string.ascii_letters + string.digits + '_()[]<>?/\\-=!@#$%^&*'

# Whatever character that is not a valid flag char
flag_char_ok = '\x01'

flag_max_len = 64

flag = ''
input = []

current_len = 1

xo = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
xo.connect((challange_ip, challange_port))

xo_read = xo.makefile()

while 1:

input += ' '
for c in chars:
input[-1] = c

xo_read.readline() # discard "input: "
xo.sendall(''.join(input) + '\n')
guessed_length = int(xo_read.readline())

print('Trying {} ({}), flag: {}'.format(c, ''.join(input), flag))
print(guessed_length)

if guessed_length == len(flag):
flag += c
input[-1] = flag_char_ok
break

print("Flag: {}".format(flag))

Let the script run for a while and we get:

Flag: NuL1_Byt35?15_IT_the_END?Why_i_c4nT_h4ndl3_That!