Project 1: Memory Corruption
Due: Monday 2 February, 11:59 PM
Goal
Welcome to the special 24nd anniversary remix edition of the memory corruption targets/sploits project!
The goal of this assignment is to gain hands-on experience with the effect of buffer overflow bugs and format string bugs.
You are given the source code for five exploitable programs,
target1, target4, target5, and target6. Your goal is to write
four exploit payloads (egg1.raw, egg4.b64, egg5.b64, and
egg6.b64), that, when supplied to the corresponding target program
should result in execution of shellcode. You are also given a warmup
towards exploiting target5, called target5easy; here the goal is
just to get the target to print out a success message when given
egg5easy.b64; shellcode is not involved.
Because we live in the future, there is no need to construct these
payloads using a C sploit program. Instead, you will write short
Python scripts (make-egg1.py, make-egg4.py, make-egg5easy.py,
make-egg5.py, and make-egg6.py) that write the corresponding
payload file.
Background
You have already read Aleph One’s “Smashing the Stack for Fun and Profit.”
For background on exploiting malloc metadata corruption, see the two classic articles by Maxx and anonymous in Phrack 57. For the theory behind malloc metadata, see the survey by Paul Wilson and friends.
For background on exploiting format string bugs, see the classic paper by scut and Team TESO. Read this paper closely, especially the sections on short writes (4.1) and direct parameter access (4.3).
You should read these papers for inspiration, not as recipes to follow literally. The 32-bit x86 environment they describe is different from our 64-bit RISC-V VM, and of course umalloc is not the same as 2001-era dlmalloc.
The course virtual machine
Now that Debian has promoted RISC-V to an official port, we will be using a Debian virtual machine. We will use the Qemu emulator to run this machine. You should be able to get Qemu running in Linux (or a BSD), on a Mac, or in WSL.
For all but the last programming project, a precompiled Qemu
distribution will be all you need; you can install it from you package
manager. If you are running Debian (or a Debianalike such as Ubuntu),
install the qemu-system-misc package, if you don’t already have it.
On a Mac with Homebrew, brew install qemu should work. The Qemu
site has installation instructions for other platforms and package
managers.
Download and unpack the course VM. The download is several hundred megabytes, but the VM disk can grow to 10 GB, so make sure to put it somewhere you won’t run out of space.
Running the VM
Run the virtual machine with the command
path/to/vm/runvm.sh
(where path/to/vm is replaced by the actual path from your current
working directory to the directory containing the VM files). If all
goes well, this will boot a VM all the way to a login prompt.
The runvm.sh script will boot a machine with two virtual cores and 8
GB RAM. If your host machine will have trouble delegating that many
resources to the VM, you can boot single-core by removing the -smp 2
line, or the amount of RAM by editing the -m 8192M option to some
smaller value.
To shut down the VM, log in as root, either directly on the console or
by running su - on a user shell over ssh, and type poweroff
If the VM isn’t shut off cleanly, its filesystem may become corrupted
and you may lose work in progress. It’s always a good idea when you
are developing in a VM to have a copy of the files elsewhere. If you
set up ssh access and install the rsync package on the VM, you can
easily sync files between the VM and host with rsync. (You’ll need to
install rsync first: as root, apt install rsync. See below for
more).
Logging in on the console
Our VM has a root (administrator) account with password root and
a user account called user with password user.
Logging in from the host over SSH
Our runvm.sh script exposes the VM’s SSH server on port 8291 of the
host. Rather than logging in directly on the console, we recommend
you SSH in from the host; this will give you a terminal with more
features than the console. You can use a command like ssh localhost
-p 8291 -l user to log in over SSH as the user user.
This becomes even easier if you set up ~/.ssh/config on your host
machine (not the VM) to add an entry like the following:
Host riscv
Hostname localhost
Port 8291
User user
If you do that, then typing
ssh riscv
on the host should log you into the VM. If you copy an SSH public key
to ~user/.ssh/authorized_keys on the VM, then you’ll be able to log
in without typing a password.
If the key you usually use for SSH is password-protected, you could
make a dedicated key just for accessing this VM (ssh-keygen -t
ed25519 -f vm-key will do the trick), add the contents of the
corresponding vm-key.pub file to authorized_keys, and add an
IdentityFile /path/to/vm-key specifier to the Host riscv stanza in
the config file on your host.
Once you have arranged for pubkey auth, you may wish to disable SSH
logins via password. As root, edit /etc/ssh/sshd_config, uncomment
the PasswordAuthentication line and replace yes with no, then
ask sshd to reload its configuration (service sshd reload should
do the trick) or just restart the VM.
Package management
Log in as root. Run
apt update
It will update your package index. When your package index is up-to-date, you can install packages with the command
apt install [package-name]
We created the VM specifically for this course, so you should already have many of the packages you need, but you might want to customize it by installing your preferred editor, version control system, code search tool, etc.
You may need to rerun apt update before installing new packages if
it has been a while since you last did.
(You can also update already-installed packages with apt upgrade.)
Note that VSCode’s remote development extension pack does not work on
RISC-V.
If you have used VSCode in the past, now may be a good time to learn
to use a programmer’s editor. (See also our prohibition on the use
of AI tools!) You can install Emacs (Debian package
emacs-nox), Vim (vim) Neovim (neovim), Kakoune (kakoune),
Helix (hx), or another editor packaged for
Debian.
The VM clock
The VM should synchronize its clock each time it boots. If you suspend your host computer while the VM is running, the VM clock may end up wrong. As root, run
ntpdate-debian
to update the clock.
The environment
We assume that you have set up the course virtual machine environment for CSE 291Y as described above.
Running as the user (not as root), download and unpack the project tarball in the VM using the command
curl -O https://cseweb.ucsd.edu/classes/wi26/cse291Y-a/dist/proj1.tar.gz
and unpack the tarball using the command tar xavf proj1.tar.gz.
Run make to build the targets as well as the driver program.
If you run a target program directly from the shell, the base
addresses of its stack and heap will be randomized by ASLR. If you
run a target program through GDB, ASLR will be disabled but stack
addresses will still vary depending on the execution environment. To
make exploits consistent, we will launch the targets through the
driver program. The driver program takes the following command-line
arguments: the target binary to run; a filename to pass to the target
as an argument; and either your pid. For example, I might run
target1 on egg1.raw using the command line:
$ ./driver target1 egg1.raw A13371337
Using GDB
You can arrange to debug the target when launched in exactly this way, so that you can use GDB to read addresses, as follows.
First, run GDB on the driver program: gdb driver. Next, before
telling GDB to run the program, ask to get control back when on
execve: catch exec. Now have GDB launch the driver, specifying
command-line arguments as above: run target1 egg1.raw A13371337.
If all goes well you should get control on exec. Now is the time to
set breakpoints in the target program (if you tried to do it earlier,
you’d have gotten breakpoints in the driver program instead): break
foo. Then continue.
Note that the length of the argument to the target program will affect the placement of stack frames (because, as you know from your operating systems course, the kernel places arguments at the very top of the stack). Try to figure out how long your egg should be early on, then fine-tune its contents once stack layout is stable.
As we mentioned in class, GDB is a powerful tool that will help you in this class and beyond. Time spent reading the GDB manual is time well spent.
The assignment
Your goal is to craft a binary payload, in egg1.raw, that will cause
target1 to execute shellcode when run as
./driver target1 egg1.raw <your-pid>
Replace <your-pid> with your actual PID (also called SIS ID, it is
your student identifier that consists of an A followed by eight
digits).
Likewise, your goal is to craft a base64-encoded payload in egg4.b64
that will cause target4 to execute shellcode when run as
./driver target4 egg4.b64 <your-pid>
to craft a base64-encoded payload in egg5easy.b64 that will cause
target5easy to print a success message when run as
./driver target5easy egg5easy.b64 <your-pid>
to craft a base64-encoded payload in egg5.b64 that will cause
target5 to execute shellcode when run as
./driver target5 egg5.b64 <your-pid>
and to craft a base64-encoded payload in egg6.b64 that will cause
target6 to execute shellcode when run as
./driver target6 egg6.b64 <your-pid>
The reason that egg4, egg5easy, egg5, and egg6 are
base64-encoded is so that you can include 0 bytes in the decoded
version; by contrast, you should have no 0 bytes in egg1.
Note that, in target5easy and target5, snprintf will still stop
processing its format string argument at the first 0 byte in
encounters. The base64 encoding will let you get 0 bytes onto the
stack (in the rawegg buffer) but not into the format string
itself.
We’ve also included a target5hard that resembles target5 but does
accepts a raw rather than base64-encoded input. Exploiting this
target is possible but much harder. You may try it for an extra
challenge, but you do not need to exploit it for full credit on this
project.
The make-egg scripts
In principle, you could create the exploit payloads egg1.raw, etc.,
by hand using a hex editor, or write programs in any programming
language to create them. For concreteness, we ask you to use Python
to create the exploit payloads. The script make-egg1.py should
write the egg1.raw file; the script make-egg4.py should write the
egg4.b64 file; the script make-egg5easy.py should write the file
egg5easy.py; the script make-egg5.py should write the egg5.b64
file; and the script make-egg6.py should write the egg6.b64 file.
You will submit the make-egg scripts along with the actual payloads; we will check that running the scripts regenerates the payloads files.
Rather than hardcoding a specific shellcode, your scripts should
incorporate the binary string shellcode.SHELLCODE (which is defined
for you in shellcode.py). The particular shellcode we’ve provided
you is 170 bytes long, but your scripts should work with other
reasonably sized shellcode, up to 200 bytes, which means you should
adapt your egg according to len(shellcode.SHELLCODE).
As shown in the starter files, Python has convenient facilities for
base64 encoding. Feel free to use any other Python features you find
handy, like bytearrays and struct.pack (for the latter you’ll need
to add an import struct), but don’t rely on any Python library that
isn’t part of the base system.
Logistics
You will submit using Gradescope. You should submit a zip file of your solution, without directory structure. Your solution should include at least the following:
-
egg1.raw,egg4.b64,egg5easy.b64,egg5.b64, andegg6.b64: These are your exploit payloads. -
make-egg1.py,make-egg4.py,make-egg5easy.py,make-egg5.py, andmake-egg6.py: These scripts should regenerate the submitted egg files exactly. -
PID: a text file with exactly one line, containing only your PID, exactly as you supplied it to the driver program.
Grading
TBD.