Securinets-CTF-2019, Matrix of Hell

Reversing the Securinets CTF 2019 "Matrix of Hell" crackme to recover its password and flag.

Matrix of Hell, 992 pt

This one is a classic crackme: run the binary, it asks for a password, and getting the right password prints the flag. I opened it in IDA and renamed the variables to make the decompiler output easier to follow.

The main function breaks down into three steps.

Step 1: Building the reference table

First the binary fills a buffer, which I renamed pass_cmp, by walking two nested loops and writing letters into it.


v14 = 0;
  for ( i = 0; i <= 4; ++i )
  {
    for ( j = 0; j <= 4; ++j )
    {
      if ( v14 == 9 )
      {
        v14 = 10;
        --j;
      }
      else
      {
        a2 = (char **)j;
        a3 = (char **)(4 * (j + 6LL * i));
        *(_DWORD *)((char *)pass_cmp + (_QWORD)a3) = v14++ + 65;
      }
    }
  }

Reimplementing the same loop in Python gives us the table it’s comparing against:


s = ['?']*100
v3=0
v4=0
for i in range(5):
   for j in range(5):
       if v14==9:
           v14=10
           j = j-1
       else:
           a3= (j+6*i)
           s[a3]=chr(v14+65)
       v14 += 1
print(''.join(s))

output--> ABCDE?FGHI??LMNOP?QRSTU?VWXYZ????           <-- pass_cmp 

So pass_cmp ends up as ABCDE?FGHI??LMNOP?QRSTU?VWXYZ????, basically the alphabet laid out with a few gaps.

Step 2: The matrix check

Next the binary reads the input, requires it to be exactly 14 characters, and then walks it byte by byte. For each input byte it searches the table as if it were a 5x5 matrix, records the matching (row, col) coordinates as characters, XORs the result, and compares it against a fixed string s1 from the data section.


printf("PASSWORD:", a2, a3);
  gets(password);
  if ( strlen(password) != 14 || (sub_83A(), !v3) )   <-- check password length
  {
    printf("ACCESS DENIED");
    exit(0);
  }
  v16 = 0;
  for ( k = 0; k < strlen(password); ++k )            <-- iterate password byte by byte
  {
    for ( l = 0; l <= 4; ++l )
    {
      for ( m = 0; m <= 4; ++m )
      {
        if ( pass_cmp[m + 6LL * l] == password[k] )   <-- Compare password byte at k index
                                                          with above output. 
        {
          new_string[v16] = l + 65;                   <-- form new string
          v4 = v16 + 1;
          new_string[v4] = m + 49;                    <-- The length of new string is double
                                                          of password.
          v16 = v4 + 1;
        }
      }
    }
  }
  for ( n = 0; n < strlen(new_string); ++n )
    s2[n] = n % 4 ^ new_string[n];                    <-- doing some xor operation and form new 
                                                          string s2
  if ( strcmp(s1, s2) )                               <-- cmp s2 with s1 and s1 is in data 
                                                          section
  {												                            <-- s1 = 'B0C2A2C6A3A7C5@6B5F0A4G2B5A2'					
    printf("ACCESS DENIED", s2);
    exit(0);
  }

So s2 is built from the coordinates via XOR and checked against s1. To go backwards, I first undo the XOR on s1 to recover the string the coordinates encode:

s1 = "B0C2A2C6A3A7C5@6B5F0A4G2B5A2"                   <-- s1
new_string = ""
for i in range(28):                                                     
   new_string += chr(i%4^ord(s1[i]))
print(f)

output--> 'B1A1A3A5A2C4C4B5B4D3A5E1B4C1'              <-- new_string

From that string I can read off the (l, m) coordinates that satisfy the check if ( pass_cmp[m + 6LL * l] == password[k] ):

cord = []
for i in range(0,28,2):
    l,m=0,0
    l = ord(f[i])-ord('A')
    m = ord(f[i+1])-ord('1')
    cord.append((l,m))
print(cord),

output--> [(1, 0), (0, 0), (0, 2), (0, 4), (0, 1), (2, 3), (2, 3), (1, 4), (1, 3), (3, 2), (0, 4), (4, 0), (1, 3), (2, 0)]

With the coordinates in hand, indexing back into pass_cmp recovers the password:

cord = [(1, 0), (0, 0), (0, 2), (0, 4), (0, 1), (2, 3), (2, 3), (1, 4), (1, 3), (3, 2), (0, 4), (4, 0), (1, 3), (2, 0)]
pass_cmp = 'ABCDE?FGHI??LMNOP?QRSTU?VWXYZ????'
password = ""
for i in range(len(cord)):                    
     password += pass_cmp[cord[i][1]+6*cord[i][0]]

print(password)

output --> 'FACEBOO?ISEVIL'                    <-- one byte is missing '?' we know what it is :)
show password is --> 'FACEBOOKISEVIL'

Step 3: The flag

One byte lands on a ? gap in the table, but the surrounding text makes it obvious: the password is FACEBOOKISEVIL. Feed that in and the binary spits out the flag:

flag : 1337_FD_DDLLLKMO_KUWRRRVL_HAHAHA