zms200
zms200

Reputation: 63

Why is my C code throwing a segmentation fault even though the return pointer points to a memory address for seemingly valid shellcode?

I am trying to follow along with a tutorial about buffer overflow (Buffer Overflow Primer by Vivek Ramachandran). I am literally following his code, which works for him in the demo, and which has worked for me until this point.

The goal of the C program below is to assign shellcode for the exit system call to a variable, and then to replace the default return address for the main function, which points to __lib_start_main, with the memory address of the shellcode variable, such that the program executes the shellcode upon completing the main function, and then exits the program gracefully, with a value of 20 (as in execiting "exit(20)"). Unfortunately, the program ends with a segmentation fault instead. I am running this on 32-bit Linux Mint. I'm using gcc to compile the code, and have compiled it with the --ggdb and -mpreferred-stack-boundary=2 options, and I've tried both with and without the -fno-stack-protector option.

Here is the code:

#include<stdio.h>

char shellcode[] = "\xbb\x16\x00\x00\x00"
                   "\xb8\x01\x00\x00\x00"
                   "\xcd\x80";

int main(){

        int *ret;

        ret = (int *)&ret +2;

        (*ret) = (int)shellcode;

}
  1. It starts by defining a variable called shellcode which holds the shellcode.
  2. the main function is called and defines the ret variable, which is loaded into the top of the stack
  3. The memory location of the ret variable, plus 2 integer spaces, which represents the memory location of that is 8 bytes down the stack (the address of the return pointer) is assigned as the value of the ret variable.
  4. The memory address of the shellcode variable is written to the memory address represented by the value of the ret variable - ie.- the return address.
  5. When the function reaches the return instruction, it executes the shellcode, which is the exit function.

I have run this through gdb, and everything seems to check out: The memory location of the shellcode variable is 0x804a01c

At the start of the execution of main, the return value is at the 3rd hex-word and points to __lib_start_main

After executing ret = (ret *)&ret +2 , the value of ret in on the stack and is 8 bytes more than the beginning of the stack

After executing (*ret) = (int)shellcode , the return pointer (3rd hex-word) contains the address of the shellcode, rather than __lib_start_main

The program seems to move to resume execution at the memory address of the shellcode, but nevertheless ends in a segmentation fault.

Thanks in advance!

Upvotes: 2

Views: 2038

Answers (3)

Mahmoud Osman
Mahmoud Osman

Reputation: 1

Your SHellcode Contain Null Bytes, Try Using Smallest Register and use xor when do you need to zero out a register, The Null Byte Problem is when C See This null byte, it stop reading after this null byte '\x00', Causing problem with execution like segmentation Fault.

Upvotes: 0

zwol
zwol

Reputation: 140886

Traditional buffer overflow exploits did involve executing code on the stack, but your program doesn't do that. Your shellcode array is not on the stack, and the construct you used to clobber main's return address to point to the shellcode array does not involve executing code on the stack. When I run your program on my Linux box (also running on an x86 CPU), compiled with gcc -O0 -m32, it does wind up setting the EIP register to point to the machine code in shellcode. But then, as it does for you, it crashes with a segmentation fault.

The reason it crashes is because shellcode is loaded into a region of memory that is marked as not executable. (The name of this memory region is "the data segment".) The processor refuses to execute machine instructions from that area, instead generating an "exception" (this is a hardware concept and not the same as a C++ exception) that the kernel translates to a SIGSEGV signal.

Old tutorials on writing shellcode and buffer overflow exploits don't warn you about this possibility, because older generations of the x86 architecture could not mark memory as not executable on a per-page basis. In the "flat" segment-register configuration used by most 32-bit x86-based operating systems, any page that was readable was also executable. However, the last several generations of the architecture have been able to mark individual pages as not executable, and you have to work around this. (If I remember correctly, per-page executability was added to the x86 architecture circa 2003, at the same time as 64-bit mode, but it took quite a bit longer for operating system support to become universal.)

On my Linux box, as above, this modified version of your program successfully transfers control to and executes the machine code in shellcode. It uses the mprotect system call to make the memory region containing shellcode executable.

#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

const char shellcode[] =
    "\xbb\x16\x00\x00\x00"
    "\xb8\x01\x00\x00\x00"
    "\xcd\x80";

int main(void)
{
  uintptr_t pagesize = sysconf(_SC_PAGESIZE);
  if (mprotect((void *)(((uintptr_t)shellcode) & ~(pagesize - 1)),
               pagesize, PROT_READ|PROT_EXEC)) {
    perror("mprotect");
    return 1;
  }

  void **ret;
  ret = (void **) &ret;
  ret[9] = (void *)shellcode;

  return 0;
}

As well as the mprotect operation itself, note how adding that chunk of code changed the stack layout and put the return address in a different place. If I compile with optimization on, the stack layout changes again and the return address isn't overwritten. Also note how I made shellcode be const char. If I hadn't done that, I would have needed to use PROT_READ|PROT_WRITE|PROT_EXEC in the mprotect call to avoid crashing too early because some random global variable suddenly wasn't writable when the C library expected it to be, and the kernel might then have failed the mprotect call because of the "W^X" security policy.

Depending on the age of your kernel and C library, making shellcode be const char might have been enough by itself, but with kernel 4.19 and glibc 2.28, which is what I have, read-only data isn't executable either.

Upvotes: 5

zms200
zms200

Reputation: 63

Adding the following option when compiling resolved the issue:

-z execstack

Upvotes: 1

Related Questions