Skip to content
Courses

Overflow and Reuse: Bonded Store

The HeapLab 2 of 2120 minutesLab

The bonded store holds goods until customs clears them. It hands over its vault code once a consignment has been cleared and you quote the seal word back to it. Nothing clears a consignment, and nothing prints the seal word.

Your goal is to get the vault code printed.

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

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

The build is silent under -Wall -Wextra.

Hint 1: where to look

Use the store normally for a while before you try to make anything go wrong, and run board after every command so you can see what it thinks it has.

Then compare what the store allocates with what it hands back. Those two sets don’t cancel out the way you might expect, and one record goes back much earlier than you would guess from reading the commands.

Hint 2: the seal word

The build was silent, including under -Wuse-after-free, so nothing is going to point you at this.

A record that has been handed back isn’t erased. The allocator writes over the beginning of it for its own bookkeeping and leaves everything past that untouched, and the lesson on use after free measured exactly how many bytes that is.

The word itself is not in the built file, so strings will not find it. Two different things the store allocates ask for the same number of bytes, which is what decides whether you can be handed the same memory back. And only one command prints a whole fixed-size record rather than stopping at the first terminator.

Hint 3: finding the approval record

For the clearance flag, start from the other end: only one command writes bytes you supply into a record, and it writes as many of them as you tell it to.

The store prints the address of every record it opens, so two commands and a subtraction settle the distance without a debugger. The offset you actually want is eight less than that distance, because a chunk’s header sits in front of a record rather than inside it.

Hint 4: getting past the header

You can’t reach a neighbouring record’s contents without writing across its header on the way, so the question is what that header holds and whether you can put it back.

You can. Chunk sizes are quantised, so a 24-byte request has exactly one possible size field, and nothing about it is secret.

Use board to check whether the flag took. That confirms the write on its own, separately from whether release then works.

Hint 5: the flag is set and release still kills the process

This is what a first attempt does. The write succeeded. Something else didn’t.

release does one thing before it prints anything at all, and that operation reads the header you wrote over on your way to the flag. Whatever you left in those eight bytes, release is about to take it seriously.

The whole route

The seal. close hands the ledger back. A note is the same 48-byte request the ledger was, so the note is served the ledger’s chunk. note copies with strcpy, so a one-character note disturbs one byte, and read prints the whole fixed-size record instead of stopping at the terminator. The allocator damaged the first sixteen bytes on the way out; the seal sits at offset 24, past them.

Run ./bond and enter:

close
note .
read
quit

The distance. open 0 and approve are both 24-byte requests, so both are 0x20 chunks. The approval record opens 32 bytes after slot 0, which puts its size field at slot 0 + 0x18 and its first field at slot 0 + 0x20.

Run ./bond and enter:

open 0
approve
board
quit

The payload. Twenty-four bytes of filler, then the eight bytes 21 00 00 00 00 00 00 00 to put the header back, then a non-zero four-byte integer for the flag. Thirty-six bytes, sent with a driver rather than a shell quote:

import sys
payload = b"L" * 24 + (0x21).to_bytes(8, "little") + (1).to_bytes(4, "little")
sys.stdout.buffer.write(b"open 0\napprove\nlabel 0 %d\n" % len(payload) + payload
+ b"\nboard\nrelease <the seal from the first step>\nquit\n")

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

If a sanitizer build behaves oddly

AddressSanitizer reports both bugs immediately, which is correct, and it also moves every record, which means the distance above no longer holds and the lab isn’t solvable on that build. Measure on the plain build and use the instrumented one only to confirm the class.