Plaid CTF 2020 Write-up 1 - reee

2020-04-19 ctf reverse engineering

Story

Tired from all of the craziness in the Inner Sanctum, you decide to venture out to the beach to relax. You doze off in the sand only to be awoken by the loud “reee” of an osprey. A shell falls out of its talons and lands right where your head was a moment ago. No rest for the weary, huh? It looks a little funny, so you pick it up and realize that it’s backwards. I guess you’ll have to reverse it.

The files

An ELF executable is given, and initial decompile shows that it’s packed.

In main there is an unpacking operation on the function flag_check. Each byte is passed to scramble where it performs some bitwise operations.

void main(int param_1)
{
  ulong scrambled_byte;
  long is_valid;
  int j;
  int i;
  
  if (param_1 < 2) {
    puts("need a flag!");
  }
  i = 0;
  while (i < 0x7a69) {
    j = 0;
    while (j < 0x228) {
      scrambled_byte = scramble((byte)flag_check[j]);
      flag_check[j] = SUB81(scrambled_byte,0);
      j = j + 1;
    }
    i = i + 1;
  }
  is_valid = flag_check();
  if (is_valid == 0) {
    puts("Wrong!");
  }
  else {
    puts("Correct!");
  }
  return;
}

Unpacking

We did not reverse engineer the scramble function, instead inserted a breakpoint at 0x004006db and dumped the .text section. The memory map of the process shows that the section has read, write, and execute permissions.

Unpacking the binary with GDB

After repairing the ELF file, we’re able to get a working binary that showed us the checking logic. We noticed some obfuscation techniques being thrown in to mess up the decompiler but Ghidra did surprisingly well.

We noticed that the flag is being passed in via rax and did some adjustments to make the output a bit more accurate. Here’s the gist of it.

ulong flag_check(char *flag)
{
  char c, key;
  int len;
  long k;
  char *flag_ptr;
  int i, j;
  bool ret;
  
  k = -1;
  flag_ptr = flag;
  /* strlen operation */
  do {
    len = (int)k;
    if (k == 0) break;
    k = k + -1;
    len = (int)k;
    c = *flag_ptr;
    flag_ptr = flag_ptr + 1;
  } while (c != '\0');
  /* scrambling the flag */
  key = 0x50;
  i = 0;
  while (i < 0x539) {
    j = 0;
    while (j < (int)(~len - 1U)) {
      flag[j] = flag[j] ^ key;
      key = key ^ flag[j];
      j = j + 1;
    }
    i = i + 1;
  }
  ret = true;
  i = 0;
  while (i < (int)(~len - 1U)) {
    if (ret == false) {
      ret = false;
    }
    else {
      /* scrambled flag is here */
      ret = flag[i] == (&scrambled_flag)[i];
    }
    i = i + 1;
  }
  return (ulong)ret;
}

This functions computes the length of the flag and performs a long series of XORing operations. Finally it checks byte-by-byte against the scrambled flag, the values of scrambled_flag when dumped in decimals are.

[72, 95, 54, 53, 53, 37, 20, 44, 29, 1, 3, 45, 12, 111, 53, 97, 126, 52, 10, 68, 36, 44, 74, 70, 25, 89, 91, 14, 120, 116, 41, 19, 44, 0]

Solving

The scrambling is then implemented with Python and the flags were replaced with symbols. Constraints were added to the scrambled flag, and we run the script with different guesses to the flag length.

import claripy

def enc(flag):
    key = 0x50
    for _ in range(1337):
        for i, c in enumerate(flag):
            a = c
            flag[i] ^= key
            key = a
    return flag

def check(flag):
    chk = [72, 95, 54, 53, 53, 37, 20, 44, 29, 1, 3, 45, 12, 111, 53, 97, 126, 52, 10, 68, 36, 44, 74, 70, 25, 89, 91, 14, 120, 116, 41, 19, 44, 0]
    for i, c in enumerate(flag):
        solver.add(c == chk[i])

# Password length is 33
password_len = 33
solver = claripy.Solver()

flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(password_len)]
flag = claripy.Concat(*flag_chars)

for c in flag_chars:
    solver.add(c >= 0x20)
    solver.add(c < 0x7f)

enc(flag_chars)
check(flag_chars)

try:
    for f in solver.eval(flag, 1):
        username = f.to_bytes(password_len, 'big')
        print(username)
except claripy.errors.UnsatError:
    print(f"Unsat for len {password_len}")

pctf{ok_nothing_too_fancy_there!}