Skip to content
Courses

ASLR Bypass: Night Dispatch

MitigationsLab 3 of 4180 minutesLab

The dispatch desk writes routing slips for the night’s deliveries. It prints each slip back to you and lets you correct it twice. It also holds a commissioning code that it never prints.

Your goal is to get the commissioning code printed, and to have the program exit cleanly afterwards.

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

f02eb3f188fee5f75589e93b16f693192cc7d705debfe62423b83bbb0cf529b3 night-dispatch-v1.0.0.tar
Terminal window
cd ~/underdunn-course/downloads
sha256sum night-dispatch-v1.0.0.tar
mkdir -p ~/underdunn-course/labs/night-dispatch
tar -xf night-dispatch-v1.0.0.tar -C ~/underdunn-course/labs/night-dispatch
cd ~/underdunn-course/labs/night-dispatch/night-dispatch-v1.0.0
sha256sum -c CHECKSUMS.sha256
sh build.sh

The build prints one warning, which is accurate and left in place. Read TASK.md and dispatch.c, then talk to the desk a few times before you measure anything.

Hint 1: work out what actually moves

The desk prints a number on every run. Before you build anything around it, work out whether it’s telling you something about this run or something about the file, because those are different claims and only one is useful.

One field in the ELF header settles the whole question, and nm is reporting a different kind of number than you might assume:

Terminal window
readelf -h ./dispatch | grep Type:
nm ./dispatch | grep -E ' T sign_off| t stamp_plain'

The desk blocks on its first read, so start it with input held open and read /proc/<pid>/maps for three separate runs, from outside a debugger. Compare the first mapping’s start across the three, and against what nm said.

Hint 2: the program prints no address, so make it print one

The echo stops at the first zero byte, so how much comes back depends on how much you sent. Ask what sits immediately after the text in memory, and whether the program put anything there worth reading. dispatch.c declares the buffer inside something larger; read that declaration, then confirm the layout in the disassembly rather than trusting it.

A pointer into the image is six significant bytes on this platform. The other two are what ends the echo. Send exactly 96 bytes, rebuild what comes back little-endian, and subtract the offset nm reports for stamp_plain.

The result should be page aligned, bottom three hex digits zero, every time. Those digits are fixed by the file and the loader never touches them, which is also why six bytes reconstruct all eight.

Hint 3: the canary needs a different leak

The canary’s first byte is zero, so an echo starting below it stops when it arrives there. That’s why Hint 2’s leak could never have given you this as well, and something has to change before the echo gets past.

The desk reads three times and answers twice, and the cap on the first read isn’t the cap on the others.

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

Take the frame from the reservation, the lea that produces the buffer’s address, and the instruction that loads fs:0x28. Send 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. Count them first: fewer than seven is a result, not a mistake.

Hint 4: the gadget the scanner says isn’t there

sign_off prints only for one argument value, so returning into it isn’t enough. You need the first argument register loaded, and a scan will tell you this program has no gadget that loads it.

The scan is right about the instructions and wrong about the bytes.

Terminal window
ROPgadget --binary ./dispatch --only 'pop|ret'

Four results out of a hundred or so gadgets, and one of them is what you want. Confirm the bytes with objdump and find the instruction those bytes are sitting inside. Code Reuse and ROP Gadgets says where to look, and main assigns something unusual on its first line. main also ends with exit, which is why exit@plt is available for the last link.

Six slots: the gadget, the argument, sign_off, the gadget again, the exit status, exit@plt. Four of the six need the base added. Before them, filler to the canary, the canary, filler for the saved frame pointer.

Hint 5: it works under GDB and never outside it

GDB disables address randomisation for its children whatever kernel.randomize_va_space says, so a chain built from addresses observed under it is built for a machine that doesn’t exist. Measure outside, and use the debugger to check your reasoning rather than to collect your numbers.

It works most of the time and occasionally not at all. Two causes, same shape. The echo stops at the first zero byte, so when one of the four randomised bytes of the leaked pointer is 0x00 the pointer comes back short, and when one of the seven canary bytes is 0x00 the canary does. Together, roughly one run in twenty-five.

Count what you received rather than assuming, and start a fresh process when it’s short, because the next one has a different base and a different canary. A retry loop is the right answer here.

Delimit each echo by the prompt that follows it rather than splitting on newlines, because a leaked byte can be 0x0a and you’ll lose it.

The whole route

The ELF type is DYN, so nm is giving you file offsets and every address has to be computed.

Leak one: send 96 bytes so the echo runs past your text into the pointer the program left after it, rebuild six bytes little-endian, subtract stamp_plain’s offset. That’s the base, and it ends in three zeros.

Leak two: send one byte past the distance from the text to the canary, so the canary’s leading zero is replaced, and rebuild as zero plus the seven returned bytes.

Chain: filler, canary, frame-pointer filler, then pop rdi ; ret found inside an immediate operand rather than as its own instruction, the argument sign_off wants, sign_off, the gadget again, your exit status, exit@plt. Add the base to every address that came from the file.

Drive it with subprocess, count bytes rather than splitting on newlines, retry on a short leak, and run it three times with no rebuild in between.

Paste exactly the line the desk 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.

A solver that got here with randomisation switched off produces the same string. Try for three consecutive runs with no rebuild in between.

If something looks wrong

The offsets on this page don’t match yours. They came from one build. Read your own with nm and objdump, and confirm every gadget’s bytes before using it.

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 desk waiting on a read.

Leave kernel.randomize_va_space at 2 when you’re done, which is where the rest of the course now expects it.