Assignment: ROP
Contents
Changelog:
- 23 Feb 2025: new instructions for ROPgadget installation; add stuff on angrop
- 8 March 2025: explicitly indicate that name should be your actual name
Your Task
-
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
orattack.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
orattack.cc
orattack.py2
(which we will run differently, as in the OVER assignment). -
Tools
-
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
-
On portal or NoMachine, the version of
python
that’s available by default is missing support for virtual environments. So you should first usemodule 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.) -
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 usepython -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 createdbin
,lib
, and several other subdirectories and apyvenv.cfg
configuration file. -
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 likepip
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
-
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
-
The slides on return-oriented programming.
-
The “definitive” article on return-oriented programming, especially section 4.
Hints
Using ROPgadget
-
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
-
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 inobjdump
and the actual address to which the executable is loaded). -
If the you run the debugger with the same settings as the executable (such as by running
gdb
withsetarch x86_64 -RL gdb
), you should get addresses in the debugger which are consistent with the addresses from testing.
The ROPchain option
-
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 runningROPgadget --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 tooutput.txt
) -
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).
-
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.
-
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
- 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
-
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
-
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 futuregets
,getchar
, etc. calls (making them faster). But when the exploit code generated by ROPgadget.py starts the shell, these buffers are discarded. -
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
-
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?
-
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.