Skip to content
Courses

Canary Bypass: Reception Desk

MitigationsLab 1 of 4150 minutesLab

The reception desk signs visitors in. You give it a name, it prints a visitor pass, and it lets you correct the name if you got it wrong. It also holds the day’s manifest, which it never shows anyone.

Your goal is to get the manifest printed.

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

0e689f857cf23bc912db764a957b23c4f8e960769a28ab09c79a0fb136d164d4 reception-desk-v1.0.0.tar
Terminal window
cd ~/underdunn-course/downloads
sha256sum reception-desk-v1.0.0.tar
mkdir -p ~/underdunn-course/labs/reception-desk
tar -xf reception-desk-v1.0.0.tar -C ~/underdunn-course/labs/reception-desk
cd ~/underdunn-course/labs/reception-desk/reception-desk-v1.0.0
sha256sum -c CHECKSUMS.sha256
sh build.sh

The build prints two warnings. Read TASK.md and desk.c, then talk to the desk a few times before you measure anything.

Hint 1: the frame, and why the declaration lies

The build’s two warnings are a claim to check rather than a measurement, and the declaration in the source is the same. It tells you how large the array is. It doesn’t tell you where the array sits in the frame, and on this program those are two different numbers, because there’s another local below the buffer. Counting from char badge[96] gives a distance to the canary that’s too small.

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

Three instructions carry it: the one that reserves the frame, the one that stores the canary, and the lea that feeds the first read. GCC can reserve space by adding a negative number rather than subtracting a positive one, so read the operand rather than matching the mnemonic. The saved frame pointer is at rbp+0 and the saved return address at rbp+8.

Hint 2: find out what the check actually stopped

Aim the return address at the manifest routine, deliver it, and watch what the desk does. It won’t work. Establish why not.

The diagnostic tells you a comparison failed. It doesn’t tell you how far your write got. Those are different facts and only one of them is on the screen. Stop at the instruction that reloads the stored canary, before the comparison resolves, and read both slots:

(gdb) break *<the canary reload in your build>
(gdb) run < <your input file>
(gdb) x/gx $rbp+8
(gdb) x/gx $rbp-8

“The write was prevented” and “the use of the write was prevented” are different claims. Only one of them is true here.

Hint 3: the desk will tell you the value

The desk prints your name back, and the routine that prints it stops at the first zero byte. The value you want begins with a zero byte, as Leaking a Stack Canary showed.

But the thing that stops the print is itself a byte you can write to. Work out the input length that reaches exactly that byte and replaces it, and predict how many bytes of the value survive.

What comes back doesn’t stop at the end of the canary. Establish where it does stop and why, rather than assuming every returned byte belongs to the value you’re after.

Hint 4: check your reconstruction against the program

Capture the echoed line as bytes rather than as text. Rebuild the value as the zero byte you overwrote followed by what came back, in the order it came back, then read the canary at its source in the same run and compare:

(gdb) x/gx $fs_base+0x28

If the two disagree, the disagreement is in your byte order or in where you started slicing. It isn’t in the program.

Hint 5: a file cannot carry this one

The input that failed in Hint 2 can’t be reused. The second half of it contains a value that doesn’t exist until the program has read the first half and answered you, and a file is fixed before the process starts.

So drive the desk from Python with subprocess: write stage one, read the answer, write stage two into the same process. The canary also has to go back exactly where it came from, because the write that reaches the return address is one continuous write and you can’t step over it.

Two things that will waste an hour otherwise. Use os.read on the pipe’s file descriptor rather than a fixed-count read on the stream, which will block waiting for bytes the desk isn’t going to send. And wait for a marker in the desk’s output rather than for a byte count, flushing after each write.

The whole route

Derive the buffer offset from the lea, the canary offset from the instruction that stores it, and subtract. The return slot is at rbp+8.

Send stage one at exactly one byte past the distance to the canary, so the canary’s leading zero is replaced and the echo runs on into it. Capture the echo as bytes, take the seven that follow, and rebuild the canary as a zero byte followed by those.

Stage two, in order: filler to the distance, the rebuilt canary, eight bytes for the saved frame pointer, and the address of the manifest routine from nm on your own binary. Write it into the same process, then close the pipe.

Run it three times. Three different canaries, three successes. If it only works on the run that produced the value you wrote down, it isn’t finished.

Paste exactly what 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.

The canary is per process. A solver that works once and fails on the next run has recovered a number rather than learned a technique, so run it a few times before you call it done.

If something looks wrong

The build reports something other than its two warnings. Restore the supplied source and run sh build.sh unmodified.

Addresses don’t match the lessons. The desk’s own symbols agree because this build isn’t position-independent. Stack addresses and the canary will not agree, and nothing here depends on it.

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