Search code examples
clinuxsecurityassemblyexploit

32 bit shellcode causes a segmentation fault when trying to push "/bin//sh" to the stack


I'm following the opensecuritytraining course "exploits 1". Currently I'm trying to exploit a simple c program with some shellcode on a 32 bit linux system using a buffer overflow. The c program:

void main(int argc, char **argv)
{
    char buf[64];
    strcpy(buf,argv[1]);
}

I compiled the program using the command "tcc -g -o basic_vuln basic_vuln.c". Then, I programmed the following shellcode.

section .text

global _start

_start:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx

mov al, 11

push ebx
push 0x68732f2f
push 0x6e69622f
mov ebx, esp

int 0x80

I compiled it by typing "nasm -f elf shell.asm; ld -o shell shell.o". When I try to execute "shell" on it's own, it works and I get a shell. Next, I disassembled the program with objdump, wrote a perl file which prints the opcodes, and then redirected the output of said perl file along with 39 nop instructions before the shellcode to a file called "shellcode", so the payload is now 64 bytes long, filling the buffer. Then, I opened the c program in gdb, and picked an address in the middle of the nop sled, which will be the new return address (0xbffff540). I appended the address to the "shellcode" file, along with 4 bytes to overwrite the saved frame pointer. The shellcode looks like this:

shellcode

Now, when I try to run this shellcode in gdb in the c program, it causes a segmentation fault at address 0xbffff575, which points at a certain point in my shellcode, 0x62, which is the character "b" in "/bin/sh". What could cause this?

Here's my stack frame, confirming that the return address I choose does return to the middle of the nop sled.

enter image description here

The course does provide shellcode that does work in gdb in the c program: workingshellcode


Solution

  • After main returns into your shellcode, ESP will probably be pointing just above that buffer. And EIP is pointing to the start of it; that's what returning into it means.

    A couple push instructions may modify the machine code at the end of the buffer, leading to a SIGILL with EIP pointing at a byte you just pushed.

    Probably the easiest fix is add esp, -128 to go all the way past your buffer. Or sub esp, -128 to go higher up the stack. (-128 is the largest magnitude 8-bit immediate you can use, avoiding introducing zeros in the machine code with sub esp, 128 or 1024. If you wanted to move the stack farther, you could of course construct a larger value in a register.)

    I didn't test this guess, but you can confirm it in GDB by single-stepping into your shellcode with si from the end of main to step by instructions.

    Use disas after each instruction to see disassembly. Or use layout reg. See the bottom of https://stackoverflow.com/tags/x86/info for more GDB debugging tips.


    The given solution is more complicated because it apparently sets up an actual argv array instead of just passing NULL pointers for char **argv and char **envp. (Which on Linux is treated the same as valid pointers to empty NULL-terminated arrays: http://man7.org/linux/man-pages/man2/execve.2.html#NOTES).

    But the key difference is that it uses jmp/call/pop to get a pointer to a string already in memory. That's only one stack slot not three. (The end of its payload before the return address is data, not instructions, but it would fail in a different way if it did too many pushes and overwrote the string instead of just storing a 0 terminator. The call jumps backwards before the pushed return address actually modifies the buffer, but if it did overwrite anything near the end it would still break.)


    @Margaret looked into this in more detail, and spotted that it's only the 3rd push that breaks anything. That makes sense: the first 2 are presumably overwriting the part of the payload that contained the new return address and the saved EBP value. And it just so happened that the compiler put main's buffer contiguous with that.

    If you actually used tcc not gcc, that's probably not a surprise. GCC would have aligned it by 16 and probably for one reason or another left a gap between the buffer and the top of the stack frame.