Assignment: OVER
Contents
Changelog:
- 18 Feb 2025: update link to slides; new set of potential libcs
- 19 Feb 2025: update dumbledore.exe to disable feature flag requesting shadow stack/IBT
- 19 Feb 2025: mention aligning stack for push + ret strategy
- 21 Feb 2025: make slides about stack smashing link actually go to non-empty PDF
In this assignment, you learn and demonstrate how buffer overflow vulnerabilities can be exploited.
Your Task
-
Your job is to create an buffer overflow exploit for this dumbledore.exe
When run normally, the executable accepts a name as input; for most names, the executable will then output something like
Thank you, (NAME INPUTTED). I recommend that you get a grade of F on this assignment.When supplying your buffer overflow input its output should be:
Thank you, YOUR NAME. I recommend that you get a grade of A on this assignment.where YOUR NAME is replaced with your actual name.
You will submit a program that outputs your malicious input for dumbledore.exe, and the comments of this program should document how your exploit was produced. That program will be submitted in a file called
attack.py3orattack.py2orattack.corattack.cc.If your attack program is in a file called
attack.py3orattack.py, we intend to test your exploit as follows:- create a new directory and placing dumbledore.exe and either this copy of libc.so.6 from a Ubuntu 20.04 system or this copy of libc.so.6 from a Ubuntu 22.04 system or this copy from a Ubuntu 24.04 system.
-
from that directory, running the shell commands:
python3 attack.py3 > attack.txt setarch -RL env - LD_LIBRARY_PATH=. ./dumbledore.exe < attack.txt
(The use of a specific version of
libcis probably not necessary for the exploit to work consistently, but we provide this to minimize the likely factors causing variation in what happens.)If your attack program is called
attack.py2, we will runpython2instead ofpython3; if it is calledattack.corattack.cc, we will compile your submission into an executable and run that instead of python.Your attack program should only output the attack string; it may not modify
dumbledore.exeorlibc.so.6or other files.(Explanation of that commmand:
- The
setarch -RLpart of the command disables address space layout randomization, so that your exploit can potentially depend on the initial address of the stack or libraries. - The
env LD_LIBRARY_PATH=.part of the command sets theLD_LIBRARY_PATHenvironment variable used by the linker to.(“the current directory”), so that the linker searches for libc.so.6 in the current directory instead of using any system version. This should limit the likelihood that your exploit won’t work on our system due to having a slightly different version of the C standard library. (Environment variables are a set of key/value pairs tracked by every process on Linux.) - The
env -part of the command clears all the environment variables we do not explicitly set to make the size of all environment variables consistent. Since environment variables are placed on the stack beforemainruns on Linux, this makes the address of the stack more consistent. )
-
Make sure that your attack program includes comments explaining how it works/why you choose particular values. (This is one reason why we are having you submit a program and not its output.)
-
Submit your
attack.py3orattack.py2orattack.corattack.ccfile to the submission site (linked above).
Assignment Resources
-
The articles “Detection and Prevention of Stack Buffer Overflow Attacks” (VPN may be required off-campus) and “Smashing the Stack for Fun and Profit”
-
Please ask any clarifying questions on Piazza or in Office Hours.
Hints
Understanding in the Executable
-
The supplied executable contains a stack buffer overrun in the
GetGradeFromInputfunction, which calls the C standard library functiongets.gets, as its manpage documents, does not check the length of the buffer supplied as an argument as is unsafe. -
Create a file name data.txt containing your name and run
dumbledore.exe./dumbledore.exe <data.txt Thank you, Charles Reiss. I recommend that you get a grade of F on this assignment. -
Your goal is to produce an input file so that the output of the program execution is as follows:
./dumbledore.exe <data.txt Thank you, Charles Reiss. I recommend that you get a grade of A on this assignment. - To do this, you will use the stack smashing technique we discussed in class. There are several strategies to write the machine code run by this attack, which both have extensive hints below:
- The first is to call a convenient
PrintGradeAndExitfunction in the supplied executable. To do this, you should be careful to set the stack pointer is less than the address of your machine code, so this function does not corrupt your machine code/data when it executes. This is probablythe easiest solution. - The second is to write code that directly prints out the string, without calling any application functions. We give examples of how to make direct calls to the operating system to print strings and to exit below. A challenge with this approach is that you cannot include the newline character directly in the middle of your attack string.
- The third is to use shellcode that actually executes a shell, and then send commands to print out the appropriate strings in from that shell (for example using “echo”). A challenge with this approach is that the supplied application does buffered I/O — if input is available, it will read in (from the OS) more than just the one line you input, assuming it will be needed later by the application anyways. This means that when the newly executed shell tries to read its input, some bytes after the buffer overflowing line may have already been consumed.
- The first is to call a convenient
- Note that the location of the stack pointer can vary slightly when your environment changes. See the section “Variations in the location of the stack pointer” under hints below. Because of this, you should plan on using a NOP sled so you don’t have to precisely predict the address of the stack pointer.
Testing
-
Rather than typing
setarch -RLeach time to disable ASLR you can run a shell with ASLR disabled by running
setarch -RL bash -
I recommend creating a file name sometimes like
input.txtwhich contains the input you are working with. After setting up a shell as described above, this will let you run the executable usingenv - LD_LIBRARY_PATH=. ./dumbledore.exe < input.txt -
You can use the debugger and still have environment variables controlled appropriate by having
gdbrun the program withenv - LD_LIBRARY_PATH=.using theexec-wrappersetting. (Relevant GDB manual section.)Create a script containing these commands called wrapper.py with the contents
#!/usr/bin/python3 import os import sys os.execve('./dumbledore.exe', ['./dumbledore.exe'] + sys.argv[2:], {'LD_LIBRARY_PATH':'.'})(I use a python script here instead of the
envcommand in order to have control overargv[0], the program name that is passed todumbledore.exe) and then run the command:chmod +x wrapper.pyThen, if you start
gdb dumbledore.exe, usingset exec-wrapper ./wrapper.pyor set exec-wrapper python3 wrapper.py
before you ask gdb to
runthe program will ensure that it is run with thewrapper.pythe environment the same as when we try to exploit it.
Disassembly and Debugging
-
A useful starting point is using
objdumpto disassemble the executable file. -
Using the debugger
gdbcan be helpful for debugging and refining your buffer overflow payload. See this list of useful GDB commands. But see the warning below about the debugger’s environment slightly changing the location of the stack pointer. -
In particular, after looking over
objdumpoutput, a good second step is running the program in GDB to find the address of the stack pointer at a relevant time. -
Since we tell you the buffer overflow occurs in
gets, it is helpful to find the call togetsand examine the state of the program at that time in the debugger. -
Drawing a picture of the state of the stack is helpful.
Shellcode production
-
You can run
objdumpon.ofiles. I would recommend usingobjdump -dr file.o, which will show disassembly and unresolved relocations, so you can tell if you accidentally generated machine code which needs the linker to complete it. (Recall that relocations are addresses the linker needs to fill in later.) -
On 64-bit x86, you can use RIP-relative addressing (that is, program counter-relative addressing) to load addresses within your machine code without worrying about the location at which your machine code is placed in memory:
code: movq value(%rip), %rax leaq value(%rip), %rbx ... value: .quad 42will place the value
42in%raxand the address of the value42in%rbx. But, unlike not using(%rip), the resulting machine code will not have any depenencies on the memory addresses eventually assigned tocodeandvalue. It will only depend on how far apartcodeandvalueare in memory.Other techniques for finding the address of your code include using a sequence like:
call next next: popq %raxto load the current program counter into
%rax. Thecallinstruction uses an address relative to the current program counter, so the resulting machine code does not include hard-coded addresses. -
Since
getsreads until a newline, you need to make sure your machine code does not contain newlines. -
The
objcopyutility can be used to extract a particular section of an object file. For exampleobjcopy -O binary --only-section=.text compiled_code.o compiled_code.rawwill take the
.textsection of the object filecompiled_code.oand put it incompiled_code.raw. (compiled_code.omight be a file generated bygcc -c some_assembly_file.s.) You might then look at the resulting file with a tool likeghexorodto extract the machine code in an less cluttered way than looking at theobjdumpoutput. -
Here is a python 3 program that outputs a binary file as a C array declaration.
Running an executable function
-
The executable contains
PrintGradeAndExitfunction. To figure out what the arugments mean, figure out what the arguments of its call toprintfare. -
A challenge with calling the
PrintGradeAndExitfunction is that our machine code and data is on the stack and could be corrupted by our call toPrintGradeAndExitif we are not careful.To avoid this, you can explicitly set the stack pointer. For example, you might use
leaq label-0x100(%rip), %rspto set the stack pointer to point
0x100bytes before alabelin your shellcode.(
label-0x100is assembly syntax for0x100bytes beforelabel.) -
Recall that the
pushqthenretallows you to jump to an location from machine code without worrying about where that machine code ends up relatively in memory. But see note below about making sure the stack is properly aligned. -
Note the x86-64 calling convention requires that the stack pointer be a multiple of 16 when a function is called. With normal use of the call instruction, this means that the stack pointer will be 8 minus a multiple of 16. (The
callinstruction will subtract 8 to push the return address.) Some functions, includingprintfin some versions of libc on Linux, will crash if the stack is not setup at properly “aligned” address.If you don’t take extra measures, probably your
pushandretsequence won’t have the stack be a multiple of 16.You can round down the stack pointer to a multiple of 16 with:
and $-16, %rspand then make it be 8 minus that by pushing a value. (On x86-64, all push instructions push 8 bytes.)
Alternate print/exit
-
If you don’t call
PrintGradeAndExit, you could instead print out the output you want directly, then exit. This is more realistic but a little more challenging. -
Instead of including a newline in your buffer overflow, you can, instead, include code to compute a newline (e.g., by adding or subtracting from another value) or to copy one from elsewhere in the application.
-
To print something out from your machien code, you could call the
printf@plt“stub” (hard-coding its address) or make a write() system call directly. An example assembly snippet to make a write system call is:mov $1, %eax /* system call number 1 = write */ mov $1, %edi /* arg 1: file descriptor number 1 = "standard output" */ lea string, %rsi /* arg 2: pointer to string */ mov $length_of_string, %rdx /* arg 3: length of string */ syscall -
If you decide that your attack code should exit directly, you can do this by caling the
exit@plt“stub” or by making anexit_groupsystem call directly. An example assembly snippet to make anexit_groupsystem call is:mov $231, %eax /* system call number 231 = exit_group */ xor %rdi, %rdi /* arg 1: exit code = 0 */ syscall
Executing a shell
-
You can find an example of shellcode that runs runs the
execvesystem call to execute/bin/shin this archive of shellcode. Note that some of the shellcode you find may make assumptions about the initial contents of registers or location of the stack pointer. If you use prebuilt shellcode like this, you must clearly cite its source. -
On Linux,
execvereplaces the current program with the executed program. The new program inherits the same input and output as the prior program. -
Standard I/O functions read ahead in their input. For example,
getsmay read part of the next line, saving it in a buffer for future calls togetsor other<stdio.h>functions. These buffers are not passed to the new program byexecve. To compensate for this, you may need to include padding in your input. -
You can print out a string from the shell using the
echocommand. -
By default, the shell won’t print out a command-prompt when its input is not a terminal.
Variations in the location of the stack pointer
-
The stack can start at slightly different locations depending on how the program is run. One cause of this is that Linux stores program arguments and “environment variables” on the stack, so the location on the stack pointer on entry to
maindepends how much space these take up. We give instructions to use theenvcommand to clear environment variables before running to make this consistent.But in a more realistic scenario, one would want to make an exploit that isn’t dependent on the exact size of the environment variables, which may be hard or impossible to predict precisely. (If you run
printenvfrom a Unix-like shell, you’ll probably see a great number of environment variables.) -
For example, the program
int main(void) { int x; printf("%p\n", &x); }has different output on my system depending on the environment variables, even with ASLR disabled:
$ setarch x86_64 -RL bash $ ./stackloc # run normally 0x7ffffffffe034 $ env - ./stackloc # run with no enviornment variables 0x7ffffffffed84 $ gdb ./stackloc ... (gdb) run 0x7ffffffffe004 -
Without taking precautions like we recommend with the
exec-wrapperoption, a particular case where this is a problem is running the program in the debugger versus not. The debugger may set a few environment variables itself, and when you run the program in the debugger, they would normally be set when the debugger runs the program in question. -
One very common way to avoid problems despite the stack starting in different locations is to use a “NOP sled”. Place a large string of NOPs before your exploit code and try to “aim” the return address in the middle of this string. This will prevent you from being sensitive to small differences in the location of the stack.
-
An encoding for a 1-byte NOP instruction on x86 is
0x90. -
You could also try to figure out how to keep the debugger from changing the enviornment (likely with some
unset envcommands), but this is less preferable, because it means your exploit is less reliable.
Writing binary data with Python 3
-
By default Python 3 expects to output strings as UTF-8 or something similar, in which you can’t easily include arbitrary bytes.
-
You should avoid using strings and instead use
bytesorbytearrayobjects. -
To output binary data to stdout, use something like
sys.stdout.buffer.write(some_bytes). (See “note” in documentation here.) (You won’t be able to useprintbecause it needs to convert its arguments to strings first.)
Credit
This assignment was adopted from Jack Davidson’s Fall 2016 assignment, which was adopted from one given previously by Andrew Appel in Princeton’s COS 217.