Hackthebox University CTF Writeup

HackTheBox University CTF Writeups

10 December 2020

By Jacob Pimental

A few weeks ago I participated in the HackTheBox University CTF. All of the challenges were well put together, especially the Reverse Engineering challenges. I decided to put together a writeup for the 3 challenges I managed to complete.

My Name Is

For this challenge we are presented with a 32-bit ELF binary. If we load this up in Cutter and navigate to the function main we can see that the application will grab the current user’s ID using geteuid as well as the username using getpwuid. It will then call ptrace to detect whether or not it is being run in a debugger and exit the application if it is.

HTB University CTF My Name Is Challenge Main function Main function of application showing geteuid and getpwuid being called

The application will then compare the username against the value ~#L-:4;f and decrypt the flag if they match. Instead of manually reversing the decryption function, to save time I used Cutter’s debgugger to step through the application and change the result of getpwuid to ~#L-:4;f. This way I don’t need to create another user with the matching name either.

CTF Challenge username check Comparison between our username and ~#L-:4;f

Bypassing anti-debug ptrace Don’t forget to change the return value from ptrace to zero to avoid detection

Hexdump of stored username Cutter makes it easy to overwrite the stored username from the Hexdump view

After running past the decryption function the flag will be in the Stack view in Cutter’s debug mode. The flag comes out to be HTB{L00k1ng_f0r_4_w31rd_n4m3}

My Name Is flag


The challenge provides us with another ELF binary. Upon opening it in Cutter, you can see that this program does not have a main function. Looking at the entrypoint it appears that syscalls will be used throughout the application rather than normal function calls. Looking at the first function that the entrypoint calls at 0x0040023c, it makes two syscalls. The first one passes the value 0x29 to eax which corresponds to the sys_socket function, which will create a “socket” object in C. The next syscall uses the value 0x2a which corresponds with the sys_connect function, which will initialize our socket object with an address to connect to as well as a port. To identify different syscalls like this, you can use a reference table like the one here. We can also see that the values 0x100007f and 0x401f are pushed onto the stack. If we right-click 0x100007f in Cutter and click Set Immediate Base To -> IP Address we will see that the application is connecting to While 0x401f is little-endian for 8000. Now we know that the application will connect to localhost over port 8000.

Socket connection syscall First function called from entrypoint which creates a socket connection

If we decide to listen to port 8000 on localhost using Netcat we are able to interact with the program. When we run the application while having Netcat running the application will send the data:

NICK ircware_2370
USER ircware 0 * :ircware
JOIN #secret

This looks a lot like it is connecting to an IRC server, which explains the challenge name. If we send the application a PING :, it will respond with a PONG : just like an irc client is supposed to do. If we look at the last function being called in the entrypoint we can see a few other commands that look interesting:

PRIVMSG #secret :@pass
PRIVMSG #secret :@exec
PRIVMSG #secret :@flag

It looks like the @flag command will also not run unless a variable is set to true. That variable is only set when we provide the correct input to the @pass command. Looking closer at the @pass branch, we can see that it will perform some operations on our input and compare the result to RJJ3DSCP. The checks that the application will perform on each character of our input are as follows:

Input validation graph Graph view of checks the application makes against our input

So all we need to do is find a value that will work with the checks made and will be equal to the current index of RJJ3DSCP. After some quick work, this is the result:

Which makes our input ASS3MBLY. If we send this to the client we receive the message PRIVMSG #secret :Accepted. Now if we try the @flag command we will receive the flag, HTB{m1N1m411st1C_fL4g_pR0v1d3r_b0T}.

ircware flag


This challenge gives us a pcap of malicious traffic that was captured during a ransomware attack. We are told that the author wasn’t able to retrieve any artifacts during this incident, so the pcap is all we have to go on.

Malception challenge info

One of the first things I noticed when crawling through the pcap was an HTTP GET request to http://utube.online:8000/xQWdrq.exe. This was most likely the ransomware that the challenge author was talking about. Using wireshark you are able to save the raw bytes of the HTTP response as a file so we can analyze the sample further. I will be using Cutter for the analysis.

