Incognito 5.0

Incognito 5.0 is a 12-hour long flagship CTF Event organised by Equinox, from 17 Apr 18:00SGT to 18 Apr 06:00SGT. There were 6 web challenges, 4 misc challenges, 3 crypto challenges, 3 rev challenges and 2 OSINT challenges. My team G04T3DP30PL3 solved 1 misc challenge, 1 crypto challenge and all 3 rev challenges, attaining 1100 points. This got us 55th place amongst 275 teams, earning us 6.996 rating points on ctftime.

This writeup includes my workings for the crypto challenge and 3 rev challenges which I solved.

crypto

Di Dah (100 points)

Di Dah Challenge Description From the challenge description, it hints about morse code being used. On opening the encrypted data, we can verify this:

dah-di-di-di-dit dah-dah-dah-dah-dit dah-di-di-di-dit di-di-di-dah-dah dah-dah-di-di-dit di-di-di-di-dah dah-di-di-di-dit dah-di-di-di-dit dah-dah-di-di-dit dah-di-di-dit di-di-di-di-dah dah-di-dit di-di-di-dah-dah dah-dah-dah-dah-dah dah-dah-di-di-dit di-di-dah-dah-dah dah-dah-di-di-dit di-di-di-dah-dah di-di-di-dah-dah di-di-di-dah-dah di-di-di-di-dit di-di-dah-dit di-di-di-dah-dah di-di-di-di-dah dah-di-di-di-dit dit dah-di-di-di-dit di-di-di-di-dah di-di-di-di-dit di-di-dah-dit di-di-di-di-dah di-di-di-di-dah di-di-di-dah-dah di-dah-dah-dah-dah di-di-di-di-dah di-di-di-di-dah di-di-di-di-dah dah-dah-dah-dah-dah dah-di-di-di-dit dah-dah-dah-di-dit di-di-di-di-dit di-di-dah-dit di-di-di-di-dit dah-dah-di-di-dit dah-di-di-di-dit dah-dah-dah-di-dit di-di-di-dah-dah di-dah-dah-dah-dah dah-dah-di-di-dit di-dah di-di-di-dah-dah di-di-di-dah-dah dah-dah-di-di-dit di-di-dah-dah-dah di-di-di-dah-dah di-di-di-dah-dah dah-dah-di-di-dit di-di-dah-dah-dah dah-dah-di-di-dit dah-di-dit dah-dah-dah-dah-dah di-dah

We can guess that each term in the encrypted file corresponds to a character. di and dit correspond to the . character, while dah corresponds to the - character. So we need to convert the dis, dits and dahs to their corresponding characters, then convert the morse code to actual characters.

My Python script below does that:

f = open("enc.txt", "rb")
input = f.read() # read ciphertext as bytes
input = input.decode() # convert input to string
input = input.split(" ") # convert string to an array of terms
n = len(input)
input[-1] = input[-1][:-1] # remove newline character
decode = {".-": "A",
          "-...": "B",
          "-.-.": "C",
          "-..": "D",
          ".": "E",
          "..-.": "F",
          "--.": "G",
          "....": "H",
          "..": "I",
          ".---": "J",
          "-.-": "K",
          ".-..": "L",
          "--": "M",
          "-.": "N",
          "---": "O",
          ".--.": "P",
          "--.-": "Q",
          ".-.": "R",
          "...": "S",
          "-": "T",
          "..-": "U",
          "...-": "V",
          ".--": "W",
          "-..-": "X",
          "-.--": "Y",
          "--..": "Z",
          "-----": "0",
          ".----": "1",
          "..---": "2",
          "...--": "3",
          "....-": "4",
          ".....": "5",
          "-....": "6",
          "--...": "7",
          "---..": "8",
          "----.": "9"}
dotmap = {'dit': '.', 'di': '.', 'dah': '-'}
# iterate through each term
for i in range(n):
    arr_elem = input[i]
    elem_split = arr_elem.split('-') # split the term into di, dit and dah
    for j in range(len(elem_split)):
        elem_split[j] = dotmap[elem_split[j]] # replace di, dit and dah with . and -
    translated = ''.join(elem_split) # join the - and . characters
    input[i] = decode[translated] # change input[i] to the morse decoded character

