CTF20K 2025: MPC

Recon

The challenge starts with a binary file, that has the following behavior:

$ ./password_checker 
Enter a password: test
Password is invalid.

We will have to guess the password here. There is no source code available for that program, therefore we have to disassemble it using a tool like Ghidra. The decompiled main function looks like this:

undefined8 main(void)

{
  int iVar1;
  long in_FS_OFFSET;
  undefined1 local_78 [104];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Enter a password: ");
  __isoc99_scanf(&DAT_004b603b,local_78);
  iVar1 = is_valid_password(local_78);
  if (iVar1 == 0) {
    puts("Password is invalid.");
  }
  else {
    puts("Password is valid.");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

The program asks for user input, passes it through the is_valid_password function and outputs text accordingly. Let’s analyze this function:

undefined8 is_valid_password(char *param_1)

{
  char cVar1;
  size_t sVar2;
  undefined8 uVar3;
  int local_10;
  
  sVar2 = strlen(param_1);
  if ((int)sVar2 == 0x17) {
    for (local_10 = 0; local_10 < 0x17; local_10 = local_10 + 1) {
      cVar1 = transform_char((int)param_1[local_10],local_10);
      if (cVar1 != valid_password_encrypted[local_10]) {
        return 0;
      }
    }
    cVar1 = transform_char((int)param_1[2],2);
    if (cVar1 == '\x1f') {
      cVar1 = transform_char((int)param_1[5],5);
      if (cVar1 == '\x1f') {
        cVar1 = transform_char((int)param_1[8],8);
        if (cVar1 == '\v') {
          uVar3 = 1;
        }
        else {
          uVar3 = 0;
        }
      }
      else {
        uVar3 = 0;
      }
    }
    else {
      uVar3 = 0;
    }
  }
  else {
    uVar3 = 0;
  }
  return uVar3;
}

The function loops through all characters in the given string (user input), and passes each of them into the transform_char function. It then checks each char against the valid_password_encrypted string at the same index. That means, if we get the encrypted version of the password, and we pass it through an inverse version of transform_char, we could get the right password..

Decompiling transform_char gives us this:

uint transform_char(byte param_1,int param_2)

{
  return param_2 + (param_1 ^ 0x33) ^ 0x55;
}

Exploitation

The function is pretty straightforward, as it consists of simple bitwise XOR operations, and one addition. Keeping in mind that both addition and bitwise-XOR have the same priority in computation, and considering the classical left-to-right calculation order, and knowing that XORing two times against the same number gives back the original value, inversing it gives:

param_1 ^ 0x55 - param_2 ^ 0x33

Now we have to find the encrypted password. Using the nm tool, we can find the memory address of a label in a program:

$ nm -C ./password_checker | grep valid_password_encrypted
00000000004b6010 R valid_password_encrypted

Having that specific address in mind, we can boot up our favorite debugger and explore that area:

(gdb) x/32bx 0x00000000004b6010
0x4b6010 <valid_password_encrypted>:    0x34    0x2a    0x1f    0x31    0x0f       0x1f    0x27    0x30
0x4b6018 <valid_password_encrypted+8>:  0x0b    0x20    0x33    0x19    0x2d       0x34    0x31    0x03
0x4b6020 <valid_password_encrypted+16>: 0x29    0x27    0x3d    0x0d    0x39       0x09    0x31    0x00
0x4b6028:       0x45    0x6e    0x74    0x65    0x72    0x20    0x61    0x20

The password is a null-terminated string, and by looking at this output we know that it is 23 bytes long. Our final ciphertext is this:

34 2a 1f 31 0f 1f 27 30 0b 20 33 19 2d 34 31 03 29 27 3d 0d 39 09 31

Now, let’s apply the inverse XOR-based transformation to every byte, concatenate the output, and print it as a string, using a simple Python script:

encrypted = [
    0x34, 0x2a, 0x1f, 0x31, 0x0f, 0x1f, 0x27, 0x30,
    0x0b, 0x20, 0x33, 0x19, 0x2d, 0x34, 0x31, 0x03,
    0x29, 0x27, 0x3d, 0x0d, 0x39, 0x09, 0x31
]

def inverse_transform_char(t, i):
    return chr(((t ^ 0x55) - i) ^ 0x33)

password = ''.join(inverse_transform_char(t, i) for i, t in enumerate(encrypted))
print(password)

There we go!

$ python exploit.py 
RM{Rev_me_or_get_Revkt}