This blogpost is a walkthrough of the UrchinSec Tanzania National CTF challenges. The UrchinSec Tanzania National CTF was an interesting ctf organized by the urchinsec team . From the ctf, I managed to solve several challenges one of which was a 500 point reverse engineering challenge.

Catalog

Hatari

This challenge is a Hard rated reverse engineering challenge. Before even going any further, my approach for solving this challenge was not the intended one, but regardless, the end justifies the means. In this challenge , we are provided with two files, the hatari elf binary and an enc.bin file containing the encrypted flag , which is encrypted using the binary.

Checking the contents of the enc.bin file , we find that it contains the encrypted flag

pqy~~sqxyu}pqp{pR{yQi{i|xzq{`~~q{na{sRp{aQypqhaaht

We can run the binary to see how the encryption works . Using the binary to encrypt the string test gives us the string pxqp. Also encrypting the string twice using the binary gives the same result, which is a good thing for us.

From the ctf instructions , we know that the flag format is urchinsec{xxxxxx}. We could try to encrypt a dummy flag and see whether the encrypted text we get, matches with the encrypted flag in the enc.bin file

A quick oneliner to achieve the same is as follows

Nice, the encrypted text we get, matches the string from the encrypted flag. Also note that same characters encrypt to the same value i.e letter x in the dummy flag, gets encrypted to v multiple times. Using this information we can try to determine the rest of the flag. Since we now know that the string urchinsec{ gets encrypted to pqy~~sqxyu, we can try to figure out what the source characters for the encrypted flag are. But how can we do this? Let’s have a recap of what we know so far

pqy~~sqxyu}pqp{pR{yQi{i|xzq{`~~q{na{sRp{aQypqhaaht  # full encrypted flag 

pqy~~sqxyu == urchinsec{
pqy~~sqxyu} == urchinsec{? # where ? represents an unknown character 

To get the character represented with the ?, we could try to bruteforce the ? character in urchinsec{? and then compare result with pqy~~sqxyu}. If we get a character that matches, we can repeat this process until we get the full flag. Below is a python function I created to bruteforce the character

#! /usr/bin/python3
import os
import string 
import subprocess

full_flag="pqy~~sqxyu}pqp{pR{yQi{i|xzq{`~~q{na{sRp{aQypqhaaht"
charset="""ABCDEFGHIJKLMNOPQRSTUVWXYZ0213456789abcdefghijklmnopqrstuvwxyz!"#$%&'()*+,-./:;<=>?@[\]_{}~"""

def runner(command): # This function justs runs a command on the terminal and stores the output in a variable
	try:
		result=subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)
		output=result.stdout.read().decode("latin-1") + result.stderr.read().decode("latin-1")
	except Exception as m33:
		output="Error"
	return output



def fuzz(known_part):
	for char in charset:
		value=f"{known_part}{char}"
		length=len(value)
		command=f"""rm enc.bin;echo '{value}' | /tmp/hatari;cat enc.bin"""
		flag_snippet=test_flag[:length]
		x=(runner(command).split('\n')[-1])
		if ((flag_snippet)) == x:
			print(f"Found matching character {char} i.e Flag snippet '{flag_snippet}' matches command result '{x}'. in {value}")

test_flag= "pqy~~sqxyu}"
known_part="urchinsec{" # we want to know what character will be encrypted to } in test_flag
fuzz(known_part)

After running the script, we notice that we get two characters(j and k) which when encrypted result to the } character. This means that every character in the encrypted flag has two possible characters.

Since our script gives us two possible outcomes, we have to manually repeat this process and try to figure out the most appropriate character. If we try to automate the process, picking the first character every time, we get a flag that does not make sense

We have to manually repeat this process as we try to predict the correct character in the flag, after three tries, we get the first word of the flag is just, We can repeat until we get the full flag After manual iteration, we get the correct flag urchinsec{just_t0_b3C_Clear_This_IS_n0t_S3curERRE}

Attachment

For this challenge, we are provided with a zip with three files: 1. Attachment.exe 2. Attachment.pdb 3. Process.bin

Opening up the Attachment.exe binary in Ghidra , we find a function called Winner which is responsible for decrypting the process.bin file using a hardcoded key pr0gre3ss. It does this by calling another function GrabWin with the filename(process.bin) and the encryption key pr0gre3ss as part of the parameters.

  • The GrabWin function the performs the decryption and then prints the flag. To easily understand how the encryption is done, i re-implemented it in python

Below is the cleaner python code to do the decryption

import os

def GrabWin(return_storage_ptr, process_bin_file, encryption_key):
    # Define variables
    decrypted_data = []
    key_index = 0
    buffer_size = 420
    buffer = [0] * buffer_size
    key_length = len(encryption_key)
    
    # Open binary file
    with open(process_bin_file, 'rb') as f:
        data = f.read()
        
        # Iterate through bytes in the binary file
        for byte in data:
            # Decrypt byte using XOR operation with key
            decrypted_byte = byte ^ ord(encryption_key[key_index])
            # Store decrypted byte
            buffer[0] = decrypted_byte
            decrypted_data.append(chr(buffer[0]))
            # Update key index with wrap-around
            key_index = (key_index + 1) % key_length

    # Extend the storage with decrypted data
    return_storage_ptr.extend(decrypted_data)
    return return_storage_ptr

# Example usage:
process_bin_file = "process.bin"
encryption_key = "pr0gre3ss"
result = []
GrabWin(result, process_bin_file, encryption_key)
print("Result:", ''.join(result))

Running the script, we get the first part of the flag The remaining part of the flag should be the address of the Winner function in hex which is 0x140018a80 as follows Merging the two , we get the final flag urchinsec{pr0grt3ss_w1th_4ttached_Details_0x140018a80}

WormHole

For this challenge , we are provided with an elf binary, and credentials to a machine we can ssh into. Logging in to the machine, we find the wormhole binary file which has the suid bit (rwsr-sr-x) set by the root user. This means that when we execute the binary, it will be executed in the context of the root user.

When we try to run the binary, we get the text Do the worm hole and then it hungs/does nothing else.

From here, we can open the binary in Ghidra to see what the binary is doing in the background. Below is the main function of the binary

We can clean it up to make it more understandable

int main(void)

{
  int flag_file_descriptor;
  int fstat_result;
  char *check_ld_preload;
  time_t current_time;
  undefined8 return_value;
  ssize_t sVar1;
  long in_FS_OFFSET;
  stat stat_buffer;

  check_ld_preload = getenv("LD_PRELOAD");
/* Check if the LD preload variable is set. If it is set, terminate*/
  if (check_ld_preload != (char *)0x0) {
    fwrite("Sorry, you can\'t use ld preload with this program.\n",1,0x33,stderr);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }

  signal(14,wormhole_handler);
  current_time = time((time_t *)0x0);
  srand((uint)current_time);
  puts("Do the worm hole...");
  sleep(10066329);
  puts("Passing through the wormhole... ");
  flag_file_descriptor = open("/flag",0);
  if (flag_file_descriptor == -1) {
    perror("open");
    return_value = 1;
  }
  else {
	/* 
	The fstat function retrieves information about an open file descriptor in
	Unix-like operating systems. It is used to obtain details such as the file
	type, size, permissions, ownership, and timestamps associated with the
	file.It takes a file descriptor and A pointer to a struct stat where the
	information about the file will be stored. 
	*/

    fstat_result = fstat(flag_file_descriptor,&stat_buffer);
    if (fstat_result == -1) {
      perror("fstat");
      return_value = 1;
    }
    else {
      fstat_result = open("/dev/tty",1);
    /* 
    The sendfile function is a system call used in Unix-like operating systems
    for efficiently transferring data between file descriptors. It is commonly
    used for high-performance file copying or sending file contents over a
    network socket without needing to copy data to userspace. Syntax is ssize_t
    sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
    */
      sVar1 = sendfile(fstat_result,flag_file_descriptor,(off_t *)0x0,stat_buffer.st_size);
      if (sVar1 == -1) {
        perror("sendfile");
        close(flag_file_descriptor);
        return_value = 1;
      }
      else {
        close(flag_file_descriptor);
        return_value = 0;
      }
    }
  }
  return return_value;
}
void wormhole_handler(void)

{
  return;
}

From the above code snippet, we can derive the following

  • The program checks whether we have set a LD_PRELOAD environmental variable, if it finds we have set it , it terminates. This is to prevent us from conducting library hooking for the sleep function
  • This next line sets up a signal handler for the signal number 0xe(SIGPWR) which is equal to 14 using the signal function. It will call the function wormhole_handler when this signal is received.
  • The program then sets the time and the prints the Do the worm hole string
  • The program then Sleeps for 10066329 seconds which is equivalent to 116.5084375 days. This makes it impractical to wait for the sleep function to complete
  • After the sleep function, the program then fetches some information about the flag file and reads the flag. It is important to note that we cannot directly run cat /flag since the file is only readable by the root user

When I was solving the challenge, i had the idea of patching the binary and replace the sleep function with something else maybe like puts so that instead of running sleep(0x999999) the program runs puts(0x999999) which would eliminate the waiting part. But soon you will realize why my idea was flawed.

To patch the binary i decided to replace the sleep function with nop instructions(0x90) so that when execution reaches the nop instruction, it will do nothing and proceed to the next instruction hence bypassing the sleep function. To do this , we first need to run objdump disassembler and find the sleep instruction in the main function.

Once, we find it, we need to replace it and save the result in a new binary wormhole_patched as shown below.

echo 'flag{Y0u_b347_7h3_w0rmh0l3}' > /flag
xxd -p /tmp/wormhole | tr -d '\n' | sed s/e832feffff/9090909090/g | xxd -r -p > /tmp/wormhole_patched; chmod +x /tmp/wormhole_patched

From the above, we see that we bypassed the sleep function and read the flag. But trying this on the provided server did not work. This is because when we create a new binary, it will lose the suid properties and hence cannot read the flag since it is owned by the root user.

To solve the challenge, one was required to terminate the running program with a signal 14 or 0xe which would cause the wormhole_handler to be invoked, hence bypassing the sleep function and printing the flag. To achieve that , we can run the binary, then press ctrl+z to background it, find its pid, then kill it using kill -14 pidhere. Once that is done, we can return to the exited program using fg (foreground the back grounded process) and we get the flag

References

Hits