input = ''.join(input) # convert input back from arr of strings to a string
print(input)

The output of this program is 696374667B4D307273335F346E645F44314440685F5768317A337233727D0A. Upon closer inspection, this is the flag that is hex-encoded. eg 69 is i in hex, and 63 is c in hex. So I add the following code to the script to obtain the flag:

flag = ""
for i in range(0, n, 2):
    sub = input[i:i + 2]
    hexa = int("0x" + sub, 16)
    flag += chr(hexa)
print(flag)

This gives us the flag: ictf{M0rs3_4nd_D1D@h_Wh1z3r3r}

rev

Vault (150 points)

The first rev challenge gives us a binary file. On using the file command we find that it is a 64-bit x86-64 ELF executable. Let’s analyse it using Ghidra:

Vault Main Function

The main function shows that the program asks for an input, runs a function called flag() and stores the string in pcVar2, then uses strcmp to check if our input matches the flag.

Let’s analyse the flag() function:

Vault Flag Function

The function simply iterates through the characters in the flagArray and assigns each character to be (char)*(undefined4 *)(ascii_values.1 + (long) local_c * 4). Since we don’t know what ascii_values.1 is, we need to solve this challenge using dynamic analysis.

So I went to debug the program using pwndbg. On disassembling the main function using disass main we see the following:

Vault Main Disassembled

We want to stop at the strcmp function, so we set a breakpoint at main + 78 by doing b* main+78. I then enter a string of a’s as my input into the program, and let it continue using c.

When our breakpoint is hit, we see the following:

Vault Registers Screenshot

Vault Assembly Code

We notice that now the RDX, RDI registers point to the address of our input string, and RDX and RSI point to the address of the flag string. The contents of these registers are later used by strcmp to check if our input string matches the flag.

We have obtained our flag: ictf{welc0me_t0_rev3rs1ng}!

Vault 2 (200 points)

Vault 2 Challenge

On analysing the binary file using Ghidra, we see the following functions:

Vault 2 Main Function

Vault 2 Check Flag Function

Vault 2 Mystery Function

Essentially, main scans for user input and uses checkFlag to verify its correctness. checkFlag then copies the input string, param_1, into a variable local_a8, and calls mysteryFunction on local_a8. It then compares the first 23 characters of local_a8 and local_28 to make sure they are equal.

Here, notice that local_28 is a variable with a size of only 8 bytes! This means that after the first 8 characters, strcmp will also compare local_a8 against the other variables in the function’s stack which are local_20, uStack_19 and uStack_18.

Looking at mysteryFunction, we notice that it is iterating through the input long param_1, and modifying each byte in the input based on local_c which is its index. It does so through XOR, multiplication and addition operations.

We can think of mysteryFunction as an encryption algorithm to encrypt our input string, and checkFlag as a function which checks that our encrypted input string is the same as the hardcoded variables in the function stack.

Since mysteryFunction encrypts the bytes of param_1 individually, with each character’s encryption depending only on its value and index (and independent of other characters’ encryption), we could modify mysteryFunction and checkFlag to allow us to brute-force each character individually.

To do this, I slightly modify mysteryFunction and checkFlag, and add a int lim parameter to them, which specifies how many characters we wish to check. The pseudocode is shown below:

flag = ""
for i from 0 to 22 # iterate through characters in the flag
    for j in ascii_characters # brute-force all ASCII values
        flag[i] = j
        if (checkFlag(flag, i + 1))
            break # we have found the correct char at index i
print(flag)

My full solve script in C is given below:

#include <stdio.h>
#include <string.h>
void mystFunc(long param_1, int lim)
{
  int local_c;

  for (local_c = 0; local_c < lim; local_c = local_c + 1) {
    *(char *)(param_1 + local_c) =
         *(char *)(param_1 + local_c) ^ (char)local_c + (char)(local_c / 5) * -5 + 1U;
  }
  return;
}
int check(char *param_1, int lim)
{
  int iVar1;
  char local_a8 [128];
  long local_28;
  long local_20;
  long uStack_18;

  local_28 = 0x7136777e62776168;
  // combine uStack_19 into local_20 since Ghidra tells us that
  // local_20 was supposed to be 7 bytes and uStack_19 was supposed to
  // be 1 byte
  local_20 = 0x326e5b306e636435;
  uStack_18 = 0x327f73357c5c7b;
  strncpy(local_a8,param_1,0x80);
  mystFunc((long)local_a8, lim);
  iVar1 = strncmp(local_a8,(char *)&local_28,lim);
  return iVar1 == 0;
}

int main() {
	char flag[23];
	for (int i = 0; i < 23; i++) {
		flag[i] = '\0';
	}
	for (int i = 0; i < 23; i++) {
		int found = 0;
		for (int j = 0; j <= 126; j++) {
			char next = (char)j;
			flag[i] = next;
			if (check(flag, i + 1)) {
				break;
			}
		}
	}
	puts(flag);
}

This gives us our flag: ictf{v4r1abl3_k3y_x0r}

Vault 3 (300 points)

Vault 3 Challenge

On analysing the binary file using Ghidra, we see the following functions:

Vault 3 Main Function

Vault 3 Check Flag Function

Vault 3 Encryption Function

Vault 3 Rotate Char Function

The main function is doing the same job as in Vault 2: Taking user input and entering it into checkFlag. The checkFlag function is also similar: It copies our input string into another string, then calls encrypt(input_cpy, (int)input). It then checks if the first 24 characters of input_cpy and key match.

Looking at encrypt, it iterates through each char in text (the copy of our input string), and modifies it to be the return value of rotateChar. rotateChar takes in a char and an int, and returns a modified uint which is the encrypted character.

We can approach this similarly to Vault 2 since encryption is done on a per-character basis. We iterate through every character, and for each character we iterate through all ASCII values until our encrypted substring matches the flag substring. My solve script (in C) is given below:

#include <stdio.h>
#include <stdbool.h>
#include <string.h>
unsigned int rotateChar(char param_1,int param_2)
{
  unsigned int uVar1;

  if (((char)param_1 < 'a') || ('z' < (char)param_1)) {
    if (((char)param_1 < 'A') || ('Z' < (char)param_1)) {
      uVar1 = (unsigned int)param_1;
    }
    else {
      uVar1 = (param_2 + (char)param_1 + -65) % 26 + 65;
    }
  }
  else {
    uVar1 = (param_2 + (char)param_1 + -97) % 26 + 97;
  }
  return uVar1;
}

void encrypt(char *text,int __edflag)
{
  char bVar1;
  char cVar2;
  int i;
  for (i = 0; text[i] != '\0'; i = i + 1) {
    bVar1 = (char)(i >> 31);
    cVar2 = rotateChar((int)(char)(((char)i + (bVar1 >> 6) & 3) - (bVar1 >> 6) ^ text[i]),3);
    text[i] = cVar2;
  }
  return;
}
bool checkFlag(char *input, int lim)
{
  int iVar1;
  char input_cpy [128];
  long key;
  long local_20;
  long local_18;

  key = 0x7a32567b6879656c;
  local_20 = 0x22785e7133237834;
  local_18 = 0x7f56305b5d6c77;
  strncpy(input_cpy,input,128);
  encrypt(input_cpy,(int)input);
  iVar1 = strncmp(input_cpy,(char *)&key,lim);
  return iVar1 == 0;
}
int main() {
	char flag[24];
	for (int i = 0; i < 24; i++) {
		flag[i] = '\0';
		for (int j = 21; j <= 125; j++) {
			flag[i] = (char)j;
			if (checkFlag(flag, i + 1)) {
				found = true;
			}
		}
	}
	puts(flag);
}

This gives us the flag: ictf{R0t4t!0n_w!th_X0R}

That concludes my solves for the Incognito 5.0 CTF. It was fun applying my static and dynamic analysis skills to the challenges given, and I hope you learnt something from reading this!