Assignment: ROP

Changelog:

Your Task

  1. Download the target executable program dumbledore-rop.exe, which is the same program that we used for the OVER assignment, compiled and linked differently. Your job is to exploit a buffer overflow using an ROP chain, most likely with assistance from the tool ROPgadget (linked below) to construct it.

    Your goal is to construct a program input such that the program’s output ends with:

    Thank you, YOUR NAME.
    I recommend that you get a grade of A on this assignment.
    

    (where YOUR NAME is your actual name or something similar).

    If your attack program is in a file called attack.py3 or attack.py, your exploit must work when run as follows, which is similar to OVER, but without the dependency on libc (dumbledore-rop.exe is statically linked, so it does not depend on libc):

    • from the directory containing dumbledore-rop.exe, running the shell command:

       python3 attack.py3 > attack.txt
       setarch -RL env - ./dumbledore-rop.exe < attack.txt
      

    or a similar program named attack.c or attack.cc or attack.py2 (which we will run differently, as in the OVER assignment).

Tools

  1. For this assignment, we recommend using the tool ROPgadget. There are also other tools, some of which are more sophisticated. (I was consdering recommending angrop, though it seems a lot less stable than ROPgadget.) Whatever tool you use, please describe that in the attack file you submit.

    Thie tools are both written in Python. To install either of them, I’d recommend setting up a Python virtual environment and then using pip within that virtual environment to install them.

setting up a Python virtual environment

  1. On portal or NoMachine, the version of python that’s available by default is missing support for virtual environments. So you should first use module load to load a more recent version of Python as in:

    module load gcccore/13.3.0 python/3.12.3
    

    (You’ll need to rerun this command for each terminal you operate or modify your .bashrc or similar.)

  2. Once you have an appropriate version of Python, choose a directory to create the virtual environment; we’ll use rop-venv for this example. Create that directory and use python -m venv ... to setup a “virtual environment” in that directory:

    mkdir rop-venv
    python -m venv rop-venv
    

    If you look inside that directory, you’ll see that python command created bin, lib, and several other subdirectories and a pyvenv.cfg configuration file.

  3. Then, “activate” that virtual environment in the current terminal:

    source bin/activate

    You’ll see that your prompt is now prefixed with something like (rop-venv)

    (rop-venv)
    cr4bd@portal05>/u/cr4bd
    $
    

    You’ll need to rerun this command for each terminal that you want to use the virtual environment.

    Once a virtual environment is activated, running python will automatically use libraries installed in that virtual environment instead of just the ones from the system. Also, commands like pip for installing new python packages will default to installing them within that virtual environment directory.

    You can deactivate a virtual environment with deactivate.

Installing ROPgadget

  1. Once you’ve setup a Python virtual environment (or portal/NoMachine or your own machine), make sure you’ve activated that virtual environment in your current terminal. Then, you can use a command like

    pip install ROPgadget==7.6
    

    to install ROPgadget (selecting version 7.6, in this case).

    After doing this, typing ROPgadget --help should show you ROPgadget’s (relatively long) usage message.

    You’ll need to re-activate the vritual environment in order for the ROPgadget command to work. (The executable will be in the bin subdirectory of your virtual environment directory.)

Addt’l Resources

  1. The slides on return-oriented programming.

  2. The “definitive” article on return-oriented programming, especially section 4.

Hints

Using ROPgadget

  1. Given a binary foo.exe, running

    ROPgadget --binary foo.exe
    

    will output a list of gadgets discovered in the executable. You might want to redirect the output to a file as in with:

    ROPgadget --binary foo.exe > gadgets.txt
    

    (and then open gadgets.txt in a text editor).

    ROPgadget.py also has many additional options which you can find by running

    ROPgadget --help
    

