Skip to content
Courses

Numbers and Formats: Weighbridge

Bug Classes and PrimitivesLab 2 of 3150 minutesLab

The weighbridge weighs lorries on the way in and prints a ticket for each one. It hands over its vault code to an operator who has stamped a ticket and can quote the duty token back to it. It won’t print any ticket until the bridge has been cleared for the day, and it never prints the duty token.

Your goal is to get the vault code printed.

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

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

The build prints one warning. Read TASK.md and bridge.c, then talk to the bridge for a while before you measure anything.

Hint 1: the check covers one end of the range

Take the check on the one command that writes a number where you ask, and describe the set of values it rejects. Then describe the set it accepts. Those two sets don’t add up to all the values the command can be given.

The variable you want isn’t inside the array. It doesn’t need to be.

Terminal window
nm ./bridge | grep -E ' [bBdD] (cleared|weights|stamped)$'

The symbols are absolute at link time, and the array’s element type gives you the size of one step. Subtract, divide, and mind the sign. Note what nm also says about stamped, because you’ll need it later and the answer is that this command can’t reach it.

Hint 2: reading the token

The build printed one warning, and it is about a call whose first argument is yours. Everything that call does with the rest of the stack is available to you. It is worth noticing that this warning is about the second half of the chain and that nothing mentions the first.

Positional arguments let you ask for one thing without printing the nine before it. A sweep of %N$p across the first dozen positions costs one run and shows you the shape of the frame. Compare it against the locals of the function making the call, which are right there in the source, and notice that one of those locals exists for no reason other than to hold a pointer to what you want.

Start by asking what each position holds, and only then ask one of them to be treated as a string. Take the %p reading as well as the %s one. You’ll want the address shortly.

Hint 3: writing with the same call

The command from Hint 1 can’t reach the stamp flag, because that flag sits on the side of the array the range check actually covers. So write to it with the call you just read with.

You have an address from Hint 2 and a known distance between two symbols, which is enough to compute the target on this run without leaking anything else.

To write, the address has to be in the format string, and you need a directive that names the position where it landed. The position walk already showed you where your own buffer starts: past a certain position, what comes back is the ASCII of what you sent, eight bytes at a time.

Hint 4: make the directive exactly eight bytes

Once you know the buffer begins at position p, the eight bytes after your first eight are at p + 1. That only holds if your directive is exactly eight bytes long, and getting this wrong is the usual reason a write silently does nothing.

payload = b"AAA%11$n" + struct.pack("<Q", token_address - 0x10)

AAA%11$n is eight characters and writes 3, which is all the bridge asks for.

Check the stamp with board rather than inferring it from the release attempt. That separates “the write didn’t happen” from “the write happened and the release wants something else”.

Hint 5: two failures that look like your fault

It segfaults on a negative slot. You’ve gone further back than the flag. Re-derive the index from the two addresses and the element size rather than adjusting it by one and trying again.

A sanitizer build reports your first bug and then stops working. Both are correct and have the same cause. AddressSanitizer inserts redzones around globals, which is how it sees the write, and moving the globals apart means your index no longer lands where you computed it. Measure on the plain build and use the instrumented one only to confirm the class.

The whole route

Clear it. cleared is thirty-two bytes before weights, and weights holds eight-byte elements, so it’s four elements back. if (slot >= SLOTS) is entirely correct about large numbers and silent about negative ones.

Run ./bridge and enter:

weigh -4 1
board
quit

Read the token. Position 9 is the stamp local, which points at duty_token. With %s it’s the token; with %p it’s the token’s runtime address.

Run ./bridge and enter:

weigh -4 1
ticket %9$s
quit

Run it again and enter:

weigh -4 1
ticket %9$p
quit

Stamp it. stamped is 0x10 below duty_token, so the leaked address minus sixteen is the target. Your text starts at position 10, so an eight-byte directive puts the address at position 11. Send AAA%11$n followed by the packed address as a ticket, then release with the token.

Paste exactly the code the bridge 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.