Binary downloaded in HTTP Request HTTP GET request and response showing binary being downloaded

Looking at the main function in Cutter shows that the program will be communicating with the utube.online domain again over port 31337. If the connection was successful, the program will send the string z11gj1. It then allocates 1024 bytes of space in memory to store the return value. This is easy to see using the rzghidra plugin that comes standard with Cutter.

Code for connection being established rzghidra output that shows the connection being established

Since the utube.online site does not actually exist there is no way to emulate this network connection to retrieve the data ourselves. Thankfully we have that pcap that contains all the network activity that occurred during the attack. Looking through we can see that the C2 will return 8 bytes of data when sent that string. That data is then xor’d with the return value of GetComputerNameExA which, when the function’s NameType parameter is passed the value of 2, will return the host name of the computer, which is another thing we don’t know right away. While looking through the pcap we can see a Kerberos as-req packet that contains the domain name of the computer, MEGACORP.LOCAL. We can drop the “.LOCAL” part and xor the “MEGACORP” half by the 8 bytes from the C2, which gives us dc 46 73 b0 5e 39 16 b3 (in hex).

Xor loop r2ghidra output showing the xor loop and GetComputerNameExA function call

C2 returned data The 8 bytes returned from the C2 after sending the string z11gj1

Kerberos packet used to find hostname Kerberos packet used to find the hostname of the machine

The program will then send another string to the C2, 533_11s4, which will respond with 7680 bytes of what looks to be encrypted data. Space is allocated to store this data using the VirtualAlloc function. It then takes the xor’d data that we calculated in the previous block, as well as the returned 7680 bytes and passes them into what looks like a decryption function. After analyzing multiple samples that have used this decryption algorithm in the past I recognize this as an RC4 decryption algorithm that uses the xor’d hostname as the key. We can verify this by trying to decrypt the returned blob in CyberChef, where we can see an MZ header which proves our hypothesis.

Mimicking C2 connection Data returned from C2 after sending second string

RC4 function call Returned data and key being passed into rc4 decryption function

RC4 function in rzghidra iSnippet of RC4 function, the constant 0x100, two loops, and xor operand helped me identify this as rc4

Verifying RC4 decrytpion Testing RC4 decryption in CyberChef to verify hypothesis

At first I did try dumping the output to a file, seeing it was a .NET binary, and opening it in DNSpy, but unfortunately DNSpy threw multiple errors when trying to load it. This was due to a large section of the binary being missing, which means that more is being done in the original application. Upon further examination, the application looks to be getting data from the resource number 101 and RC4 decrypting that using the same key as before. The decrypted data is then inserted it into the .NET binary from earlier at offset 0xdb0. Using a quick python script we can get the fully functioning second stage!

from arc4 import ARC4

key = b'\xdc\x46\x73\xb0\x5e\x39\x16\xb3'
arc4 = ARC4(key)
# Encrypted stream output from Wireshark
enc_dat = open('encrypted.bin', 'rb').read().strip()
dec_dat = list(arc4.decrypt(enc_dat))

arc4_2 = ARC4(key)
# Resource extracted from binary
enc_dat2 = open('10_101_1033', 'rb').read().strip()
dec_dat2 = arc4_2.decrypt(enc_dat2)
index = 0xdb0
counter = 0
while counter < len(dec_dat2):
    dec_dat[index + counter] = dec_dat2[counter]
    counter += 1
open('decrypted.bin', 'wb').write(bytes(dec_dat))

Extracting second stage Retrieving the resource, RC4 decrypting it and inserting it at offset 0xdb0

The .NET binary contains two classes that it uses for encryption. The first class, called Stego will create a random RSA key and use that to encrypt data. The second class, called Graphy uses the AES algorithm to encrypt data. The function EncryptFile from the class CorpClass takes the arguments of a filename, and a byte array, key. This function will generate a GUID and hash that using MD5, the output is used as the AES key to encrypt the file. It will then encrypt the MD5 hash of the GUID using the Stego class (RSA), as well as xor the private RSA key by the byte array passed as a parameter. The results of these operations are then concatenated into a byte array and the length is sent to the utube.online domain over port 31338, followed by the data itself.

