neven@dimitri-dev:~$
cd ../hackultet-2026

$cat read-me-if-you-can.mdPwnHackultet 2026

Read Me If You Can

A service that lets you read any file on the server deletes the flag from disk before you ever connect, but the process that read it into memory is still running, and /proc can read a process's own memory directly.

  • #proc-filesystem
  • #memory-disclosure
  • #deleted-file

$man read-me-if-you-can

What this one teaches:

  • Using the /proc filesystem to learn about a running process from the outside (its environment, its command line, its parent's command line).
  • That deleting a file doesn't erase data already read into a process's memory.
  • Reading a process's own memory through /proc/self/mem once you know where to look.

$hint --one-at-a-time

Stuck? Open one hint, then go back and try again before reading the next.

Hint 1 of 2show

The flag file is deleted before you can read it directly. Where else might its contents still be sitting?

Hint 2 of 2show

The read syntax supports an offset and a length. What file, read at the right offset, would show you another process's memory?

Full solution below. Spoilers ahead.

Task description

You can read any file you want, good luck!

A plain TCP service: send a path, get its contents back, up to 4096 bytes.

The obvious files don’t exist

Guessing at /flag.txt, /app/flag.txt, /root/flag.txt all failed, so the first step was mapping out the environment through files that are always readable:

  • /etc/passwd confirmed it’s Linux and confirmed reading works at all.
  • /proc/self/environ revealed the working directory, /app.
  • /proc/self/cmdline named the running binary, ./chall.
  • /proc/1/cmdline, the very first process in the container, revealed its startup command, which happened to compile the challenge from source if a binary wasn’t already present: gcc /app/challsSource_Code.c -o chall; ./chall. That source file’s name was right there in the command line.

Reading the source

/app/challsSource_Code.c was still sitting on disk and readable. Two things in it mattered:

volatile char flag_buffer[256] = {0};
int fd = open("flag.txt", O_RDONLY);
if (fd != -1) { read(fd, (void*)flag_buffer, 255); close(fd); }
unlink("flag.txt");

The flag is read into a stack buffer once, at startup, and the file is then deleted. By the time anyone connects, flag.txt genuinely doesn’t exist on disk any more; there’s nothing left to open() directly.

char *offset_ptr = strstr(input, "::");
offset = strtoull(offset_ptr + 2, NULL, 0);
char *length_ptr = strstr(offset_ptr + 2, "::");
length = strtoul(length_ptr + 2, NULL, 0);
ssize_t bytes_read = pread(fd, buf, length, offset);

Less obviously, the input format supports filepath::offset::length, letting a request read from an arbitrary byte offset within a file, not just from the start.

Why the flag isn’t actually gone

Deleting a file on Linux removes its name from the directory, not necessarily its data; a file only truly disappears once nothing still has it open, and the process that read the flag into flag_buffer is still running, with that buffer still sitting on its own stack. /proc/<pid>/mem exposes a running process’s memory directly, readable (with the right offset and length) just like any other file, and self is a shortcut to the currently running process, here the very same one holding the flag in memory.

Finding the stack and reading it

/proc/self/maps lists every memory region a process has mapped, including one tagged [stack] with its address range. Once that range is known, reading /proc/self/mem at that offset, for that length, using the service’s own ::offset::length syntax, dumps the entire stack, flag buffer included, in plaintext.

# Step 1: get the stack range from /proc/self/maps
# → e.g. 0x7ffd5dc9e000-0x7ffd5dcbf000

# Step 2: read it back through /proc/self/mem
send(f'/proc/self/mem::{stack_start}::{stack_size}\n')

# Step 3: pull the flag out of the dumped bytes
import re
match = re.search(rb'CTF2026\[.*?\]', dumped_data)

Result

CTF2026[794780859246]

The idea underneath it

The program’s own defence, read the secret once, then delete the file, assumed that deleting the file was enough. It wasn’t, because the data had already moved from disk into a running process’s memory, and nothing in the program ever stopped an arbitrary file read primitive from reaching that process’s own memory through /proc.

$cat flag.txt revealCTF2026[794780859246]

Written by Neven Marić. This writeup was written with the help of Claude Code (Anthropic).