ASLR Bypass: Night Dispatch
Night Dispatch
Section titled “Night Dispatch”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.
Get the files
Section titled “Get the files”Download night-dispatch-v1.0.0.tar into
~/underdunn-course/downloads and check it against this digest:
f02eb3f188fee5f75589e93b16f693192cc7d705debfe62423b83bbb0cf529b3 night-dispatch-v1.0.0.tarcd ~/underdunn-course/downloadssha256sum night-dispatch-v1.0.0.tarmkdir -p ~/underdunn-course/labs/night-dispatchtar -xf night-dispatch-v1.0.0.tar -C ~/underdunn-course/labs/night-dispatchcd ~/underdunn-course/labs/night-dispatch/night-dispatch-v1.0.0sha256sum -c CHECKSUMS.sha256sh build.shThe 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:
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.
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.
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.
The flag
Section titled “The flag”Paste exactly the line the desk printed, with no surrounding whitespace:
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.