Numbers and Formats: Weighbridge
Weighbridge
Section titled “Weighbridge”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.
Get the files
Section titled “Get the files”Download weighbridge-v1.0.0.tar into ~/underdunn-course/downloads
and check it against this digest:
b61ad862f58cb94191b3d206753facc07caac4ea9f336cca28cbb9f5184abb2d weighbridge-v1.0.0.tarcd ~/underdunn-course/downloadssha256sum weighbridge-v1.0.0.tarmkdir -p ~/underdunn-course/labs/weighbridgetar -xf weighbridge-v1.0.0.tar -C ~/underdunn-course/labs/weighbridgecd ~/underdunn-course/labs/weighbridge/weighbridge-v1.0.0sha256sum -c CHECKSUMS.sha256sh build.shThe 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.
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 1boardquitRead 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 1ticket %9$squitRun it again and enter:
weigh -4 1ticket %9$pquitStamp 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.
The flag
Section titled “The flag”Paste exactly the code the bridge printed, with no surrounding whitespace: