After following a series of tips, you have arrived at your destination; a giant vault door. Water drips and steam hisses from the locking mechanism, as you examine the small display - “PLEASE SUPPLY PASSWORD”. Below, a typewriter for you to input. You must study the mechanism hard - you might only have one shot…

Category: Reversing

Solver: s3rpentL0ver

Flag: HTB{vt4bl3s_4r3_c00l_huh}

Writeup

The challenge is downloaded via a zip file. After unpacking it, we get a single executable file named “vault”. The first thing we do when we download a potentially malicious executable is, of course, to execute it. The output looks like this:

┌──(kali㉿kali)-[~/…/htb/ctf/uni21/rev_vault]
└─$ ./vault  
Could not find credentials

The solution takes two steps: 1) static analysis and 2) dynamic analysis/debugging.

Static analysis

When opening the executable in Ghidra, we have some trouble finding main, because vault is a stripped binary. In order to find the main function, we need to search for strings in the string segment:

ghidra2.png

If we follow these strings, we can find the function that references them: (I took some extra screenshots that aren’t included here)

ghidra4.png

Here we can see three print calls, with the top one being the one we have already witnessed. We can see a call to the function open() whose return value is saved to bVar2. If open() fails, the credentials cannot be found. If the credentials happen to be found, we go on to the credential checking:

  bVar1 = true;
  local_234 = 0;
  while( true ) {
    local_241 = 0;
    if (local_234 < 0x19) {
      local_241 = good();
    }
    if ((local_241 & 1) == 0) break;
    get((char *)local_218);
    bVar2 = (***(code ***)(&PTR_PTR_00117880)[(byte)(&DAT_0010e090)[(int)local_234]])();
    if ((int)local_219 != (uint)bVar2) {
      bVar1 = false;
    }
    local_234 = local_234 + 1;
  }
  if (bVar1) {
    operator<<<std--char_traits<char>>
              ((basic_ostream *)&cout,"Credentials Accepted! Vault Unlocking...\n");
  }
  else {
    operator<<<std--char_traits<char>>
              ((basic_ostream *)&cout,
               "Incorrect Credentials - Anti Intruder Sequence Activated...\n");
  }

We can see that the password/flag has to be exactly 25 bytes long (compared to 0x19). Also, the flag seems to be checked byte by byte and one false byte results in bVar1 being set to false, which only gets us the “Incorrect Credentials” response AFTER the loop has finished! We also see that each byte of our flag is checked against this code beauty:

bVar2 = (***(code ***)(&PTR_PTR_00117880)[(byte)(&DAT_0010e090)[(int)local_234]])()

So, we have a function call that somehow returns the correct byte for each index of our flag. If we can observe the result of this function call dynamically, we can restore the correct flag. One more problem: Where do we put the flag? Since Ghidra only returns gibberish for code, we should check the assembly instructions of this function. Yes, we can do that in Ghidra too, but I prefer radare2 for this kind of thing because it’s prettier. Here is the disassembled function in r2:

radare2_1.png

We can see mention of a “flags.txt” string. A reasonable assumption would be that the flag should be in this file. We can also see that some jmps go a bit further than the disass goes, because some of the jmp instructions seem to confuse r2. We can disassemble the whole thing with pd150 though:

radare2_2.png

This is more or less the loop I pasted above. Way below at 0x0000c3b6 you can see that eax, our loop counter is incremented. The jmp 0xc2d9 at the bottom closes our loop. We are looking for a call that doesn’t go to a static function, which is usually done by putting the function address into a cpu register and then calling that register. I already marked the line in the screenshot, but here it is:

0x0000c378      ffd1           call rcx

This call will put the correct byte either into a register or on the stack/heap, that’s how it’s done. After the function call, we can see some sleight of hand with the registers:

       │    0x0000c378      ffd1           call rcx
       │    0x0000c37a      88c1           mov cl, al
       │    0x0000c37c      888dc5fdffff   mov byte [rbp - 0x23b], cl
       │    0x0000c382      e900000000     jmp 0xc387
       │    ; CODE XREF from unk @ 
       │    0x0000c387      8a85c5fdffff   mov al, byte [rbp - 0x23b]
       │    0x0000c38d      8885d3fdffff   mov byte [rbp - 0x22d], al
       │    0x0000c393      0fbe85effdff.  movsx eax, byte [rbp - 0x211]
       │    0x0000c39a      0fb68dd3fdff.  movzx ecx, byte [rbp - 0x22d]
       │    0x0000c3a1      39c8           cmp eax, ecx

There are a lot of ways to find out which register holds the correct byte. For example: load aaaaaaaaaaaaaaaaaaaaaaaaa as a flag, set a breakpoint at the cmp and then check which register holds an a. You could also follow the trace of the istream call in the screenshot, where the byte from flag.txt is read into a stack variable. Anyway, one way or another we find out that the correct flag byte ends up in ecx. Time to go dynamic.

Dynamic analysis

We are using gdb for this one. The issue with gdb is that the debugger has issues with finding the correct entry point of stripped libraries. We can mitigate this by executing

info proc mappings

Which outputs the memory maps for this process. Each of these maps has a start and end point. Radare2 already gave us the address offset of this function (0xc220). If we add this offset to each starting point and set a breakpoint there, we will find out the dynamic address of our function in gdb. This isn’t part of the solution, just a little issue with stripped binaries as a whole.

After this, we can set a breakpoint at the cmp instruction and just iterate through while checking ecx every time. We get this array of bytes:

48 54 42 7b 76 74 34 62 6c 33 73 5f 34 72 33 5f 63 30 30 6c 5f 68 75 68 7d 

Which translates into our flag HTB{vt4bl3s_4r3_c00l_huh}. Solved.