TBTL CTF 2023

Baby Flag Checker – Reverse Engineering Challenge Writeup | TBTL CTF 2023

Challenge : Baby Flag Checker

Category : Reverse Engineering

This is a classic "Algorithm Reversal" challenge. We just need to simulate what the program does, but in reverse. We open the binary in ghidra, rename some of the variables to understand the code better and dissect the logic behind it.

undefined8 main(void)

{
  size_t sVar1;
  int j;
  int i;
  int k;
  int m;
  
  printf("Enter flag: ");
  __isoc99_scanf(&DAT_00100b2d,&flag);
  sVar1 = strlen(&flag);
  if (sVar1 != 0x2b) {
    no();
  }
  ct._0_4_ = SEXT14(flag);
  ct._4_4_ = SEXT14(DAT_00301141);
  j = 0;
  for (i = 2; i < 43; i = i + 1) {
    if ((&flag)[i] != '\0') {
      for (k = i; k < 43; k = k + i) {
        if ((&flag)[k] != '\0') {
          *(uint *)(ct + (long)k * 4) = *(uint *)(KEY + (long)j * 4) ^ (int)(&flag)[k];
          (&flag)[k] = 0;
        }
      }
      j = j + 1;
    }
  }
  for (m = 0; m < 43; m = m + 1) {
    if (*(int *)(ct + (long)m * 4) != *(int *)(EXP + (long)m * 4)) {
      no();
    }
  }
  yes();
  return 0;
}

Input: The program asks the user for a flag.Length Check: It checks if the length is 0x2b (which is 43 in decimal). If not, it fails immediately. Encryption Loop: It runs the input through a series of loops to scramble (encrypt) it. Comparison: It compares the scrambled result against a hardcoded array of values (let's call it EXP for Expected). 2. Understanding the Logic (The "Sieve"): The encryption logic is distinct because of how the loops behave:

for (i = 2; i < 43; i = i + 1) {
    if ((&flag)[i] != '\0') {  // If this spot hasn't been touched yet...
        for (k = i; k < 43; k = k + i) { // Jump by steps of i
             // ... XOR the character with a KEY
             // ... Set the flag character to 0 (mark as processed)
        }
    }
}

This logic is the Sieve of Eratosthenes, an ancient algorithm used to find prime numbers. How it works here: It starts at index 2 (the first prime). It grabs a KEY and encrypts index 2, 4, 6, 8... (multiples of 2). Then it marks them as "0" so they aren't processed again. The loop moves to 3. Since 3 wasn't a multiple of 2, it's still there. It encrypts 3, 6, 9... (Note: 6 was already done by 2, so the code skips it thanks to the if flag[k] != 0 check).

We check the .rodata section of Ghidra and find the values used by KEY and EXP. We extract the values and use them in our solution script to get the correct flag.

#The target values we want to match
EXP = [
    0x54, 0x42, 0xc4, 0xc0, 0xeb, 0x96, 0xe2, 0xf1, 
    0xe4, 0xe4, 0xa0, 0xda, 0xa7, 0x63, 0xf5, 0xe2, 
    0xa3, 0x63, 0xaf, 0x20, 0xcf, 0xca, 0xa4, 0xbf, 
    0xf3, 0xaa, 0xcf, 0xb9, 0xa3, 0x2a, 0xa1, 0x34, 
    0xa6, 0xd3, 0xe9, 0xe3, 0xe5, 0xfb, 0xf8, 0xbf, 
    0xe2, 0x09, 0xed
]

#The keys used for XORing
KEY = [
    0x90, 0x8c, 0xd3, 0xc5, 0xef, 0x0b, 0x10, 
    0x01, 0xd1, 0x19, 0x5a, 0xa4, 0x3a, 0xda
]

#DECRYPTION LOGIC

#Create a list to hold our recovered characters (length 43)
flag = ['?'] * 43
#A list to keep track of which spots we have already decrypted
visited = [False] * 43

#1. Handle the skipped bytes
#The loop starts at i=2, so indices 0 and 1 are never encrypted.
flag[0] = chr(EXP[0]) # 'T'
flag[1] = chr(EXP[1]) # 'B'
visited[0] = True
visited[1] = True

#2. Replicate the Sieve Loop
key_idx = 0 # Keep track of which key we are using

for i in range(2, 43):
    # Only process this number if it hasn't been "crossed out" (visited) yet
    # This ensures we only trigger on Prime numbers (2, 3, 5, 7...)
    if not visited[i]:
        
        # Process all multiples of i (i, 2i, 3i...)
        for k in range(i, 43, i):
            
            # If this specific multiple hasn't been processed yet...
            if not visited[k]:
                # Decrypt it! 
                # Flag_Char = Encrypted_Val XOR Key
                decrypted_value = EXP[k] ^ KEY[key_idx]
                flag[k] = chr(decrypted_value)
                
                # Mark it as done so we don't touch it again
                visited[k] = True
        
        # Move to the next key for the next prime number
        key_idx += 1
print("".join(flag))

The script produces the final flag:TBTL{REDACTED}

0 people love this