EncryptFile function First part of the EncryptFile function showing the file encryption along with keys being sent to the C2

The .NET binary then sends the filename of the file it is encypting (after xor’ing it), followed by the encrypted contents of the file. Since we have a PCAP of the network traffic during the time of the attack, and we know that the ransomware will send the key used to encrypt the file to the C2 along with the file contents we can start work on recovering the encrytped files. First we need to recover the MD5 hash that was used to encrypt the file in the first place. To do this, we need to decrypt the RSA private key that was sent along with the hash. We know the RSA key was xor’d by the second parameter of the EncryptFile function. Looking back at the first stage, we can see that the RC4 key used earlier was also passed to another function that will use the CLR function CreateInstance to create a new instance of CorpClass. From this we can determine that the RC4 key is also used as the xor key to encrypt the RSA private key. After dumping the streams contents from Wireshark I created another python script to parse and decode the information sent to the C2.

def xor_decrypt(dat, key):
    d = ''
    for i in range(len(dat)):
        c = chr(dat[i]^key[i%len(key)])
        d += c
    return d

# Reusing RC4 key to xor decrypt the RSA key
key = b'\xdc\x46\x73\xb0\x5e\x39\x16\xb3'
# TCP Stream dumped from Wireshark
stream = open('tcp_stream.bin', 'rb').read()
cursor = stream.index(b'\n')
priv_key_length = int(stream[:cursor])
enc_key_data = stream[cursor+1:cursor+1+priv_key_length]
# Decoding key data
key_data = xor_decrypt(enc_key_data, key)
rsa_priv_key = key_data[key_data.index('<RSA'):]
enc_md5 = enc_key_data[:key_data.index('<RSA')]
b64_enc_md5 = b64.b64encode(enc_md5)
cursor = cursor+1+priv_key_length

stream = stream[cursor:]
cursor = stream.index(b'\n')
file_name_length = int(stream[:cursor])
enc_file_name = stream[cursor+1:cursor+1+file_name_length]
# Decoding file name
file_name = xor_decrypt(enc_file_name, key)
cursor = cursor+1+file_name_length

stream = stream[cursor:]
cursor = stream.index(b'\n')
file_length = int(stream[:cursor])
enc_file = stream[cursor+1:cursor+1+file_length]
cursor = cursor+1+file_length
# Write encrypted file contents to a file to be decrypted later
open(file_name, 'wb').write(enc_file)

print(f'Length of priv key: {priv_key_length}')
print(f'RSA Priv Key: {rsa_priv_key}')
print(f'Base64 Encoded MD5 Hash: {b64_enc_md5}')
print(f'Length of file name: {file_name_length}')
print(f'File name is: {file_name}')
print(f'Length of file: {file_length}')

Packet capture of encrypted data sent Data sent to the C2 in Wireshark

Encryptfile function Rest of the EncryptFile function showing additional data sent to the C2

Verifying python script works Output from Python script showing data sent to the C2

The RSA key is stored as an XML string, which can be easily imported into other .NET applications. The python script will also output a base64 encoded version of the MD5 hash that was used to encrypt the file. This makes it easier to port over to other .NET applications for decryption. Looking at exactly how the Graphy class will utilize the MD5 hash for encrypting files, we can see that it is first passed into the function Rfc2898DeriveBytes which will generate pseuo-random bytes based on a password and salt. The salt variable being passed into the function is hard-coded, which makes it easy to replicate. The Encrypt function will then grab 32 bytes from the Rfc2898 object, which will be used as the AES key to encrypt data, as well as an additional 16 bytes to use for the IV. We can replicate this using the site .Net Fiddle to print out the AES key and IV.

using System;
using System.Security.Cryptography;
using System.Text;

public class Program
        public static void Main()
                String b64_data = "R6WOPgcwXUmI9oqeHUsDGxMZwKKx0IPS3O5Y3Hj8LoElUaW9PCwidD3/DfnSg7P9ANm5vW+TXL0OLYxIezeDALy+rxCQRRMETh4bwV8GG7ttUay/eJ5iOoD/pEk9r0QgrPxrMVM73+/KGXPx1ICcMlkxlqR+Mm4x00oiPFwjDAk=";
                byte[] data = System.Convert.FromBase64String(b64_data);
                String xml="<RSAKeyValue><Modulus>5Ja1+N9HfEsTH7qUcefmDTV6rRYRaoY3eZQyFP1mazR0elBDiQDFZ6mWI9FRrSrQsg2wzTj+DKr6bjt7ixiG0vXQVwaNZfEJXTMMjC7SQVKLVGfpbm0+2fVTdYN0OrJIDzlnlQPw8LBsxUB8Bs3I+7b/h9fNnHaJxlbtIQocliU=</Modulus><Exponent>AQAB</Exponent><P>5KcMfUVAcjXycv8OfX+SjOAQAb/H0OA6rNgGo3RLHo1JRyz2rSatbjjdND3Tsk8lU+DXd+IyStPeNGwrG55t1w==</P><Q>/+21PgxbiCQQJG5uKVkjbszpcKOlL0Am0cd9eHcU52wMydiu2fIJf2XCm8bVr8FpcCJLfJxs3s20rz5AC01EYw==</Q><DP>A7nlLjVus23FobIeXlUx4jHUkPK7IuBElISAtzEx+DF9PDezXWb/9IfgsvU++ezoQtGrMTzybN2/BUOuACk4yQ==</DP><DQ>8kW8xTg9jetVvKctcccIW+NvOUoxHUHFfeEzTc6s40bN9GZDX95YT1mtmHnp369geN5+R0Btb52b5ikvx4MlsQ==</DQ><InverseQ>gNdWGXhW2WdpLmLEqJeUjMVq2l+mJxRwlsiSzaw9q03YDrUnpkKl1eROqFyf3it9WvOKnvqC0TSpdhcBsAgRLw==</InverseQ><D>4cDGLue0Xdh3JprKCESSOvFaGp70zFOJbhUh8QDhqXbAohuq1x9f1iTyFqWfGHp0aaSDu+pRXIlvknZEaPbsDoYKZkNK5Qcv3zhaOqh7l1VPfF2G3tm5ghcZV4x86BnMFakAL9kIYuM1XFxaRnUgStke0zR6ykat/EIU3DVoD3E=</D></RSAKeyValue>";
                RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
                byte[] r = rsa.Decrypt(data, false);
                string password;
                password = System.Convert.ToBase64String(r);
                byte[] salt = new byte[] {21, 204, 127, 153, 3, 237, 10, 26, 19, 103, 23, 31, 55, 49, 32, 57};
                Rfc2898DeriveBytes rfc = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt, 2);
                byte[] key = rfc.GetBytes(32);
                byte[] iv = rfc.GetBytes(16);

Getting Key and IV Key and IV used for AES Encryption

Now that we have the AES Key and IV, we can toss the encrypted file we extracted from the TCP Stream into CyberChef and decrypt it. This will decrypt to a PDF file that contains the flag HTB{m1nd_b3nd1ng_m@lwAr3!??}

AES Decryption using Cyberchef AES Decryption using CyberChef, we can tell the decryption worked as there is a valid PDF header

Malception flag The decrypted PDF file


I definitely enjoyed this CTF. The Malception challenge was especially interesting and challenging. I look forward to reading the other writeups for this CTF as I did not have enough time to complete the final RE challenge on the list. If you have any questions on how I solved the challenges or would like to share how you solved them, feel free to reach out to me on my Twitter or LinkedIn.

Thanks for reading and happy reversing!

Cutter, rzghidra, CTF, HackTheBox, Rizin

More Content Like This: