DSO NUS 2021 CTF - SOAR (Reverse)

I really, really don't want to reverse this ;-;
ctfdso-nus-ctfrev
SOARLooking for a scholarship? Help us find the secret hidden in this one!

This challenge was a fun one since I did not really ‘solve’ it. I bruteforced my way through by using Qiling.

We are handed a PDF File called SOAR-challenge.pdf. Opening it up, we see a advertisement for DSO (organizers, that’s a very smart way to do it!), but nothing else very interesting. I thought it was unusual for a challenge to be in the reversing category but not offer anything to really reverse, so I peeked deeper into the file using binwalk, thinking that were probably embedded files in the pdf:

 binwalk SOAR-challenge.pdf
 
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PDF document, version: "1.4"
70            0x46            Zip archive data, at least v2.0 to extract, compressed size: 4729, uncompressed size: 4760, name: soar.zip
40792         0x9F58          End of Zip archive, footer length: 22

Extracting the files:

binwalk -e SOAR-challenge.pdf

The generated _SOAR-challenge.pdf.extracted folder has this structure:

> _SOAR-challenge.pdf.extracted
    > 46.zip
    > soar.zip

Opening up the soar.zip file, we see that it has one file inside, called soar. However, we cannot extract the file out as the zip is password protected. I threw the zip file into an online zip password recovery (ahem: cracking) service, and got the soar file. The password was dso, very appropriate.

Initial Analysis

Running the soar binary gives no output, so I opened up the file in Ghidra.

 
// WARNING: Removing unreachable block (ram,0x00101c99)
// WARNING: Could not reconcile some variable overlaps
 
undefined8 main(void)
 
{
 
  bVar14 = 0;
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_690 = (char *)malloc(0xd50c51);
  mem_i = 0;
  local_6b0 = 0;
  local_6a8 = 0;
  mem = malloc(0xd50c51);
  mem_i = 2;
  while ((long)mem_i < 0xd50c52) {
    *(undefined *)((long)mem + mem_i) = 1;
    mem_i = mem_i + 1;
  }
  mem_i = 2;
  while ((long)mem_i < 0xd50c52) {
    if (*(char *)((long)mem + mem_i) != '\0') {
      local_6b0 = mem_i * mem_i;
      while ((long)local_6b0 < 0xd50c51) {
        *(undefined *)((long)mem + local_6b0) = 0;
        local_6b0 = local_6b0 + mem_i;
      }
    }
    mem_i = mem_i + 1;
  }
  time(&local_6c0);
  CURRENT_TIME = localtime(&local_6c0);
  uVar9 = CURRENT_TIME->tm_min;
  buffer[0] = 0x36;
  buffer[1] = 0x26;
  buffer[2] = 0x18;
  buffer[3] = 0xb;
  buffer[4] = 0x24;
  buffer[5] = 0x11;
  buffer[6] = 0x12;
  buffer[7] = 0x10;
  buffer[8] = 0x1f;
  buffer[9] = 0x28;
  buffer[10] = 0x20;
  buffer[11] = 0x33;
  buffer[12] = 0x23;
  buffer[13] = 0x1e;
  buffer[14] = 0x16;
  buffer[15] = 0x2f;
  buffer[16] = 0x37;
  buffer[17] = 0xd;
  buffer[18] = 0x25;
  buffer[19] = 0x2c;
  buffer[20] = 0x21;
  buffer[21] = 0x14;
  buffer[22] = 0x15;
  buffer[23] = 0xc;
  buffer[24] = 0x1a;
  buffer[25] = 0x30;
  buffer[26] = 0x1c;
  buffer[27] = 0x1d;
  buffer[28] = 0xe;
  buffer[29] = 0x19;
  buffer[30] = 0x2e;
  buffer[31] = 0x17;
  buffer[32] = 0x31;
  buffer[33] = 0x34;
  buffer[34] = 0x29;
  buffer[35] = 0x39;
  buffer[36] = 0x13;
  buffer[37] = 0x32;
  buffer[38] = 0x2a;
  buffer[39] = 0x2d;
  buffer[40] = 0x35;
  buffer[41] = 0x38;
  buffer[42] = 0x1b;
  buffer[43] = 0x2b;
  buffer[44] = 0x27;
  buffer[45] = 0x22;
  buffer[46] = 0xf;
  local_5ac = 5;
  lVar8 = 0x30;
  plVar10 = &DAT_00102040;
  plVar12 = local_398;
  while (lVar8 != 0) {
    lVar8 = lVar8 + -1;
    *plVar12 = *plVar10;
    plVar10 = plVar10 + (ulong)bVar14 * -2 + 1;
    plVar12 = plVar12 + (ulong)bVar14 * -2 + 1;
  }
  local_690[0x2f] = '\0';
  mem_i = 0;
  while ((long)mem_i < 0x2f) {
    local_398[mem_i] = local_398[mem_i] + -0x1a000;
    local_398[mem_i] = local_398[mem_i] + -0xc51;
    local_398[mem_i] = local_398[mem_i] - (long)(int)((int)local_220 + 5U ^ uVar9);
    local_690[buffer[mem_i] - ((int)local_220 + 5)] = (char)local_398[mem_i];
    mem_i = mem_i + 1;
  }
  mem_i = 0;
  local_678 = fopen(local_690,"r");
  if (local_678 != NULL) {
    fseek(local_678,0,SEEK_END);
    lVar8 = ftell(local_678);
    mem_i = (ulong)(lVar8 == (int)((local_5ac + (int)local_220 ^ uVar9) + 0x18ce6));
    fclose(local_678);
  }
  iVar2 = CURRENT_TIME->tm_hour;
  if (mem_i != 0) {
    local_6a0 = 0;
    while (local_6a0 < 0xd50c51) {
      if (*(char *)((long)mem + local_6a0) != '\0') {
        local_670 = malloc(0x2a);
        lVar8 = local_6a0;
        local_6cc = (int)local_6a0;
        local_6b0 = 0;
        lVar4 = local_6b0;
        do {
          local_6b0 = lVar4;
          bVar3 = (byte)(local_6cc >> 0x37);
          uVar9 = (uint)(local_6cc >> 0x1f) >> 0x1c;
          if ((int)((local_6cc + uVar9 & 0xf) - uVar9) < 10) {
            cVar5 = '0';
          }
          else {
            cVar5 = 'W';
          }
          *(byte *)((long)local_670 + local_6b0) =
               (((char)local_6cc + (bVar3 >> 4) & 0xf) - (bVar3 >> 4)) + cVar5;
          if (local_6cc < 0) {
            local_6cc = local_6cc + 0xf;
          }
          local_6cc = local_6cc >> 4;
          lVar4 = local_6b0 + 1;
        } while (local_6cc != 0);
        iVar6 = (int)local_6b0;
        *(undefined *)((long)local_670 + local_6b0 + 1) = 0;
        local_6a0 = 0;
        local_6b0 = SEXT48(iVar6);
        while (local_6a0 < (long)local_6b0) {
          uVar1 = *(undefined *)((long)local_670 + local_6a0);
          *(undefined *)((long)local_670 + local_6a0) = *(undefined *)((long)local_670 +local_6b0);
          *(undefined *)((long)local_670 + local_6b0) = uVar1;
          local_6a0 = local_6a0 + 1;
          local_6b0 = local_6b0 - 1;
        }
        local_698 = 0;
        while (local_698 < iVar6 + 1) {
          local_690[local_6a8] = *(char *)(local_698 + (long)local_670);
          local_698 = local_698 + 1;
          local_6a8 = local_6a8 + 1;
        }
        local_690[local_6a8] = local_690[local_6a8] + 'C';
        local_690[local_6a8 + 1] = local_690[local_6a8 + 1];
        local_690[local_6a8 + 2] = local_690[local_6a8 + 2] + 'L';
        local_690[local_6a8 + 1] = 'S';
        local_6a8 = local_6a8 + 3;
        local_6a0 = lVar8;
        free(local_670);
      }
      local_6a0 = local_6a0 + 1;
    }
  }
  lVar8 = 0x41;
  plVar10 = &DAT_001021c0;
  plVar12 = local_218;
  while (lVar8 != 0) {
    lVar8 = lVar8 + -1;
    *plVar12 = *plVar10;
    plVar10 = plVar10 + (ulong)bVar14 * -2 + 1;
    plVar12 = plVar12 + (ulong)bVar14 * -2 + 1;
  }
  lVar8 = 0x20;
  puVar11 = &DAT_001023e0;
  puVar13 = local_4a8;
  while (lVar8 != 0) {
    lVar8 = lVar8 + -1;
    *puVar13 = *puVar11;
    puVar11 = puVar11 + (ulong)bVar14 * -2 + 1;
    puVar13 = puVar13 + (ulong)bVar14 * -2 + 1;
  }
  *(undefined4 *)puVar13 = *(undefined4 *)puVar11;
  mem_i = 0;
  while (mem_i < 0x40) {
    if (*(int *)((long)local_4a8 + mem_i * 4) - iVar2 < 1) {
      iVar6 = iVar2 - *(int *)((long)local_4a8 + mem_i * 4);
    }
    else {
      iVar6 = *(int *)((long)local_4a8 + mem_i * 4) - iVar2;
    }
    if (*(int *)((long)local_4a8 + mem_i * 4) - iVar2 < 1) {
      iVar7 = iVar2 - *(int *)((long)local_4a8 + mem_i * 4);
    }
    else {
      iVar7 = *(int *)((long)local_4a8 + mem_i * 4) - iVar2;
    }
    aiStack1448[(ulong)(long)iVar7 % 0x41] = (int)local_690[local_218[(ulong)(long)iVar6 %0x41]];
    mem_i = mem_i + 1;
  }
  mem_i = 0;
  while (mem_i < 0x40) {
    if ((mem_i & 1) == 0) {
      local_690[mem_i] = (char)aiStack1448[mem_i];
      if (mem_i == 0) {
        local_6b0 = mem_i;
      }
    }
    else {
      if ((iVar2 == local_3a8 + (int)local_18) &&
         (local_690[mem_i] = (char)aiStack1448[mem_i], mem_i == 1)) {
        local_6b0 = local_6b0 + 3;
      }
    }
    mem_i = mem_i + 1;
  }
  local_690[mem_i] = '\0';
  printf(*(char **)(fStr + local_6b0 * 8),local_690,local_690);
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
                    // WARNING: Subroutine does not return
  __stack_chk_fail();
}

