Thursday, December 2, 2021

Hack The Box Cyber Santa CTF - Reversing Day 1 - Infiltration Writeup

Challenge File:  rev_infiltration.zip

The zip contents contained a single binary file called "client".

The challenge also has a Docker instance that we need to connect to in order to receive the ciphertext to crack:  nc 159.65.20.193 31708

Each time we connect to the server, we receive a different glob of gibberish like this:  

When you run the "client" binary, it spits out the usage syntax:

./client [server] [port]

Putting two and two together, we ran the binary against the target server:

./client 159.65.20.193 31708

[!] Untrusted Client Location - Enabling Opaque Mode

It gives a weird "Untrusted Client" message, then exits.  Well that doesn't help us at all, but what we've hypothesized is the ciphertext should be ingested and deciphered by the binary in some way.  Time to load up GDB and IDA to observe what's happening!  First let's run a "file" on the binary:

file client
client: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bcb9d17215725749cf2ce0ee9ef5df3c98ba8f00, for GNU/Linux 4.4.0, stripped

It's 64-bit and OF COURSE IT'S STRIPPED!  CTF's will do anything to make sure you take longer than you should.  This means we need to manually look at addresses since the function names aren't clear.  I also looked at it with checksec to see if PIE is enabled since we're going deep with this:

checksec client
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

OF COURSE PIE IS ENABLED!!!  This means all addresses observed in debugging needs to take the dynamic running binary base address into consideration and adding the tiny offsets from IDA into GDB when setting breakpoints or disassembling functions.

I opened the binary in both Ghidra and IDA, and the decompilation code looks clearer in IDA.  A lot of the function names are missing in Ghidra.  First I loaded up GDB (I'm using the GEF plugin) and ran it once till completion so it can load the dynamic binary instruction addresses into memory:

gdb ./client

gef➤  run

gef➤  info file

We see the binary addresses in the "5's", more importantly, here's the instruction addresses:
        Before running once:          0x00000000000010d0 - 0x00000000000017d5 is .text
        After running once:             0x00005555555550d0 - 0x00005555555557d5 is .text

Let's figure out what address offsets we want to use to disassemble and set breakpoints at since we don't know function names with the binary being stripped and PIE enabled.  It's time analyze IDA's assembly and decompiled code with <F5>.  Starting at the main() function, I notice after some error checking to make sure we've connected to an IP and Port correctly, it calls this function that I renamed to "goes_to_funcs()" because this function calls other potentially useful functions:

       else                                    // connect() success
        {
          socket_file_descriptor_num = (const char *)ret;
          ret = 0;
          goes_to_funcs((int)socket_file_descriptor_num);
         }

In this "goes_to_funcs" function, I see it calls two other functions, in which I called one of them "contents()" because there's a lot of math stuff happens in it, and another "flag_here()" because that's where the "[!] Untrusted Client Location - Enabling Opaque Mode" message is being printed out with some compare statements in it.  So, I was fairly confident something important is happening here for us to find the flag.

char *goes_to_funcs(int file_descriptor) 

{

  char *result;

  result = (char *)content_here(file_descriptor);

  if ( !(_DWORD)result )

    return flag_here(file_descriptor);

  return result;

}

 Following the train of thought, here are the relevant parts of the "flag_here()" function:

char *__fastcall flag_here(int num_recvd) 

{

  char *result; // rax

  _BYTE buffer_got[1032]; // [rsp+0h] [rbp-418h] BYREF

  unsigned __int64 canary; // [rsp+408h] [rbp-10h]

  canary = __readfsqword(40u);

  recv(num_recvd, buffer_got, 1024uLL, 0);

  puts("[!] Untrusted Client Location - Enabling Opaque Mode");

  result = (char *)(canary - __readfsqword(40u));

  if ( result && (int)buffer_got > 1 )

  return result;

}

The bold piece of code I highlighted seems to be a good spot to put a breakpoint since the next line takes the "result" variable and does some kind of comparison.  Inside that if statement, it does a bunch of math, then returns the "result" variable, so I figure it's important.  When looking for flags in ciphertext, any kind of compare statement is a good sign because the flag could be in memory for comparison.

Now, back to figuring out what address to set the breakpoint at in GDB.  IDA shows that line of code to be at offset 0x132D:

The majority of the beginning of binary hex addresses stay the same.  Only the last 3 characters of the hex binary address changes with the offsets for each line of assembly code.  Applying this knowledge, above, we saw the .text address starts at 0x00005555555550d0.  Changing the last 3 letters to the last 3 letters of the offset 0x132d, it becomes:

Before:  0x00005555555550d0

After:    0x000055555555532d

In GDB, we set a breakpoint at that address, and run with the IP and Port from the server:

gef➤  break *0x000055555555532d 

gef➤  run 159.65.20.193 31708 

The program runs, then breaks at the breakpoint.  WHAT'S THIS!?  Do I spy a piece of the flag format!?


I'm surprised because I didn't expect to see the flag so soon.  I thought we had much more work to do.  That piece of the flag is in the RSP register, so let's print out the strings of RSP to see what other text is there:


HTB{n0t_qu1t3_s0_0p4qu3}

And there we have the flag!  Reverse engineering isn't so bad once you learn to navigate the crooks and crannies of addresses and offsets quicker.




 



No comments:

Post a Comment