Assignment: ROP

Changelog:

  • 13 March 2026: module load command to account for portal updates

Your Task

  1. 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 .c or .cc file (in which case we will compile the program accordingly instead of running it with python3).

  2. 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 attack2 do the same such that it works:

    • with ASLR disabled (setarch -R ./dumbledore2.exe; not setarch -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_gets before prompting for a name, which can aid with defeating ASLR. z # Tools

  3. 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 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 gcc/14.2.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.)

Testing your program

  1. 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.txt
    

    Note that your program must work if we rename attack.txt to another name.

    Replace python3 attack1.py as appropriate if you write your attack in C or C++.

  2. 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)
      
  3. 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:

  1. Alternately, we supply an interact.py that attempts to automate the above:

    python3 interact.py \
        --attack python3 attack.py \
        --vulnerable-output-copy dumbledore1-output.txt
        -- ./dumbledore1.exe
    

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
    

When executables are offset

  1. 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 ROPgadget an offset with the --offset option:

    ROPgadget --binary binary-file --offset 0x123456 ...
    

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

  2. 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.

  3. 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 with objdump -t dumbledore2.exe and use this to determine the starting address of the executable in memory. For example, if _IO_gets were 0x1234 bytes into the executable and was found at memory address 0x993234, then we would know the executable starts in memory at 0x992000

    Once 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

  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 binary-file --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 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.

  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.

    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 --badbytes option

    dumbledore2.exe does not have this restriction.

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.

Alternate: manual ROP chains

  1. 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.

  2. 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.

  3. 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 call is 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 ret in an ROP chain to effectively move the stack pointer by 8 bytes without doing anything else.

  4. 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); ret to 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

  1. You could use input() to read a line of the program’s input as a string.

  2. You can use something like int("0x12345", 16) to convert the hexadecimal number 0x12345 into an integer.

  3. 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 use sys.stdout.write(...)

  4. To make sure what you wrote goes immediately to the program, use something like sys.stdout.buffer.flush(). (Alternately, you can reconfigure the buffering on sys.stdout.buffer.)

Doing I/O from C

  1. You could use fgets() to read a line of input into a char array.

  2. You can use strtol("0x12345", NULL, 16) to convert the hexadecimal number 0x12345 to an long

  3. You can do fwrite(...) to write bytes to a program.

  4. To make sure what you wrote goes immediately to the program, use something like fflush(stdout). (Alternately, you can reconfigure teh buffering with something like setbuf.)

Executing a command with a shell

Delaying or 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 cannot just output the shell command immediately after the exploit string for the buffer overflow.

    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

    • 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" * 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 ""
    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?

  1. 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.

  1. portal is a set of many machines named portal01, portal02, etc. You may need to ssh portalXX where where portalXX is the specific machine you are using.