Skip to content
Courses

ROP Chains: Message Relay

MitigationsLab 2 of 4180 minutesLab

The relay station forwards messages. You hand it a message, it reads the message back to you, and it lets you correct it once. It also holds a routing token that it never prints.

Your goal is to get the routing token printed, and to have the program exit cleanly afterwards.

Download message-relay-v1.0.0.tar into ~/underdunn-course/downloads and check it against this digest:

f5c246cfbba19e4e7ef174ce76bfe5821526c0a64474ab13ed55540da1ba3e1d message-relay-v1.0.0.tar
Terminal window
cd ~/underdunn-course/downloads
sha256sum message-relay-v1.0.0.tar
mkdir -p ~/underdunn-course/labs/message-relay
tar -xf message-relay-v1.0.0.tar -C ~/underdunn-course/labs/message-relay
cd ~/underdunn-course/labs/message-relay/message-relay-v1.0.0
sha256sum -c CHECKSUMS.sha256
sh build.sh
sudo sysctl -w kernel.randomize_va_space=0

Read build.sh before you run it and note what isn’t in it. Read TASK.md and relay.c, then talk to the relay a few times before you measure anything.

Hint 1: the frame and the canary

Two numbers you might expect to be equal are not: the buffer’s declared size, and its distance to the canary. Something else is in the frame.

Terminal window
objdump -d -Mintel ./relay | sed -n '/<relay>:/,/^$/p'

Subtract the buffer’s offset from rbp from the canary’s. The return slot is at rbp+8.

The relay reads twice into one buffer and answers in between, and the cap on the first read isn’t arbitrary. It prints your message back with %s, which stops at the first zero byte, and the value you want begins with one. Send exactly one byte more than the distance, capture the echo as bytes, and rebuild the canary as a zero followed by the seven that came back. Check it against fs:0x28 in GDB before you rely on it.

Hint 2: two different obstacles, and only one is about the stack

Before building anything, establish two things, and predict each answer before you check it.

Can a payload of your own run anywhere you can reach? That’s a question about permission, and it’s decided before the program executes a single instruction. The ELF program headers record it and the live process mappings confirm it.

Does the program contain the instructions a reuse chain needs? That’s a question about supply, and a disassembly listing is the wrong place to look.

Terminal window
readelf -l ./relay | grep -A 1 GNU_STACK
ROPgadget --binary ./relay | tail -1
for g in 'pop rdi' 'pop rsi' 'pop rax' syscall; do
printf '%-10s %s\n' "$g" "$(ROPgadget --binary ./relay | grep -c "$g")"
done

Check the total next to the four counts. The relay isn’t short of gadgets. It’s short of these ones.

Hint 3: the instructions are somewhere else

The relay loads a library that wasn’t written for you and contains far more than the program does. Scan it the same way. You need something to load the first argument register, something to load rax, and a syscall.

Terminal window
ROPgadget --binary /usr/lib/x86_64-linux-gnu/libc.so.6 --only 'pop|ret' | grep ' pop rdi ; ret$'
ROPgadget --binary /usr/lib/x86_64-linux-gnu/libc.so.6 | grep ': syscall$'

What a scan gives you for a shared object is not an address. Work out what it is and what you have to add to it, and confirm the bytes with objdump -d --start-address=... --stop-address=... before trusting the tool.

For the base: the relay blocks on its first read, which leaves a live process for as long as you need. Read /proc/<pid>/maps, and don’t take this number from GDB without checking it against a run outside GDB.

Hint 4: building the chain

The chain is a queue of return addresses. Every link that has to come back ends in a return; the last one doesn’t, because it can’t.

Printing needs an argument register and a function that prints. One of those is in the library and one is in the program, and mixing them in a single chain is fine, because ret doesn’t care where an address points.

Work out what the exit system call needs in rax and rdi, and order the loads so nothing later undoes something earlier. Check the status with echo $?, and treat a segmentation fault as an unfinished chain rather than a near miss.

Stage one leaks. Stage two is filler to the canary, the canary, filler for the saved frame pointer, then the chain. Eight slots is enough.

Hint 5: it works, then occasionally aborts

Roughly one run in twenty-five, one of the seven bytes you’re leaking is itself 0x00, so the echo stops early and you rebuild the canary from six real bytes and one that belongs to the prompt.

Check the length of what came back rather than assuming it, and start a fresh process when it’s short. The next one has a different canary. Don’t delimit the echo with a newline either, because a canary byte can be 0x0a.

The whole route

Derive the distance to the canary from the disassembly. Leak the canary with a stage-one write one byte past it, rebuilding as zero plus seven.

GNU_STACK is RW, so nothing of yours can execute. The relay has plenty of gadgets and none of the four you need, so take pop rdi ; ret, a pop rax, and syscall out of libc, add each offset to the base from /proc/<pid>/maps, and confirm the bytes with objdump.

Stage two: filler, canary, eight bytes of frame-pointer filler, then set rdi to the token and return into the program’s own print routine, then load rax and rdi for exit and return into syscall.

Drive it with subprocess, os.read rather than a fixed count, and read the library base inside the solver so it’s never stale. Run it three times, retrying on a short leak.

Paste exactly the line the relay printed, with no surrounding whitespace:

Enter the complete flag, including UNDERDUNN{ and the closing brace. The check happens in your browser; the flag is not sent anywhere.

The token says nothing about the exit status, which is half of what this lab asked for. A chain that prints and then segfaults isn’t finished.

If something looks wrong

The gadget offsets from the lessons don’t work. They belong to one build of glibc. Read your own and confirm the bytes with objdump.

The library base changes between runs. Check sysctl kernel.randomize_va_space. It resets on reboot, and a debugger disables randomisation on its own regardless of the setting.

A solver run is still alive. Check the command with ps -p PID -o pid=,comm= before signalling it, and close the pipe rather than leaving the relay waiting on a read.

Leave the sysctl as you found it when you’re done. The next lesson changes it.