Assignment: ROP
Changelog:
- 13 March 2026:
module loadcommand to account for portal updates
Your Task
-
Download the first target program dumbledore1.exe. 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 run this program 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, like your computing ID).
We will not run this program with ASLR disabled, but it is linked to require fixed addresses for its machine code.
You will submit an attack program whose name starts with
attack1. We will run this program such that it receives its stdin from dumbledore1.exe and such that its stdout is send to the dumbledore1.exe. See instructions on “testing your program” below for emulating this.(This is different from prior assignments primarily to give you the flexibility of not having the input be given to the dumbledore1.exe all at once and/or to have input only be given after being prompted for. If you don’t take advantage of this flexibility, you can test like in prior assignments.)
Your program may not modify the dumbledore1.exe executable or interact with it other than by supplying input to it or receiving its output.
You can also supply your program has a
.cor.ccfile (in which case we will compile the program accordingly instead of running it with python3). -
Download the second target program dumbledore2.exe.
This program is the same as the first except:
-
it is linked to be position independent;
-
instead of reading a line, it asks how many bytes to read, then reads that many bytes for the name.
Because it reads the input differently, it may have a slightly different stack layout (which would affect ROP chain placement).
In a program whose name begins with
attack2do the same such that it works:-
with ASLR disabled (
setarch -R ./dumbledore2.exe; notsetarch -RL) for partial credit -
regardless of whether ASLR is disabled for full credit.
(My recommendation would be to get it working with ASLR disabled, then with ASLR enabled.)
Both of the versions of the program output the address of
_IO_getsbefore prompting for a name, which can aid with defeating ASLR. z # Tools -
-
For this assignment, we recommend using the tool ROPgadget. There are also other tools, some of which are more sophisticated, like angrop (which I don’t describe here mainly because it is harder to install). Whatever tool you use, please describe that in the
attackfile 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
pipwithin that virtual environment to install them.
setting up a Python virtual environment
-
On portal or NoMachine, the version of
pythonthat’s available by default is missing support for virtual environments. So you should first usemodule loadto load a more recent version of Python as in:module load gcc/14.2.0 python/3.12.3(You’ll need to rerun this command for each terminal you operate or modify your
.bashrcor similar.) -
Once you have an appropriate version of Python, choose a directory to create the virtual environment; we’ll use
rop-venvfor this example. Create that directory and usepython -m venv ...to setup a “virtual environment” in that directory:mkdir rop-venv python -m venv rop-venvIf you look inside that directory, you’ll see that
pythoncommand createdbin,lib, and several other subdirectories and apyvenv.cfgconfiguration 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
pythonwill automatically use libraries installed in that virtual environment instead of just the ones from the system. Also, commands likepipfor 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.6to install ROPgadget (selecting version 7.6, in this case).
After doing this, typing
ROPgadget --helpshould 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
binsubdirectory of your virtual environment directory.)
Testing your program
-
If your program immediately outputs its the attack input (without reading any input), you can test it like:
python3 attack1.py > attack.txt ./dumbledore1.exe < attack.txtNote that your program must work if we rename
attack.txtto another name.Replace
python3 attack1.pyas appropriate if you write your attack in C or C++. -
If your program outputs the attack input with deliberate pauses (without reading any input), you can test it like:
python3 attack1.py | ./dumbledore1.exe-
Note that it’s important the correct output come from
dumbledore1.exe, and not your program. (It’s not acceptable if your attack1.py outputs the “Thank you …” message to stderr or similar.) -
If you want to imitate this in a gdb, you can do something like:
$ gdb ./dumbledore1.exe ... (gdb) run <(python3 attack1.py)
-
-
If your program reads the output of the dumbledoreX.exe (for example, waiting until it reads a prompt before supplying the next line of input):
This will place the output of dumbledore1.exe in dumbledore1-output.txt.
You can use “named pipes” to connect your program to ./dumbledore1.exe:
-
First make two named pipe files with the
mkfifocommand:mkfifo inputfromvuln mkfifo outputtovuln -
Run your attack program with its input and output redirected to use these pipes:
python3 attack.py <inputfromvuln >outputtovulnThis command should hang until you run the vulnerable program using these pipes (next step).
If you want to also see the program’s output, you can use something like
cat inputfromvuln | tee /dev/tty | python3 attack.py >outputtovuln(This represents a shell “pipeline”, where the output of
cat inputfromvulnbecomes the input oftee /dev/tty, and the output fromtee /dev/ttybecomes the input frompython3 attack.py >outputtovuln. Theteecommand copies its input the files specified by each of its arguments and its output./dev/ttyis a special file which represents the currently active terminal.) -
Then, probably in another terminal on the same machine1, run the vulnerable program using these pipes:
./dumbledore1.exe <outputtovuln | tee inputfromvuln -
You could also, potentially, do this in gdb using something like
gdb ./dumbledore1.exethen at the
(gdb)prompt using something likerun
inputfromvuln
-
Alternately, we supply an
interact.pythat attempts to automate the above:python3 interact.py \ --attack python3 attack.py \ --vulnerable-output-copy dumbledore1-output.txt -- ./dumbledore1.exe
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.exewill 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.txtin a text editor).ROPgadget.py also has many additional options which you can find by running
ROPgadget --help
When executables are offset
-
dumbledore2.exe is position-independent, so it can be loaded at different addresses.
By default, ROPgadget.py assuems that executables and library 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
ROPgadgetan offset with the--offsetoption:ROPgadget --binary binary-file --offset 0x123456 ...(where
0x123456is the difference between the addresses shown inobjdumpand the actual address to which the the binary-file is loaded). -
For getting things working without ASLR enabled, you run the debugger with debugger with ASLR disabled and look at the addresses there to discover the offset. You can supply this as an offset argument to ROPgadget.
-
To handle ASLR, you can use the pointer output by dumbledore2.exe. It outputs the address of
_IO_gets. You can look up the offset of _IO_gets in the executable withobjdump -t dumbledore2.exeand use this to determine the starting address of the executable in memory. For example, if_IO_getswere 0x1234 bytes into the executable and was found at memory address 0x993234, then we would know the executable starts in memory at 0x992000Once you know the starting addres at which the executable is loaded, you can take an ROP chain generated with ROPgadget with no offset and transform it from code like:
p += pack('<Q', 0x123456)to
p += pack('<Q', the_starting_address + 0x123456)in order to get an ROP chain customized for the executable’s address.
The ROPchain option
-
ROPgadget has an
--ropchainoption 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 binary-file --ropchainwill 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.txtto 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
%rdito 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 or start with 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.For dumbledore1.exe, this causes the program to stop reading input, so you would not be able to input these addresses. You can have ROPgadget.py ignore these gadgets by using the
--badbytesoptiondumbledore2.exe does not have this restriction.
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
objdumpoutput 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.
Alternate: manual ROP chains
-
You can also produce an ROP chain manually. This would avoid the need to execute a command with a shell. But I think this approach is more work overall.
-
If you construct an ROP chain manually, one option would be to construct a call to PrintGradeAndExit or to puts. One way to “call” a function would be to return to its entrypoint.
Another option would be to make the write system call directly.
Note that these options (including running a shell) are essentially the same as those presented in the OVER assignment, but instead of writing machine code, we are writing ROP chains.
-
If you call existing functions, you may need to worry about stack alignment. The calling convention on x86-64 requires the stack be at a multiple of 16 when
callis made. This means that it needs to be equal to 8 mod 16 when a function starts running.(If this causes your code not to work, you’ll notice something like a segmentation fault on an instruction like
movaps %xmm0, 10(%rsp)(movaps = “mov aligned packed signle precision floating-point values”) or some similar “aligned” move instruction.)
You can insert a gadget that is just
retin an ROP chain to effectively move the stack pointer by 8 bytes without doing anything else. -
When constructing these calls, you need to set the argument registers correctly. Some of the arguments will be pointer to string. Although it’s possible to have a string on the stack and provide its address, this often won’t work because the function you run (e.g. puts) will have local variables that overwrite that space.
Some options for avoiding this issue:
-
You can do what ROPgadget does to create the string /bin/sh. It uses an ROP chain to write it to part of the .data section, then uses the address of that part of the .data section.
For example, you could repeatedly use a gadget like
mov %rdi, (%rax); retto copy 8 bytes of your strings into some fixed memory lcoations until you’ve copied the entire strings.Note that this strategy avoids the need to find out the address of the stack and make sure it is consistent, so it is probably more reliable.
-
You can put the strings above (at a higher address than) your ROP chain on the stack.
-
You can find a gadget that lets you “pivot” the stack location to “skip over” your strings that are before the first return address.
An example of such a gadget would be
pop %rsp; ret; this gadget will pop a value of the current stack to set a new stack pointer, then use that new stack pointer to find the next gadget to run.
-
Doing I/O from Python
-
You could use input() to read a line of the program’s input as a string.
-
You can use something like
int("0x12345", 16)to convert the hexadecimal number 0x12345 into an integer. -
You can do
sys.stdout.buffer.write(...)to write bytes to the program.This
write()method expects a sequence of bytes, not a string.To write a string, you can do something like
the_string.encode("UTF-8")to convert it to bytes, or usesys.stdout.write(...) -
To make sure what you wrote goes immediately to the program, use something like
sys.stdout.buffer.flush(). (Alternately, you can reconfigure the buffering onsys.stdout.buffer.)
Doing I/O from C
-
You could use
fgets()to read a line of input into a char array. -
You can use
strtol("0x12345", NULL, 16)to convert the hexadecimal number 0x12345 to an long -
You can do
fwrite(...)to write bytes to a program. -
To make sure what you wrote goes immediately to the program, use something like
fflush(stdout). (Alternately, you can reconfigure teh buffering with something likesetbuf.)
Executing a command with a shell
Delaying or 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 cannot just output the shell command immediately after the exploit string for the buffer overflow.
stdio.h functions like
getsdefault 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
-
wait after sending the buffer overflow string, and then send the shell command. If you do this, you will not be able to use the testing technique of saving your programs output to a file and then running it directly.
(To make this waiting work consistently when using a debugger, you might want to read the program’s prompt before outputting your buffer overflow string.)
-
have some unused bytes between the attack string and the shell commands, such as several thousand newlines. (Note that the python expression
"\n" * 10000will 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 "" echo "Thank you, YOUR NAME." echo "I recommend that you get a grade of A on this assignment."(The first echo command ensures your output starts on a blank line.)
No output?
-
If your ROP chain causes the program to not produce any output, this is probably because you need to add padding or more delay 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.
-
portalis a set of many machines namedportal01,portal02, etc. You may need tossh portalXXwhere whereportalXXis the specific machine you are using. ↩