The executable is offset

  1. The dumbledore-rop.exe we supply is position-independent, so it can be loaded at a different locations. (If we did not disable address space layout randomization (ASLR), this would allow the executable to fully take advantage of ASLR.)

    By default, ROPgadget.py assuems that executables are loaded at the addresses encoded in them (the one shown by objdump), which isn’t the same as the address that’s chosen by the OS, even with ASLR disabled.

    You can change this default by giving ROPgadget an offset with the --offset option:

    ROPgadget --binary foo.exe --offset 0x123456 ...
    

    (where 0x123456 is the difference between the addresses shown in objdump and the actual address to which the executable is loaded).

  2. If the you run the debugger with the same settings as the executable (such as by running gdb with setarch x86_64 -RL gdb), you should get addresses in the debugger which are consistent with the addresses from testing.

The ROPchain option

  1. ROPgadget has an --ropchain option that attempts to construct a generic ROP chain that executes a shell (that is, a program that accepts commands to run, like what you get when you open a terminal on Linux). So running

    ROPgadget --binary foo.exe --ropchain
    

    will output the list of gadgets, followed by a Python script given the gadgets found in foo.exe.

    (In a Unix-like shell, you can use something like

    some_command > output.txt
    

    to run the command some_command, redirecting its output to output.txt)

  2. To construct the generic ROP chain, ROPgadget has a set of particular gadgets it looks for that can accomplish tasks like setting %rdi to a certain value or performing a system call. Assuming it finds an appropriate gadget for each step of the “execute a shell” process, it will be able to output a full ROP chain.

    If it’s not able to do that (often due to restrictions from forbidden bytes, see next setion), then it will fail, but that does not mean an ROP exploit isn’t possible. In such cases, you can often still construct a chain manually (or a more sophisticated tool might be able to do so automatically).

  3. Although you could construct a custom ROP chain that outputs the text you want, it would probably be easier to use this generic ROP chain, and then provide a shell command that will output the text you desire.

    In the “executing a command with a shell” section below, we describe how to construct suitable input for the shell that this exploit would start.

  4. The Python script that ROPgadget constructs is incomplete; it places the chain in a variable p, but does not:

    • output that chain
    • add padding such that the beginning of the chain will overwrite a return address
    • add shell commands after the chain so some useful command will run afterwards
    • add padding before the shell command to deal with stdio buffering (see hint below)

Forbidden bytes

  1. It’s possible that some gadgets that ROPgadget.py finds will have an address which when written out as bytes contain 0x10, which is the newline character. Since this causes the program to stop reading input, you would not be able to input these addresses. You can have ROPgadget.py ignore these gadgets by using the --badbytes option

Positioning the ROP chain

  1. You need to make sure the ROP chain is positioned properly over the return address. If it is not, you are likely to get a segfault, or the normal output.

    Positioning the beginning of the ROP chain should be exactly like positioning the overwritten return address in OVER (though the program is slightly different). You can either: * examine the objdump output to determine what’s on the stack in the vulnerable function, and therefore the distance between the return address and the beginning of the buffer; or * try inputting different amounts of data and see what length is required to get a segfault. This should indicate where the return address starts relative to the buffer.

Executing a command with a shell

Padding before shell comamnd to handle buffers

  1. To execute a command in the shell that ROPgadget’s exploit code would start, you will need to output a shell command after the exploit string. When you do this, you will need additional padding before this shell command to deal with gets’s buffering.

    stdio.h functions like gets default to reading a lot of extra data from a file to avoid making many calls to the OS to get data. This data is buffered for future gets, getchar, etc. calls (making them faster). But when the exploit code generated by ROPgadget.py starts the shell, these buffers are discarded.

  2. To work around stdio buffering, you can have some unused bytes between the attack string and the shell commands, such as several thousand newlines. (Note that the python expression "\n" * 10000 will produce a string containing 10000 newlines.) This will act similarly to a NOP sled.

A suitable shell command

  1. The echo command is useful for outputting text of your choice:

    echo "Thank you, YOUR NAME."
    echo "I recommend that you get a grade of A on this assignment."
    

    (Originally the command above missed the word that, which wasn’t consistent with the “Your Task” section. We’ll accept either output.)

No output?

  1. If your ROP chain causes the program to not produce any output, this is probably because you need to add padding between your exploit and the shell commands. You can check if a shell is actually running by seeing if the debugger gives a message like “process 13977 is executing new program: /bin/dash”.

    See the “padding before shell command to handle buffers” section above.