Skip to content

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, and egg6.b64: These are your exploit payloads.

  • make-egg1.py, make-egg4.py, make-egg5easy.py, make-egg5.py, and make-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.