Well, that’s a lot of code. I was playing in this competition solo, so I was too lazy to properly reverse it at that time. Instead, I wrote a script that would solve it in the background for me, while I worked on other challenges in the foreground. But, to even write a script to bruteforce our way though, we need to find out what is variable (modifiable by us here), as it looks like this program takes no input.

Looking a bit deeper

The program takes in the current time via

  CURRENT_TIME = localtime(&local_6c0);

but only uses the tm_min and tm_hour fields

  uVar9 = CURRENT_TIME->tm_min;
  ...
  iVar2 = CURRENT_TIME->tm_hour;

Furthermore, it opens a file, but only reads in it’s file size, then compares it with another number. This number might be a derivative of tm_min, since the code before this uses tm_min, but we are not sure yet.

  mem_i = 0;
  local_678 = fopen(local_690,"r"); // open file
  if (local_678 != NULL) {          // if exists...
    fseek(local_678,0,SEEK_END);    // seek to the end
    lVar8 = ftell(local_678);       // get the position of the current seek,
                                    // which is the end of the file,
                                    // effectively giving us the length of the file
    mem_i = (ulong)(lVar8 == (int)((local_5ac + (int)local_220 ^ uVar9) + 0x18ce6)); // compare it with this
    fclose(local_678);              // close back file
  }
  iVar2 = CURRENT_TIME->tm_hour;
  if (mem_i != 0) {                 // mem_i is either 0 or non-zero here
                                    // after this comparison, mem_i is overwritten
  ...

In the end, a printf-formatted string is printed out. We can surmise that local_690 is a char array, since the end has a null byte, and thus that the format string has a %s format specifier.

  local_690[mem_i] = '\0';
  printf(*(char **)(fStr + local_6b0 * 8),local_690,local_690);

So our variables are: the tm_min and tm_hour fields of the tm struct returned by localtime(), and the size of a file (that we do not know the name of).

Solution

We have to iterate through all possible tm_min (0-60) and tm_hour (0-24) values.

As for the file, we don’t even need to know it’s name or file size! Note that the variables used, lVar8 (size of file) and local_678 (file pointer) are overwritten afterwards, and they only serve to set mem_i to either 0 or 1 (the result of the comparison with a number). We can set the value of mem_i when it is next used, the if (mem_i != 0), to the value we want to try out. I assumed that we take the branch since the default was to avoid it (mem_i is set as 0 originally).

To put it simply, before tm_min and tm_hour are used, we have to set it to the values we want, and when comparing mem_i with 0 in the if, we have to set it to 1. I used Qiling for this task, but there are alternatives as well, such as Frida and GDB Scripting. I did not even need to read the documentation much, as it was very user friendly. Skimming the home page of Qiling and its examples is sufficient.

from qiling import *
from pwn import *
 
base = 0x555555554000 # base address of executable, loaded by Qiling when PIE is enabled
 
# These offsets can be found from the binary
printf = base + 0x01d48
set_localtime = base + 0x139b
comparison = base + 0x174a
 
for h in range(0, 24):
    for m in range(0, 60):
 
        def get_output(ql: Qiling):
            fmt = ql.mem.string(ql.reg.RDI)
            inp1 = ql.mem.string(ql.reg.RSI)
            out = [(h,m), fmt, inp1]
            print(out)
 
        def set_localtime_fn(q: Qiling):
            tm = q.reg.RAX # output of localtime, the tm struct, is pointed by RAX
            q.mem.write(tm + 4, p32(m)) # &tm + 4 = tm_min
            q.mem.write(tm + 8, p32(h)) # &tm + 8 = tm_hour
 
        def set_comparison(q):
            mem_i = q.reg.RBP-0x6b0
            q.mem.write(mem_i, p32(1))
 
        q = Qiling(['./soar'], rootfs="/", console=False) # make it a bit less noisy when console=False
 
        q.hook_address(get_output, printf)
        q.hook_address(set_localtime_fn, set_localtime)
        q.hook_address(set_comparison, comparison) # hooks
 
        q.run() # wheee

The initial output is as follows:

[(0, 0), '\n', '']

[(0, 1), '\n', '']

[(0, 2), '\n', '']

[(0, 3), '\n', '']
...

After a long wait (while I was solving other challenges in the foreground :)), we see this:

...
[(10, 58), '\n', '']

[(10, 59), '\n', '']

[(11, 0), '%s\n', '4c7b655ed2e3eb42f1d886786c14fe5a757e416f5373de5cd2e4089b870eb5da']
4c7b655ed2e3eb42f1d886786c14fe5a757e416f5373de5cd2e4089b870eb5da
[(11, 1), '%s\n', '4c7b655ed2e3eb42f1d886786c14fe5a757e416f5373de5cd2e4089b870eb5da']
4c7b655ed2e3eb42f1d886786c14fe5a757e416f5373de5cd2e4089b870eb5da
...

Huzzah!

Conclusion

This probably was not the intended solution, but I was short on time. Luckily the script needed very little debugging, and I managed to get valid outputs on my first few tries. Overall, I am quite amazed with Qiling and it’s ability to isolate and emulate binaries, and even provide a high level of introspection.

I did manage to know what the ‘intended’ solution was, which was to download a file from DSO’s site, https://www.dso.org.sg/Media/Default/pdf/SOAR%20Insert.pdf (this PDF is actually referenced in an unused function that I did not notice), which was the file that we checked the size for, then bruteforcing tm_min (tm_hour doesn’t even matter!).

Or download the file, then wait for noon 😂