Skip to content
Courses

Payload Writing: Vault Terminal

Stack SmashingLab 2 of 2150 minutesLab

The vault terminal is the keypad on the strongroom door. Somebody types an access code and the terminal says whether it matched. What the strongroom holds is written down inside the program, and nothing prints it.

Your goal is to get the vault’s contents printed.

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

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

build.sh uses the same flags the lessons used, including the executable stack. This lab also needs the VM setting from the last two lessons:

Terminal window
sudo sysctl -w kernel.randomize_va_space=0

Read TASK.md and vault.c, then run the terminal a few times before you measure anything, with the code it accepts and with codes it doesn’t. Notice what the accepting run prints, which is not what you might expect a vault to say.

Hint 1: this delivery path is stricter than the lessons’

Read how the terminal takes its input instead of assuming it matches the programs you’ve already broken. The function it uses stops on a class of bytes rather than on one particular byte.

Whatever that class turns out to be, the constraint applies to every byte you send. Not just the instructions: the padding and the eight bytes of address at the end are input too.

Write the forbidden values down as a list now. You’ll be scanning a complete input against it later and it’s easier than rediscovering it at midnight.

Hint 2: the frame, and an instruction that doesn’t look like one

The instruction that reserves the buffer may not be the one you’ve been looking for. GCC can reserve stack space by adding a negative number instead of subtracting a positive one, so read the operand rather than matching the mnemonic.

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

Take the buffer’s position from the lea that produces its address, apply the layout you derived in Crashes and Evidence of Control, and state two numbers: the offset to the saved return address, and the length that covers all eight of its bytes.

Hint 3: what your payload actually needs

The vault is a global, so nm gives you its address without running anything. Its contents are a separate question, and the program does something to them before it ever asks for a code.

Terminal window
nm -n ./vault | grep -i vault
strings ./vault | head -40

Run strings and note what it doesn’t show you, then read what main does first. Your payload needs the vault’s address, not its bytes.

Load that address into rsi as an immediate rather than with a rip-relative lea. Your payload won’t be running at the address it was assembled for.

Hint 4: placing it and aiming

Assemble with as, extract with objcopy, then disassemble your own extracted bytes to confirm they’re what you meant, and check the count.

Build the input as sled, then payload, then the aim address, sized so the address lands exactly at the offset from Hint 2. The sled covers the difference between the address you measured in the debugger and the one the buffer actually has in the shell, so aim into the middle of it rather than at its first byte.

Then scan the whole thing against your forbidden list. If your chosen aim contains a forbidden byte, pick a different one inside the sled.

Hint 5: two failures that look like your own mistake

The terminal prints its ordinary last line and exits cleanly. Your input never reached the return slot. Before you change any length, scan the input for a forbidden byte, including the address. The delivery path truncated you somewhere.

It works under GDB and faults in the shell. Check both addresses. The debugger’s environment isn’t the shell’s, and the sled is what covers the gap between them. Stop at the ret and check where the aim actually points:

(gdb) break *<the ret in check_code>
(gdb) run < <your input file>
(gdb) x/gx $rsp

That value has to fall inside your sled, not at its edge.

The whole route

Derive the offset and covering length from check_code’s frame, reading the reservation operand rather than the mnemonic. Get the vault’s address from nm; the contents aren’t in the file because main transforms them at run time, which is why strings shows nothing useful.

Write the write and exit pair from Writing Payloads in Assembly, with the vault’s address in rsi as an immediate. Assemble, extract, disassemble to check, and count the bytes.

Build sled + payload + aim so the aim sits at the derived offset, pick an aim in the middle of the sled, and scan every byte of the result against the forbidden list. Confirm with x/gx $rsp at the ret that the aim lands inside the sled, then run it outside the debugger and check the exit status next to the output.

Run the terminal with your input outside the debugger and paste exactly what it 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.

A result observed under GDB doesn’t establish what happens in the shell, and a clean exit doesn’t on its own establish that the program produced the intended result.

If something looks wrong

The VM setting didn’t stick. sysctl kernel.randomize_va_space reports the current value, and it resets on reboot.

The build differs from the lessons’. Restore the supplied source and run sh build.sh unmodified. It keeps the executable stack.