Flare-On 8 Challenge 10 - wizardcult

We are given a PCAP to start off with this challenge.

Looking at the TCP streams, a command injection seems to be going on.

cmd inject

We can then see a binary named induct being requested and downloaded onto the machine.

induct request
induct transfer

We can extract the binary out for analysis.

The binary is a golang binary and luckily for us, the symbols are not stripped.
Looking at main_main, we can see that it creates a new IRC client, attempting to connect to wizardcult.flare-on.com. It also registers 2 functions to run upon 2 events, main_main_func1 and main_main_func2.

register

main_main_func1 is triggered upon successfully connecting to the IRC server while main_main_func2 is triggered upon the PRIVMSG event.
A check is done in main_main_func2 to check if the targeted user is dung3onm4st3r13.

user check

wizardcult_comms_ProcessDMMessage is then called to parse the message sent to the user.

call parser

The function checks if the message contains the string what is your quest? and replies with My quest is to retrieve the Big Pterodactyl Fossil.

string checking

If the message contains you have learnt how to create the Potion of, it will parse the potion name, and perform a lookup of the ingredients via wizardcult_tables_GetBytesFromTable.

potion parsing

The format of the message can be seen from the PCAP traffic.

pcap traffic

GetByteFromTable

The function removes all instances of and in the message and calls wizardcult_tables_Lookup in a loop.
The function is called with each word in the list of keywords (in this case, ingredients) parsed from the message, and the specified table (in this case, wizardcult_tables_Ingredients). The return values are then placed into a slice.

slice building

The function simplies take the word passed in as argument and iterates over each word in the specified table, comparing them via a memequal. If the words are the same, the index of the word in the table is returned.

byte lookup

The slice of bytes built by wizardcult_tables_GetBytesFromTable is then fed into wizardcult_vm_LoadProgram.

vm loading

Within wizardcult_vm_LoadProgram, the bytes are being decoded into a vm_Program object, and some components of the VM is being set up.

vm decode
vm setup
.

We can view how the VM program looks like decoded with the help of degob.

At the start of wizardcult_vm_LoadProgram, encoding_gob___ptr_Decoder__Decode is called with the address of vm_Program, which transform a binary blob into a set of well-defined nested structures. Using degob, the process can be reversed, and we can obtain the opcodes for each potion, revealing that each potion is actually a VM program.

From here, we can go on to trace and follow the VM’s program exection.
However, there is an alternative method to solve this.

In main_main_func1 we can see that the Potion Of Acid Resistance is mapped to the dungeon of Graf's Infernal Disco.

potion mapping

It seems that when this potion is used, the wizardcult_potion_CommandPotion function will be executed.
Similarly the Potion of Water Breathing is mapped to the dungeon of The Sunken Crypt, which will execute wizardcult_potion_ReadFile.

potion mapping

We now need to investigate how the commands to execute/file to read are passed into these functions.
Looking back at wizardcult_comms_ProcessDMMessage, we see that when the message contains “you enter the dungeon”, it splits the descriptions of the dungeon and calls wizardcult_tables_GetBytesFromTable to perform a lookup using the DungeonDescriptions table.

dungeon descriptions

We have previously established that the wizardcult_tables_GetBytesFromTable function returns a slice of indexes, from a slice of words, according to the selected table. We can perform the lookup ourselves by extracting the DungeonDescriptions table.
The table is a slice, where the first value is a pointer to the first element in an array of golang strings, with the next 2 int64 values being the capacity and length of the slice. (Note: the string and slice structs has to be created manaully)

slice

We can go to the string element of the array, and declare an string array of size 0x2e8. We can then export the array out into a file from IDA.

export data

We can now extract the 1st chunk of dungeon descriptions from the PCAP, and perform a lookup of their indexes using the extracted DungeonDescriptions table.

Decoding the resulting hex bytes would reveal ls /mages_tower.
This suggests that the C2 server is:

  • Instructing command parameters by giving descriptions of a dungeon
  • Executing commands based on dungeon name
  • Binary performs a lookup of the respective potion to use, based on the dungeon name to execute the commands
  • The VM is used to obfuscate/encrypt the data before it is sent back to the server as spell casting text

Decoding the 2nd chunk of dungeon descriptions from the PCAP reveals: /mages_tower/cool_wizard_meme.png. It is likely that the 2ng huge blob of spell casts is our ciphertext for the PNG holding the flag.

The C2 server can be mimicked by creating an IRC bot. The bot can be found on my Github
Our goal is to find out the mapping of each of plaintext to its ciphertext, generating a reverse lookup table, and retrieve the original PNG.

After getting the bot out, I generated a 300 byte null-byte file and instructed the binary to read the file.

polyalphabetic

Even though the file is made up of the same bytes, the ciphertext only repeats every 8 lines. It is also noticed that the spells and damage changes. It suggests the following:

  1. The spell represents a byte
  2. The damage represents 2 bytes, delimited by ‘d
  3. A 24-byte polyalphabetic cipher

Due to the polyalphabetic nature of the cipher, we would need to generate a lookup table of each byte at each of the 24 positions.

1
2
3
4
5
6
7
8
9
# edit accordingly to filename for binary to read from
filename = "PLACEHOLDER"

byte_buffer = b''
for count in range(256):
  byte_buffer += (count.to_bytes(1,byteorder='big') * 24)

with open(filename,'wb') as f:
    f.write(byte_buffer)

The ciphertext returned by the binary can be cleaned up to be a lookup table.

Getting the flag now is just a matter of reverse mapping.
The lookups and script to solve can be found on my Github

[email protected]